Class: CocinaDisplay::Dates::Date
- Inherits:
-
Object
- Object
- CocinaDisplay::Dates::Date
- Includes:
- Comparable
- Defined in:
- lib/cocina_display/dates/date.rb
Overview
A date to be converted to a Date object.
Direct Known Subclasses
DateRange, EdtfFormat, ExtractorDateFormat, Iso8601Format, MarcFormat, W3cdtfFormat
Constant Summary collapse
- UNPARSABLE_VALUES =
List of values that we shouldn’t even attempt to parse.
["0000-00-00", "9999", "uuuu", "[uuuu]"].freeze
- BCE_CHAR_SORT_MAP =
Used to sort BCE dates correctly in lexicographic order.
{"0" => "9", "1" => "8", "2" => "7", "3" => "6", "4" => "5", "5" => "4", "6" => "3", "7" => "2", "8" => "1", "9" => "0"}.freeze
Instance Attribute Summary collapse
-
#cocina ⇒ Object
readonly
Returns the value of attribute cocina.
-
#date ⇒ Object
readonly
Returns the value of attribute date.
-
#encoding ⇒ String?
The encoding name of this date, if specified.
-
#type ⇒ String?
The type of this date, if any, such as “creation”, “publication”, etc.
Class Method Summary collapse
-
.format_date(date, precision, allowed_precisions) ⇒ Object
Returns the date in the format specified by the precision.
-
.from_cocina(cocina) ⇒ CocinaDisplay::Date
Construct a Date from parsed Cocina data.
-
.normalize_to_edtf(value) ⇒ String
Apply any encoding-specific munging or text extraction logic.
- .notifier ⇒ Object
-
.parse_date(value) ⇒ Date?
Parse a string to a Date object according to the given encoding.
Instance Method Summary collapse
- #<=>(other) ⇒ Integer?
-
#approximate? ⇒ Boolean
Was the date marked as approximate?.
-
#as_range ⇒ Range<Date>?
Range of Dates between earliest possible date and latest possible date.
-
#base_value ⇒ String
Value reduced to digits and hyphen.
-
#decoded_value(allowed_precisions: [:day, :month, :year, :decade, :century, :unknown]) ⇒ String
Decoded version of the date with “BCE” or “CE”.
-
#earliest_date ⇒ Date
Earliest possible date encoded in data, respecting unspecified/imprecise info.
-
#encoding? ⇒ Boolean
Was an encoding declared for this date?.
-
#end? ⇒ Boolean
Is this the end date in a range?.
-
#inferred? ⇒ Boolean
Was the date marked as inferred?.
-
#initialize(cocina) ⇒ Date
constructor
A new instance of Date.
-
#known? ⇒ Boolean
Does this represent a known date?.
-
#label ⇒ String
Label used to group the date for display.
-
#latest_date ⇒ Date
Latest possible date encoded in data, respecting unspecified/imprecise info.
-
#parsable? ⇒ Boolean
Is the value present and not a known unparsable value like “9999”?.
-
#parsed_date? ⇒ Boolean
Did we successfully parse a date from the Cocina data?.
-
#precision ⇒ Symbol
How precise is the parsed date information?.
-
#primary? ⇒ Boolean
Was the date marked as primary?.
-
#qualified? ⇒ Boolean
Does this date have a qualifier? E.g.
-
#qualified_value ⇒ Object
Decoded date with “BCE” or “CE” and qualifier markers applied.
-
#qualifier ⇒ String?
The qualifier for this date, if any, such as “approximate”, “inferred”, etc.
-
#questionable? ⇒ Boolean
Was the date marked as approximate?.
-
#sort_key ⇒ String
Key used to sort this date.
-
#start? ⇒ Boolean
Is this the start date in a range?.
-
#to_a ⇒ Array<Date>
Array of all individual Dates that are described by the data.
-
#to_s ⇒ String
The string representation of the date for display.
-
#value ⇒ String
The text representation of the date, as stored in Cocina.
Constructor Details
#initialize(cocina) ⇒ Date
Returns a new instance of Date.
107 108 109 110 111 112 |
# File 'lib/cocina_display/dates/date.rb', line 107 def initialize(cocina) @cocina = cocina @date = self.class.parse_date(cocina["value"]) @type = cocina["type"] unless ["start", "end"].include?(cocina["type"]) @encoding = cocina.dig("encoding", "code") end |
Instance Attribute Details
#cocina ⇒ Object (readonly)
Returns the value of attribute cocina.
96 97 98 |
# File 'lib/cocina_display/dates/date.rb', line 96 def cocina @cocina end |
#date ⇒ Object (readonly)
Returns the value of attribute date.
96 97 98 |
# File 'lib/cocina_display/dates/date.rb', line 96 def date @date end |
#encoding ⇒ String?
The encoding name of this date, if specified.
105 106 107 |
# File 'lib/cocina_display/dates/date.rb', line 105 def encoding @encoding end |
#type ⇒ String?
The type of this date, if any, such as “creation”, “publication”, etc.
100 101 102 |
# File 'lib/cocina_display/dates/date.rb', line 100 def type @type end |
Class Method Details
.format_date(date, precision, allowed_precisions) ⇒ Object
allowed_precisions should be ordered by granularity, with most specific first.
Returns the date in the format specified by the precision. Supports e.g. retrieving year precision when the actual date is more precise.
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 |
# File 'lib/cocina_display/dates/date.rb', line 380 def format_date(date, precision, allowed_precisions) precision = allowed_precisions.first unless allowed_precisions.include?(precision) case precision when :unknown "Unknown" when :day date.strftime("%B %-d, %Y") when :month date.strftime("%B %Y") when :year year = date.year if year < 1 "#{year.abs + 1} BCE" # Any dates before the year 1000 are explicitly marked CE elsif year >= 1 && year < 1000 "#{year} CE" else year.to_s end when :decade "#{EDTF::Decade.new(date.year).year}s" when :century if date.year.negative? "#{((date.year / 100).abs + 1).ordinalize} century BCE" else "#{((date.year / 100) + 1).ordinalize} century" end end end |
.from_cocina(cocina) ⇒ CocinaDisplay::Date
Construct a Date from parsed Cocina data.
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/cocina_display/dates/date.rb', line 24 def self.from_cocina(cocina) # Create a DateRange instead if structuredValue(s) are present return DateRange.from_cocina(cocina) if cocina["structuredValue"].present? # If an encoding was declared, use it. Cocina validates this case cocina.dig("encoding", "code") when "w3cdtf" W3cdtfFormat.new(cocina) when "iso8601" Iso8601Format.new(cocina) when "marc" MarcFormat.new(cocina) when "edtf" EdtfFormat.new(cocina) else # No declared encoding, or unknown encoding value = cocina["value"] # Don't bother with weird unparseable values date_class = UnparseableDate if value =~ /\p{Hebrew}/ || value =~ /^-/ # Try to match against known date formats using their regexes # Order matters here; more specific formats should be checked first date_class ||= [ UndeclaredEdtfFormat, MMDDYYYYFormat, MMDDYYFormat, YearRangeFormat, DecadeAsYearDashFormat, DecadeStringFormat, EmbeddedBCYearFormat, EmbeddedYearFormat, EmbeddedThreeDigitYearFormat, EmbeddedYearWithBracketsFormat, MysteryCenturyFormat, CenturyFormat, RomanNumeralCenturyFormat, RomanNumeralYearFormat, OneOrTwoDigitYearFormat ].find { |klass| klass.supports?(value) } # If no specific format matched, use the base class date_class ||= CocinaDisplay::Dates::Date date_class.new(cocina) end end |
.normalize_to_edtf(value) ⇒ String
This is the “fallback” version when no other parser matches.
Apply any encoding-specific munging or text extraction logic.
84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/cocina_display/dates/date.rb', line 84 def self.normalize_to_edtf(value) unless value notifier&.notify("Invalid date value: #{value}") return end sanitized = value.gsub(/^\[+/, "").gsub(/[.\]]+$/, "") sanitized = value.rjust(4, "0") if /^\d{3}$/.match?(value) sanitized end |
.notifier ⇒ Object
17 18 19 |
# File 'lib/cocina_display/dates/date.rb', line 17 def self.notifier CocinaDisplay.notifier end |
.parse_date(value) ⇒ Date?
Parse a string to a Date object according to the given encoding. Delegates to the parser subclass normalize_to_edtf method.
76 77 78 |
# File 'lib/cocina_display/dates/date.rb', line 76 def self.parse_date(value) ::Date.edtf(normalize_to_edtf(value)) end |
Instance Method Details
#<=>(other) ⇒ Integer?
Also supports ‘date1.between?(date2, date3)` via Comparable.
Compare this date to another CocinaDisplay::Dates::Date or CocinaDisplay::Dates::DateRange using its #sort_key.
117 118 119 |
# File 'lib/cocina_display/dates/date.rb', line 117 def <=>(other) sort_key <=> other.sort_key if other.is_a?(Date) || other.is_a?(DateRange) end |
#approximate? ⇒ Boolean
Was the date marked as approximate?
184 185 186 |
# File 'lib/cocina_display/dates/date.rb', line 184 def approximate? qualifier == "approximate" end |
#as_range ⇒ Range<Date>?
Output has day precision, using the first day/month if unspecified.
If the range is open-ended, uses today’s date as the end date.
EDTF::Sets can be disjoint ranges, but this method will return the full span, unlike #to_a.
Range of CocinaDisplay::Dates::Dates between earliest possible date and latest possible date.
349 350 351 352 353 354 355 356 |
# File 'lib/cocina_display/dates/date.rb', line 349 def as_range return unless earliest_date || latest_date start = earliest_date || latest_date stop = latest_date || ::Date.today start..stop end |
#base_value ⇒ String
This is important for uniqueness checks in Imprint display.
Value reduced to digits and hyphen. Used for comparison/deduping.
299 300 301 302 303 304 305 |
# File 'lib/cocina_display/dates/date.rb', line 299 def base_value if value =~ /^\[?1\d{3}-\d{2}\??\]?$/ return value.sub(/(\d{2})(\d{2})-(\d{2})/, '\1\2-\1\3') end value.gsub(/(?<!\d)(\d{1,3})([xu-]{1,3})/i) { "#{Regexp.last_match(1)}#{"0" * Regexp.last_match(2).length}" }.scan(/[\d-]/).join end |
#decoded_value(allowed_precisions: [:day, :month, :year, :decade, :century, :unknown]) ⇒ String
Decoded version of the date with “BCE” or “CE”. Strips leading zeroes.
311 312 313 314 315 316 317 318 319 320 321 322 323 324 |
# File 'lib/cocina_display/dates/date.rb', line 311 def decoded_value(allowed_precisions: [:day, :month, :year, :decade, :century, :unknown]) if date.is_a?(EDTF::Interval) range = [ Date.format_date(date.min, date.min.precision, allowed_precisions), Date.format_date(date.max, date.max.precision, allowed_precisions) ].uniq.compact return value.strip if range.empty? range.join(" - ") else Date.format_date(date, precision, allowed_precisions) || value.strip end end |
#earliest_date ⇒ Date
Earliest possible date encoded in data, respecting unspecified/imprecise info.
413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 |
# File 'lib/cocina_display/dates/date.rb', line 413 def earliest_date return nil if date.nil? case date_range when EDTF::Unknown nil when EDTF::Epoch, EDTF::Interval, EDTF::Season date_range.min when EDTF::Set date_range.to_a.first else d = date.dup d = d.change(month: 1, day: 1) if date.precision == :year d = d.change(day: 1) if date.precision == :month d = d.change(month: 1) if date.unspecified.unspecified? :month d = d.change(day: 1) if date.unspecified.unspecified? :day d end end |
#encoding? ⇒ Boolean
Was an encoding declared for this date?
164 165 166 |
# File 'lib/cocina_display/dates/date.rb', line 164 def encoding? encoding.present? end |
#end? ⇒ Boolean
The Cocina will mark end dates with “type”: “end”.
Is this the end date in a range?
178 179 180 |
# File 'lib/cocina_display/dates/date.rb', line 178 def end? cocina["type"] == "end" end |
#inferred? ⇒ Boolean
Was the date marked as inferred?
190 191 192 |
# File 'lib/cocina_display/dates/date.rb', line 190 def inferred? qualifier == "inferred" end |
#known? ⇒ Boolean
Does this represent a known date?
158 159 160 |
# File 'lib/cocina_display/dates/date.rb', line 158 def known? !date.is_a?(EDTF::Unknown) end |
#label ⇒ String
Label used to group the date for display.
140 141 142 |
# File 'lib/cocina_display/dates/date.rb', line 140 def label cocina["displayLabel"].presence || type_label end |
#latest_date ⇒ Date
Latest possible date encoded in data, respecting unspecified/imprecise info.
435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 |
# File 'lib/cocina_display/dates/date.rb', line 435 def latest_date return nil if date.nil? case date_range when EDTF::Unknown nil when EDTF::Epoch, EDTF::Interval, EDTF::Season date_range.max when EDTF::Set date_range.to_a.last.change(month: 12, day: 31) else d = date.dup d = d.change(month: 12, day: 31) if date.precision == :year d = d.change(day: days_in_month(date.month, date.year)) if date.precision == :month d = d.change(month: 12) if date.unspecified.unspecified? :month d = d.change(day: days_in_month(date.month, date.year)) if date.unspecified.unspecified? :day d end end |
#parsable? ⇒ Boolean
Is the value present and not a known unparsable value like “9999”?
209 210 211 |
# File 'lib/cocina_display/dates/date.rb', line 209 def parsable? value.present? && !UNPARSABLE_VALUES.include?(value) end |
#parsed_date? ⇒ Boolean
Did we successfully parse a date from the Cocina data?
215 216 217 |
# File 'lib/cocina_display/dates/date.rb', line 215 def parsed_date? date.present? end |
#precision ⇒ Symbol
How precise is the parsed date information?
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 |
# File 'lib/cocina_display/dates/date.rb', line 221 def precision return :unknown unless date_range || date if date_range.is_a? EDTF::Century return :century elsif date_range.is_a? EDTF::Decade return :decade end case date when EDTF::Season :month when EDTF::Unknown :unknown when EDTF::Interval date.precision else case date.precision when :month date.unspecified.unspecified?(:month) ? :year : :month when :day d = date.unspecified.unspecified?(:day) ? :month : :day date.unspecified.unspecified?(:month) ? :year : d else date.precision end end end |
#primary? ⇒ Boolean
In MODS XML, this corresponds to the keyDate attribute.
Was the date marked as primary?
203 204 205 |
# File 'lib/cocina_display/dates/date.rb', line 203 def primary? cocina["status"] == "primary" end |
#qualified? ⇒ Boolean
Does this date have a qualifier? E.g. “approximate”, “inferred”, etc.
152 153 154 |
# File 'lib/cocina_display/dates/date.rb', line 152 def qualified? qualifier.present? end |
#qualified_value ⇒ Object
Decoded date with “BCE” or “CE” and qualifier markers applied.
329 330 331 332 333 334 335 336 337 338 339 340 341 342 |
# File 'lib/cocina_display/dates/date.rb', line 329 def qualified_value qualified_format = case qualifier when "approximate" "[ca. %s]" when "questionable" "[%s?]" when "inferred" "[%s]" else "%s" end format(qualified_format, decoded_value) end |
#qualifier ⇒ String?
The qualifier for this date, if any, such as “approximate”, “inferred”, etc.
146 147 148 |
# File 'lib/cocina_display/dates/date.rb', line 146 def qualifier cocina["qualifier"] end |
#questionable? ⇒ Boolean
Was the date marked as approximate?
196 197 198 |
# File 'lib/cocina_display/dates/date.rb', line 196 def questionable? qualifier == "questionable" end |
#sort_key ⇒ String
Key used to sort this date. Respects BCE/CE ordering and precision.
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 |
# File 'lib/cocina_display/dates/date.rb', line 254 def sort_key # Even if not parsed, we might need to sort it for display later return "" unless parsed_date? # Use the start of an interval for sorting sort_date = date.is_a?(EDTF::Interval) ? date.from : date # Get the parsed year, month, and day values year, month, day = if sort_date.respond_to?(:values) sort_date.values else [sort_date.year, nil, nil] end # Format year into sortable string year_str = if year > 0 # for CE dates, we can just pad them out to 4 digits and sort normally... year.to_s.rjust(4, "0") else # ... but for BCE, because we're sorting lexically, we need to invert the digits (replacing 0 with 9, 1 with 8, etc.), # we prefix it with a hyphen (which will sort before any digit) and the number of digits (also inverted) to get # it to sort correctly. inverted_year = year.abs.to_s.chars.map { |c| BCE_CHAR_SORT_MAP[c] }.join length_prefix = BCE_CHAR_SORT_MAP[inverted_year.to_s.length.to_s] "-#{length_prefix}#{inverted_year}" end # Format month and day into sortable strings, pad to 2 digits month_str = month ? month.to_s.rjust(2, "0") : "00" day_str = day ? day.to_s.rjust(2, "0") : "00" # Join into a sortable string; add hyphens so decade/century sort first case precision when :decade [year_str[0...-1], "-", month_str, day_str].join when :century [year_str[0...-2], "--", month_str, day_str].join else [year_str, month_str, day_str].join end end |
#start? ⇒ Boolean
The Cocina will mark start dates with “type”: “start”.
Is this the start date in a range?
171 172 173 |
# File 'lib/cocina_display/dates/date.rb', line 171 def start? cocina["type"] == "start" end |
#to_a ⇒ Array<Date>
Output dates will have the same precision as the input date (e.g. year vs day).
If the range is open-ended, uses today’s date as the end date.
EDTF::Sets can be disjoint ranges; unlike #as_range this method will respect any gaps.
Array of all individual CocinaDisplay::Dates::Dates that are described by the data.
363 364 365 366 367 368 369 370 |
# File 'lib/cocina_display/dates/date.rb', line 363 def to_a case date when EDTF::Set date.to_a else as_range.to_a end end |
#to_s ⇒ String
The string representation of the date for display. Uses the raw value if the date was not encoded or couldn’t be parsed.
130 131 132 133 134 135 136 |
# File 'lib/cocina_display/dates/date.rb', line 130 def to_s if !parsed_date? || (!encoding? && value !~ /^-?\d+$/ && value !~ /^[\dXxu?-]{4}$/) value.strip else qualified_value end end |
#value ⇒ String
The text representation of the date, as stored in Cocina.
123 124 125 |
# File 'lib/cocina_display/dates/date.rb', line 123 def value cocina["value"] end |