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.
-
#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.
-
.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], ignore_unparseable: false, display_original_value: true) ⇒ String
Decoded version of the date with “BCE” or “CE”.
-
#encoding ⇒ String?
The encoding of this date, if specified.
-
#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.
-
#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.
-
#value ⇒ String
The text representation of the date, as stored in Cocina.
Constructor Details
#initialize(cocina) ⇒ Date
Returns a new instance of Date.
89 90 91 92 93 |
# File 'lib/cocina_display/dates/date.rb', line 89 def initialize(cocina) @cocina = cocina @date = self.class.parse_date(cocina["value"]) @type = cocina["type"] unless ["start", "end"].include?(cocina["type"]) end |
Instance Attribute Details
#cocina ⇒ Object (readonly)
Returns the value of attribute cocina.
83 84 85 |
# File 'lib/cocina_display/dates/date.rb', line 83 def cocina @cocina end |
#date ⇒ Object (readonly)
Returns the value of attribute date.
83 84 85 |
# File 'lib/cocina_display/dates/date.rb', line 83 def date @date end |
#type ⇒ String?
The type of this date, if any, such as “creation”, “publication”, etc.
87 88 89 |
# File 'lib/cocina_display/dates/date.rb', line 87 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.
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 |
# File 'lib/cocina_display/dates/date.rb', line 346 def format_date(date, precision, allowed_precisions) precision = allowed_precisions.first unless allowed_precisions.include?(precision) case precision 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.
17 18 19 20 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 |
# File 'lib/cocina_display/dates/date.rb', line 17 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 ||= [ 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.
76 77 78 79 80 81 |
# File 'lib/cocina_display/dates/date.rb', line 76 def self.normalize_to_edtf(value) sanitized = value.gsub(/^[\[]+/, "").gsub(/[\.\]]+$/, "") sanitized = value.rjust(4, "0") if /^\d{3}$/.match?(value) sanitized 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.
68 69 70 |
# File 'lib/cocina_display/dates/date.rb', line 68 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.
96 97 98 |
# File 'lib/cocina_display/dates/date.rb', line 96 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?
148 149 150 |
# File 'lib/cocina_display/dates/date.rb', line 148 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.
318 319 320 321 322 |
# File 'lib/cocina_display/dates/date.rb', line 318 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.
259 260 261 262 263 264 265 |
# File 'lib/cocina_display/dates/date.rb', line 259 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], ignore_unparseable: false, display_original_value: true) ⇒ String
Decoded version of the date with “BCE” or “CE”. Strips leading zeroes.
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 |
# File 'lib/cocina_display/dates/date.rb', line 273 def decoded_value(allowed_precisions: [:day, :month, :year, :decade, :century], 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 ⇒ String?
The encoding of this date, if specified.
122 123 124 |
# File 'lib/cocina_display/dates/date.rb', line 122 def encoding cocina.dig("encoding", "code") end |
#encoding? ⇒ Boolean
Was an encoding declared for this date?
128 129 130 |
# File 'lib/cocina_display/dates/date.rb', line 128 def encoding? encoding.present? end |
#end? ⇒ Boolean
The Cocina will mark end dates with “type”: “end”.
Is this the end date in a range?
142 143 144 |
# File 'lib/cocina_display/dates/date.rb', line 142 def end? cocina["type"] == "end" end |
#inferred? ⇒ Boolean
Was the date marked as inferred?
154 155 156 |
# File 'lib/cocina_display/dates/date.rb', line 154 def inferred? qualifier == "inferred" end |
#parsable? ⇒ Boolean
Is the value present and not a known unparsable value like “9999”?
173 174 175 |
# File 'lib/cocina_display/dates/date.rb', line 173 def parsable? value.present? && !UNPARSABLE_VALUES.include?(value) end |
#parsed_date? ⇒ Boolean
Did we successfully parse a date from the Cocina data?
179 180 181 |
# File 'lib/cocina_display/dates/date.rb', line 179 def parsed_date? date.present? end |
#precision ⇒ Symbol
How precise is the parsed date information?
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
# File 'lib/cocina_display/dates/date.rb', line 185 def precision return :unknown unless date_range || date if date_range.is_a? EDTF::Century :century elsif date_range.is_a? EDTF::Decade :decade elsif date.is_a? EDTF::Season :month elsif date.is_a? 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?
167 168 169 |
# File 'lib/cocina_display/dates/date.rb', line 167 def primary? cocina["status"] == "primary" end |
#qualified? ⇒ Boolean
Does this date have a qualifier? E.g. “approximate”, “inferred”, etc.
114 115 116 |
# File 'lib/cocina_display/dates/date.rb', line 114 def qualified? qualifier.present? end |
#qualified_value ⇒ Object
Decoded date with “BCE” or “CE” and qualifier markers applied.
300 301 302 303 304 305 306 307 308 309 310 311 312 313 |
# File 'lib/cocina_display/dates/date.rb', line 300 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.
108 109 110 |
# File 'lib/cocina_display/dates/date.rb', line 108 def qualifier cocina["qualifier"] end |
#questionable? ⇒ Boolean
Was the date marked as approximate?
160 161 162 |
# File 'lib/cocina_display/dates/date.rb', line 160 def questionable? qualifier == "questionable" end |
#sort_key ⇒ String
Key used to sort this date. Respects BCE/CE ordering and precision.
214 215 216 217 218 219 220 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 248 249 250 251 252 253 254 |
# File 'lib/cocina_display/dates/date.rb', line 214 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?
135 136 137 |
# File 'lib/cocina_display/dates/date.rb', line 135 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.
327 328 329 330 331 332 333 334 |
# File 'lib/cocina_display/dates/date.rb', line 327 def to_a case date when EDTF::Set date.to_a else as_range.to_a end end |
#value ⇒ String
The text representation of the date, as stored in Cocina.
102 103 104 |
# File 'lib/cocina_display/dates/date.rb', line 102 def value cocina["value"] end |