Class: ActiveSanction::Scorer::Subject
- Inherits:
-
Object
- Object
- ActiveSanction::Scorer::Subject
- Extended by:
- T::Sig
- Defined in:
- lib/active_sanction/scorer/subject.rb
Overview
What the caller knows about the person or company being screened.
subject = ActiveSanction::Scorer::Subject.new(
name: "Bosco Ntaganda",
type: :individual,
dates_of_birth: "1973",
nationalities: %w[CD],
identifiers: [{ kind: :passport, value: "AB-123 456" }]
)
subject.form.value # => "bosco ntaganda"
One side of every comparison the scorer makes, and the mirror image of the Entity on the other side: the same four kinds of evidence, arriving from an application's own customer record rather than from a government.
Only name is required, and that is the shape of the problem
Most callers have a name and little else, and most records carry less than that -- Canada supplies no aliases and often no date of birth. So every field but the name is optional on both sides, and a field absent on either side is neutral rather than a conflict. See Adjustments, where that rule is the difference between a screening tool and a tool that systematically under-scores the sparser lists.
type decides two things
It is the entity type the caller is asking about, and it does two jobs
that are easy to confuse. It selects the normalizer's stoplists, so a
company name is folded with its legal form stripped -- and both sides of
a comparison have to be folded the same way, which is why the scorer
folds each candidate name under its own entity's type. And it filters:
a subject that says :individual is never scored against a vessel, at
any name similarity. See Scorer.
Passing no type is a legitimate answer and a different question -- no stoplist, no filter -- rather than a worse one.
The fold happens once, here
form is the folded name, produced by the one Normalizer.call every
other stage uses, and held for the life of the subject. A screening call
compares one subject against a few hundred candidates, and folding the
query per candidate would be the same string folded a few hundred times.
A caller that has already folded a name passes the Form, which is what screening one name against several indexes should do.
Where Query fits
This is the scorer's input, not the library's public screening API. The
Query object (#33) validates what a host application sends -- a
threshold, a limit, a source filter -- and builds one of these for the
matcher to score with. Everything on this class is evidence about a
subject; nothing on it is a search option.
Instances are frozen on construction and compare by value.
Instance Attribute Summary collapse
-
#countries ⇒ Array<String>
readonly
The alpha-2 codes #nationalities resolved to, which may be shorter than the list it came from: a value Country does not recognize is dropped here rather than guessed at, and the scorer treats a subject whose countries did not all resolve as one that cannot contradict a record.
- #dates_of_birth ⇒ Array<PartialDate> readonly
-
#form ⇒ Normalizer::Form
readonly
The folded name every comparison runs against.
- #identifiers ⇒ Array<Identifier> readonly
-
#nationalities ⇒ Array<String>
readonly
As published by the caller, in the caller's own vocabulary:
RU,RussiaandRussian Federationare all fine here. - #type ⇒ Symbol? readonly
Class Method Summary collapse
Instance Method Summary collapse
- #==(other) ⇒ Boolean (also: #eql?)
-
#countries? ⇒ Boolean
True when every nationality the caller gave resolved to a country.
- #dates_of_birth? ⇒ Boolean
- #hash ⇒ Integer
- #identifiers? ⇒ Boolean
-
#initialize(name:, type: nil, dates_of_birth: [], nationalities: [], identifiers: []) ⇒ void
constructor
nameis a String, a Name or an already-folded Form. - #inspect ⇒ String
-
#name ⇒ String
The name as the caller wrote it, which is what a report quotes back.
- #to_h ⇒ Hash{Symbol => T.untyped}
Constructor Details
#initialize(name:, type: nil, dates_of_birth: [], nationalities: [], identifiers: []) ⇒ void
name is a String, a Name or an already-folded Form. The three
collections each accept a single value as a collection of one, since
dates_of_birth: "1973" is what a caller with one date writes.
Dates accept anything PartialDate reads, including the free text these lists publish; identifiers accept an Identifier, its hash, or a bare document number, which becomes an identifier of unstated kind.
116 117 118 119 120 121 122 123 124 |
# File 'lib/active_sanction/scorer/subject.rb', line 116 def initialize(name:, type: nil, dates_of_birth: [], nationalities: [], identifiers: []) @type = T.let(type!(type), T.nilable(Symbol)) @form = T.let(form!(name), Normalizer::Form) @dates_of_birth = T.let(Array(dates_of_birth).map { |value| date!(value) }.freeze, T::Array[PartialDate]) @nationalities = T.let(strings(nationalities), T::Array[String]) @identifiers = T.let(Array(identifiers).map { |value| identifier!(value) }.freeze, T::Array[Identifier]) @countries = T.let(resolve(@nationalities), T::Array[String]) freeze end |
Instance Attribute Details
#countries ⇒ Array<String> (readonly)
The alpha-2 codes #nationalities resolved to, which may be shorter than the list it came from: a value Country does not recognize is dropped here rather than guessed at, and the scorer treats a subject whose countries did not all resolve as one that cannot contradict a record. See Adjustments.
136 137 138 |
# File 'lib/active_sanction/scorer/subject.rb', line 136 def countries @countries end |
#dates_of_birth ⇒ Array<PartialDate> (readonly)
85 86 87 |
# File 'lib/active_sanction/scorer/subject.rb', line 85 def dates_of_birth @dates_of_birth end |
#form ⇒ Normalizer::Form (readonly)
The folded name every comparison runs against.
79 80 81 |
# File 'lib/active_sanction/scorer/subject.rb', line 79 def form @form end |
#identifiers ⇒ Array<Identifier> (readonly)
94 95 96 |
# File 'lib/active_sanction/scorer/subject.rb', line 94 def identifiers @identifiers end |
#nationalities ⇒ Array<String> (readonly)
As published by the caller, in the caller's own vocabulary: RU,
Russia and Russian Federation are all fine here. See #countries
for the resolved form the scorer compares on.
91 92 93 |
# File 'lib/active_sanction/scorer/subject.rb', line 91 def nationalities @nationalities end |
#type ⇒ Symbol? (readonly)
82 83 84 |
# File 'lib/active_sanction/scorer/subject.rb', line 82 def type @type end |
Class Method Details
.from_h(hash) ⇒ T.attached_class
97 98 99 100 101 102 103 |
# File 'lib/active_sanction/scorer/subject.rb', line 97 def self.from_h(hash) attributes = hash.to_h.transform_keys(&:to_sym) unknown = attributes.keys - MEMBERS raise InvalidArgument, "unknown Subject attribute(s): #{unknown.join(", ")}" if unknown.any? T.unsafe(self).new(**attributes) end |
Instance Method Details
#==(other) ⇒ Boolean Also known as: eql?
161 162 163 164 165 |
# File 'lib/active_sanction/scorer/subject.rb', line 161 def ==(other) return false unless other.instance_of?(self.class) to_h == other.to_h end |
#countries? ⇒ Boolean
True when every nationality the caller gave resolved to a country. A conflict penalty is only applied when both sides can say this.
141 |
# File 'lib/active_sanction/scorer/subject.rb', line 141 def countries? = nationalities.any? && countries.size == nationalities.uniq.size |
#dates_of_birth? ⇒ Boolean
144 |
# File 'lib/active_sanction/scorer/subject.rb', line 144 def dates_of_birth? = dates_of_birth.any? |
#hash ⇒ Integer
169 |
# File 'lib/active_sanction/scorer/subject.rb', line 169 def hash = [self.class, to_h].hash |
#identifiers? ⇒ Boolean
147 |
# File 'lib/active_sanction/scorer/subject.rb', line 147 def identifiers? = identifiers.any? |
#inspect ⇒ String
172 |
# File 'lib/active_sanction/scorer/subject.rb', line 172 def inspect = "#<#{self.class} #{name.inspect}#{" type=#{type.inspect}" if type}>" |
#name ⇒ String
The name as the caller wrote it, which is what a report quotes back.
128 |
# File 'lib/active_sanction/scorer/subject.rb', line 128 def name = form.original |
#to_h ⇒ Hash{Symbol => T.untyped}
150 151 152 153 154 155 156 157 158 |
# File 'lib/active_sanction/scorer/subject.rb', line 150 def to_h { name: name, type: type, dates_of_birth: dates_of_birth.map(&:to_h), nationalities: nationalities, identifiers: identifiers.map(&:to_h) } end |