Class: CocinaDisplay::Dates::Date
- Inherits:
-
Object
- Object
- CocinaDisplay::Dates::Date
- 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) ⇒ Object
-
#approximate? ⇒ Boolean
Was the date marked as approximate?.
-
#as_range ⇒ Range
Range 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], ignore_unparseable: false, display_original_value: true) ⇒ String
Decoded version of the date with “BCE” or “CE”.
-
#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.
-
#label ⇒ String
Label used to group the date for display.
-
#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
Array of all dates that fall into the range of possible dates in 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.
104 105 106 107 108 109 |
# File 'lib/cocina_display/dates/date.rb', line 104 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.
93 94 95 |
# File 'lib/cocina_display/dates/date.rb', line 93 def cocina @cocina end |
#date ⇒ Object (readonly)
Returns the value of attribute date.
93 94 95 |
# File 'lib/cocina_display/dates/date.rb', line 93 def date @date end |
#encoding ⇒ String?
The encoding name of this date, if specified.
102 103 104 |
# File 'lib/cocina_display/dates/date.rb', line 102 def encoding @encoding end |
#type ⇒ String?
The type of this date, if any, such as “creation”, “publication”, etc.
97 98 99 |
# File 'lib/cocina_display/dates/date.rb', line 97 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.
370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 |
# File 'lib/cocina_display/dates/date.rb', line 370 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 %e, %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.
21 22 23 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 |
# File 'lib/cocina_display/dates/date.rb', line 21 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.
81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/cocina_display/dates/date.rb', line 81 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
14 15 16 |
# File 'lib/cocina_display/dates/date.rb', line 14 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.
73 74 75 |
# File 'lib/cocina_display/dates/date.rb', line 73 def self.parse_date(value) ::Date.edtf(normalize_to_edtf(value)) end |
Instance Method Details
#<=>(other) ⇒ Object
Compare this date to another CocinaDisplay::Dates::Date or CocinaDisplay::Dates::DateRange using its #sort_key.
112 113 114 |
# File 'lib/cocina_display/dates/date.rb', line 112 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?
168 169 170 |
# File 'lib/cocina_display/dates/date.rb', line 168 def approximate? qualifier == "approximate" end |
#as_range ⇒ Range
Some encodings support disjoint sets of ranges, so this method could be less accurate than #to_a.
Range between earliest possible date and latest possible date.
342 343 344 345 346 |
# File 'lib/cocina_display/dates/date.rb', line 342 def as_range return unless earliest_date && latest_date earliest_date..latest_date end |
#base_value ⇒ String
This is important for uniqueness checks in Imprint display.
Value reduced to digits and hyphen. Used for comparison/deduping.
283 284 285 286 287 288 289 |
# File 'lib/cocina_display/dates/date.rb', line 283 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], ignore_unparseable: false, display_original_value: true) ⇒ String
Decoded version of the date with “BCE” or “CE”. Strips leading zeroes.
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 |
# File 'lib/cocina_display/dates/date.rb', line 297 def decoded_value(allowed_precisions: [:day, :month, :year, :decade, :century, :unknown], ignore_unparseable: false, display_original_value: true) return if ignore_unparseable && !parsed_date? return value.strip unless parsed_date? if display_original_value unless encoding? return value.strip unless value =~ /^-?\d+$/ || value =~ /^[\dXxu?-]{4}$/ end end 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 |
#encoding? ⇒ Boolean
Was an encoding declared for this date?
148 149 150 |
# File 'lib/cocina_display/dates/date.rb', line 148 def encoding? encoding.present? end |
#end? ⇒ Boolean
The Cocina will mark end dates with “type”: “end”.
Is this the end date in a range?
162 163 164 |
# File 'lib/cocina_display/dates/date.rb', line 162 def end? cocina["type"] == "end" end |
#inferred? ⇒ Boolean
Was the date marked as inferred?
174 175 176 |
# File 'lib/cocina_display/dates/date.rb', line 174 def inferred? qualifier == "inferred" end |
#label ⇒ String
Label used to group the date for display.
130 131 132 |
# File 'lib/cocina_display/dates/date.rb', line 130 def label cocina["displayLabel"].presence || type_label end |
#parsable? ⇒ Boolean
Is the value present and not a known unparsable value like “9999”?
193 194 195 |
# File 'lib/cocina_display/dates/date.rb', line 193 def parsable? value.present? && !UNPARSABLE_VALUES.include?(value) end |
#parsed_date? ⇒ Boolean
Did we successfully parse a date from the Cocina data?
199 200 201 |
# File 'lib/cocina_display/dates/date.rb', line 199 def parsed_date? date.present? end |
#precision ⇒ Symbol
How precise is the parsed date information?
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 |
# File 'lib/cocina_display/dates/date.rb', line 205 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?
187 188 189 |
# File 'lib/cocina_display/dates/date.rb', line 187 def primary? cocina["status"] == "primary" end |
#qualified? ⇒ Boolean
Does this date have a qualifier? E.g. “approximate”, “inferred”, etc.
142 143 144 |
# File 'lib/cocina_display/dates/date.rb', line 142 def qualified? qualifier.present? end |
#qualified_value ⇒ Object
Decoded date with “BCE” or “CE” and qualifier markers applied.
324 325 326 327 328 329 330 331 332 333 334 335 336 337 |
# File 'lib/cocina_display/dates/date.rb', line 324 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.
136 137 138 |
# File 'lib/cocina_display/dates/date.rb', line 136 def qualifier cocina["qualifier"] end |
#questionable? ⇒ Boolean
Was the date marked as approximate?
180 181 182 |
# File 'lib/cocina_display/dates/date.rb', line 180 def questionable? qualifier == "questionable" end |
#sort_key ⇒ String
Key used to sort this date. Respects BCE/CE ordering and precision.
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 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 |
# File 'lib/cocina_display/dates/date.rb', line 238 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?
155 156 157 |
# File 'lib/cocina_display/dates/date.rb', line 155 def start? cocina["type"] == "start" end |
#to_a ⇒ Array
Some encodings support disjoint sets of ranges, so this method could be more accurate than #as_range.
Array of all dates that fall into the range of possible dates in the data.
351 352 353 354 355 356 357 358 |
# File 'lib/cocina_display/dates/date.rb', line 351 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.
124 125 126 |
# File 'lib/cocina_display/dates/date.rb', line 124 def to_s qualified_value end |
#value ⇒ String
The text representation of the date, as stored in Cocina.
118 119 120 |
# File 'lib/cocina_display/dates/date.rb', line 118 def value cocina["value"] end |