Module: ActiveSanction::Country
- Extended by:
- T::Sig
- Defined in:
- lib/active_sanction/country.rb
Overview
Which country a publisher meant, as an ISO 3166-1 alpha-2 code.
ActiveSanction::Country.code("Russia") # => "RU"
ActiveSanction::Country.code("RUS") # => "RU"
ActiveSanction::Country.code("Russian Federation") # => "RU"
ActiveSanction::Country.code("Ruritania") # => nil
ActiveSanction::Country.name("RU") # => "Russian Federation"
Why the scorer cannot compare these as strings
Nationality is the one identifier on these records that is published as
prose. A caller screening a customer has an ISO code in a database column;
the UN files NATIONALITY/VALUE as whatever its Committee wrote, and OFAC
writes nationality into the middle of a remarks sentence. So a query of
RU meets a record of Russian Federation, and a query of Iran meets
Iran, Islamic Republic of.
Compared as strings those are three disagreements, and a scorer that reads a disagreement as a conflict does not merely fail to boost the match -- it penalizes it, on exactly the records where the caller supplied the most information. That is the worst shape a false negative can take, so the comparison happens on a resolved code or it does not happen at all: see Scorer::Adjustments, which treats an unresolvable country as absent rather than as a conflict.
What resolution is, and what it is not
A lookup, not a guess. Every spelling in countries.txt -- the alpha-2,
the alpha-3, the ISO name and the aliases a list actually writes -- is
folded once at load and mapped to its alpha-2 code, and a string that is
not in that table returns nil. There is no fuzzy matching here on purpose:
Niger and Nigeria are two countries, Guinea is three, and a scorer
that resolved a country approximately would be applying a decisive
adjustment on a guess.
Input is folded by the same Normalizer::Form names are folded by, which
is what makes côte d'ivoire, Cote D Ivoire and CÔTE-D'IVOIRE one
lookup. It is folded with no stoplist: a country is not an entity, and the
organization lists would take Republic out of half this table.
The table is a data file
lib/active_sanction/countries.txt, one country per line, for the reason
the normalizer's dictionaries are files: what belongs on it is settled by
reading government lists rather than by reading Ruby, and a contributor
adding the spelling their market uses should be sending a one-line diff.
Class Method Summary collapse
-
.code(value) ⇒ String?
The alpha-2 code for a name, a code or an alias, or nil for anything not in the table.
-
.codes ⇒ Array<String>
Every alpha-2 code in the table, in file order.
-
.fold(value) ⇒ String
The fold a lookup key goes through.
-
.name(value) ⇒ String?
The ISO name for an alpha-2 code, for a hit a person has to read.
-
.read(path = TABLE) ⇒ Array<(Hash{String => String}, Hash{String => String})>
Reads the shipped table, or another one shaped like it.
Class Method Details
.code(value) ⇒ String?
The alpha-2 code for a name, a code or an alias, or nil for anything not in the table. nil and blank are nil rather than an error: an absent nationality is the common case on these lists, and the scorer asks about it record by record.
75 76 77 78 79 80 |
# File 'lib/active_sanction/country.rb', line 75 def code(value) return nil if value.nil? key = fold(value.to_s) key.empty? ? nil : CODES[key] end |
.codes ⇒ Array<String>
Every alpha-2 code in the table, in file order.
94 |
# File 'lib/active_sanction/country.rb', line 94 def codes = NAMES.keys |
.fold(value) ⇒ String
The fold a lookup key goes through. Public because a collision in the table is a fact about folded strings rather than about the file, and the suite checks for one.
100 |
# File 'lib/active_sanction/country.rb', line 100 def fold(value) = Normalizer::Form.new(value).value |
.name(value) ⇒ String?
The ISO name for an alpha-2 code, for a hit a person has to read. The ISO name rather than the alias a publisher wrote, deliberately: the published string is still on the record, and an explanation that names one country two ways is one a reviewer has to reconcile.
87 88 89 90 |
# File 'lib/active_sanction/country.rb', line 87 def name(value) found = code(value) found && NAMES[found] end |
.read(path = TABLE) ⇒ Array<(Hash{String => String}, Hash{String => String})>
Reads the shipped table, or another one shaped like it. Returns the pair the constants below hold: spelling => alpha-2, and alpha-2 => ISO name.
A spelling claimed twice raises rather than resolving to whichever line came first. Two countries quietly sharing a name would move a nationality adjustment onto the wrong record, and it would do it silently -- the table would still load and every lookup would still answer.
112 113 114 115 116 117 118 119 120 121 |
# File 'lib/active_sanction/country.rb', line 112 def read(path = TABLE) codes = {} names = {} each_row(path) do |fields| alpha2 = T.must(fields.first) names[alpha2] = fields[2] fields.each { |field| claim(codes, field, alpha2) } end [codes.freeze, names.freeze] end |