Class: ActiveSanction::PartialDate
- Inherits:
-
Object
- Object
- ActiveSanction::PartialDate
- Extended by:
- T::Sig
- Defined in:
- lib/active_sanction/partial_date.rb,
lib/active_sanction/partial_date/parser.rb
Overview
A date a sanctions list published imprecisely. Date cannot hold one:
collapsing "1972" to 1972-01-01 invents a precision the publisher never
claimed, and a scorer that believes it will call 1972 and 1972-04-29 a
conflict when they are in fact a match.
PartialDate.parse("1972") # year only
PartialDate.parse("circa 1962") # approximate
PartialDate.parse("between 1971 and 1973") # a span
PartialDate.new(year: 1965, month: 4, day: 29)
Every instance carries a first and last possible date, which is what makes #overlaps? and #conflicts_with? exact regardless of how precise either side is. Instances are frozen on construction and compare by value.
Constant Summary collapse
- PRECISIONS =
:range is a precision in the sense the scorer (#32) cares about -- how much of the calendar a date could be -- not a grammatical one.
T.let(%i[year month day range].freeze, T::Array[Symbol])
Instance Attribute Summary collapse
- #approximate ⇒ Boolean readonly
- #day ⇒ Integer? readonly
-
#first_date ⇒ Date
readonly
The two dates that bound this one, whatever shape it is -- which is what makes #overlaps? exact regardless of how precise either side is.
- #from ⇒ PartialDate? readonly
- #last_date ⇒ Date readonly
- #month ⇒ Integer? readonly
- #to ⇒ PartialDate? readonly
-
#year ⇒ Integer?
readonly
A point date carries year/month/day and no endpoints; a range carries its endpoints and no year of its own.
Class Method Summary collapse
-
.from_h(hash) ⇒ T.attached_class
Rebuilds a date from #to_h output.
-
.parse(text) ⇒ PartialDate?
Reads a date expression from free text, returning nil on anything it cannot read.
-
.range(from, to, approximate: false) ⇒ T.attached_class
A span, from the UN's
TYPE_OF_DATE = BETWEEN.
Instance Method Summary collapse
- #==(other) ⇒ Boolean (also: #eql?)
- #approximate? ⇒ Boolean
-
#comparison_range ⇒ Range<Date>
Widened by APPROXIMATE_SLACK_YEARS when the publisher said circa.
-
#conflicts_with?(other) ⇒ Boolean
The strict complement of #overlaps? for two known dates.
- #hash ⇒ Integer
-
#initialize(year: nil, month: nil, day: nil, from: nil, to: nil, approximate: false) ⇒ void
constructor
Untyped on purpose, and the same choice Entity makes: these arrive as whatever a publisher wrote and a parser made of it.
- #inspect ⇒ String
-
#overlaps?(other) ⇒ Boolean
True when the two dates could describe the same day.
- #precision ⇒ Symbol
- #range? ⇒ Boolean
- #to_h ⇒ Hash{Symbol => T.untyped}
-
#to_range ⇒ Range<Date>
Every date this could be, which is the whole point of the type.
-
#to_s ⇒ String
Renders in a form .parse reads back, so a date survives a trip through free text -- which is how OFAC publishes them in the first place.
Constructor Details
#initialize(year: nil, month: nil, day: nil, from: nil, to: nil, approximate: false) ⇒ void
Untyped on purpose, and the same choice Entity makes: these arrive as whatever a publisher wrote and a parser made of it. What comes back out is typed -- see the readers above.
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/active_sanction/partial_date.rb', line 109 def initialize(year: nil, month: nil, day: nil, from: nil, to: nil, approximate: false) @approximate = T.let(approximate ? true : false, T::Boolean) @year = T.let(nil, T.nilable(Integer)) @month = T.let(nil, T.nilable(Integer)) @day = T.let(nil, T.nilable(Integer)) @from = T.let(nil, T.nilable(PartialDate)) @to = T.let(nil, T.nilable(PartialDate)) # Both branches assign the members they own and hand back the pair of # dates that bound them, which is what makes those two non-nil for every # instance rather than for most of them. first, last = if from.nil? && to.nil? assign_point(year, month, day) else reject_mixed_shape(year, month, day) assign_range(from, to) end @first_date = T.let(first, Date) @last_date = T.let(last, Date) freeze end |
Instance Attribute Details
#approximate ⇒ Boolean (readonly)
65 66 67 |
# File 'lib/active_sanction/partial_date.rb', line 65 def approximate @approximate end |
#day ⇒ Integer? (readonly)
56 57 58 |
# File 'lib/active_sanction/partial_date.rb', line 56 def day @day end |
#first_date ⇒ Date (readonly)
The two dates that bound this one, whatever shape it is -- which is what makes #overlaps? exact regardless of how precise either side is. Never nil: #initialize derives both for every instance it will build.
71 72 73 |
# File 'lib/active_sanction/partial_date.rb', line 71 def first_date @first_date end |
#from ⇒ PartialDate? (readonly)
59 60 61 |
# File 'lib/active_sanction/partial_date.rb', line 59 def from @from end |
#last_date ⇒ Date (readonly)
74 75 76 |
# File 'lib/active_sanction/partial_date.rb', line 74 def last_date @last_date end |
#month ⇒ Integer? (readonly)
53 54 55 |
# File 'lib/active_sanction/partial_date.rb', line 53 def month @month end |
#to ⇒ PartialDate? (readonly)
62 63 64 |
# File 'lib/active_sanction/partial_date.rb', line 62 def to @to end |
#year ⇒ Integer? (readonly)
A point date carries year/month/day and no endpoints; a range carries its endpoints and no year of its own. Which is which is #range?.
50 51 52 |
# File 'lib/active_sanction/partial_date.rb', line 50 def year @year end |
Class Method Details
.from_h(hash) ⇒ T.attached_class
Rebuilds a date from #to_h output. Accepts string keys, so a record that has been through JSON round-trips without a separate coercion step.
87 88 89 90 91 92 93 |
# File 'lib/active_sanction/partial_date.rb', line 87 def self.from_h(hash) attributes = hash.to_h.transform_keys(&:to_sym) unknown = attributes.keys - MEMBERS raise InvalidArgument, "unknown PartialDate attribute(s): #{unknown.join(", ")}" if unknown.any? new(**attributes) end |
.parse(text) ⇒ PartialDate?
Reads a date expression from free text, returning nil on anything it cannot read. The vocabulary lives in Parser, which is where new source spellings get added.
80 81 82 |
# File 'lib/active_sanction/partial_date.rb', line 80 def self.parse(text) Parser.call(text) end |
.range(from, to, approximate: false) ⇒ T.attached_class
A span, from the UN's TYPE_OF_DATE = BETWEEN. Endpoints may be
PartialDates, #to_h hashes, or strings this class can parse.
98 99 100 |
# File 'lib/active_sanction/partial_date.rb', line 98 def self.range(from, to, approximate: false) new(from: from, to: to, approximate: approximate) end |
Instance Method Details
#==(other) ⇒ Boolean Also known as: eql?
195 196 197 198 199 |
# File 'lib/active_sanction/partial_date.rb', line 195 def ==(other) return false unless other.instance_of?(self.class) to_h == other.to_h end |
#approximate? ⇒ Boolean
135 |
# File 'lib/active_sanction/partial_date.rb', line 135 def approximate? = approximate |
#comparison_range ⇒ Range<Date>
Widened by APPROXIMATE_SLACK_YEARS when the publisher said circa.
175 176 177 178 179 |
# File 'lib/active_sanction/partial_date.rb', line 175 def comparison_range return to_range unless approximate? first_date.prev_year(APPROXIMATE_SLACK_YEARS)..last_date.next_year(APPROXIMATE_SLACK_YEARS) end |
#conflicts_with?(other) ⇒ Boolean
The strict complement of #overlaps? for two known dates. A missing date is not a conflict -- nobody claimed anything to contradict -- so nil answers false to both questions.
167 168 169 170 171 |
# File 'lib/active_sanction/partial_date.rb', line 167 def conflicts_with?(other) return false if other.nil? !overlaps?(other) end |
#hash ⇒ Integer
203 204 205 |
# File 'lib/active_sanction/partial_date.rb', line 203 def hash [self.class, to_h].hash end |
#inspect ⇒ String
208 209 210 |
# File 'lib/active_sanction/partial_date.rb', line 208 def inspect "#<#{self.class} #{self} precision=#{precision.inspect}>" end |
#overlaps?(other) ⇒ Boolean
True when the two dates could describe the same day. Precision does not have to match: a year-only date overlaps every full date inside it, which is what lets #32 treat 1972 against 1972-04-29 as a moderate boost rather than a miss.
155 156 157 158 159 160 161 |
# File 'lib/active_sanction/partial_date.rb', line 155 def overlaps?(other) return false if other.nil? mine = comparison_range theirs = comparable!(other).comparison_range mine.first <= theirs.last && theirs.first <= mine.last end |
#precision ⇒ Symbol
138 139 140 141 142 143 144 |
# File 'lib/active_sanction/partial_date.rb', line 138 def precision return :range if range? return :day if day return :month if month :year end |
#range? ⇒ Boolean
132 |
# File 'lib/active_sanction/partial_date.rb', line 132 def range? = !from.nil? |
#to_h ⇒ Hash{Symbol => T.untyped}
182 183 184 |
# File 'lib/active_sanction/partial_date.rb', line 182 def to_h { year: year, month: month, day: day, from: from&.to_h, to: to&.to_h, approximate: approximate } end |
#to_range ⇒ Range<Date>
Every date this could be, which is the whole point of the type.
148 |
# File 'lib/active_sanction/partial_date.rb', line 148 def to_range = first_date..last_date |
#to_s ⇒ String
Renders in a form .parse reads back, so a date survives a trip through free text -- which is how OFAC publishes them in the first place.
189 190 191 192 |
# File 'lib/active_sanction/partial_date.rb', line 189 def to_s text = range? ? "#{from} to #{to}" : point_to_s approximate? ? "circa #{text}" : text end |