cocina_display 1.1.3 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +0 -1
  3. data/.standard.yml +1 -1
  4. data/README.md +21 -2
  5. data/config/i18n-tasks.yml +0 -0
  6. data/config/licenses.yml +59 -0
  7. data/config/locales/en.yml +109 -0
  8. data/config/marc_countries.yml +385 -0
  9. data/config/marc_relators.yml +310 -0
  10. data/config/searchworks_languages.yml +520 -0
  11. data/lib/cocina_display/cocina_record.rb +29 -64
  12. data/lib/cocina_display/concerns/accesses.rb +78 -0
  13. data/lib/cocina_display/concerns/contributors.rb +32 -11
  14. data/lib/cocina_display/concerns/events.rb +19 -6
  15. data/lib/cocina_display/concerns/forms.rb +98 -11
  16. data/lib/cocina_display/concerns/geospatial.rb +9 -5
  17. data/lib/cocina_display/concerns/identifiers.rb +25 -5
  18. data/lib/cocina_display/concerns/languages.rb +6 -2
  19. data/lib/cocina_display/concerns/notes.rb +36 -0
  20. data/lib/cocina_display/concerns/related_resources.rb +20 -0
  21. data/lib/cocina_display/concerns/subjects.rb +25 -8
  22. data/lib/cocina_display/concerns/titles.rb +67 -25
  23. data/lib/cocina_display/concerns/{access.rb → url_helpers.rb} +3 -3
  24. data/lib/cocina_display/concerns.rb +6 -0
  25. data/lib/cocina_display/contributors/contributor.rb +47 -26
  26. data/lib/cocina_display/contributors/name.rb +18 -14
  27. data/lib/cocina_display/contributors/role.rb +31 -13
  28. data/lib/cocina_display/dates/date.rb +55 -14
  29. data/lib/cocina_display/dates/date_range.rb +0 -2
  30. data/lib/cocina_display/description/access.rb +41 -0
  31. data/lib/cocina_display/description/access_contact.rb +11 -0
  32. data/lib/cocina_display/description/url.rb +17 -0
  33. data/lib/cocina_display/display_data.rb +104 -0
  34. data/lib/cocina_display/events/event.rb +8 -4
  35. data/lib/cocina_display/events/imprint.rb +0 -10
  36. data/lib/cocina_display/events/location.rb +9 -3
  37. data/lib/cocina_display/events/note.rb +33 -0
  38. data/lib/cocina_display/forms/form.rb +71 -0
  39. data/lib/cocina_display/forms/genre.rb +12 -0
  40. data/lib/cocina_display/forms/resource_type.rb +38 -0
  41. data/lib/cocina_display/geospatial.rb +1 -1
  42. data/lib/cocina_display/identifier.rb +101 -0
  43. data/lib/cocina_display/json_backed_record.rb +27 -0
  44. data/lib/cocina_display/language.rb +18 -12
  45. data/lib/cocina_display/license.rb +32 -0
  46. data/lib/cocina_display/note.rb +103 -0
  47. data/lib/cocina_display/related_resource.rb +74 -0
  48. data/lib/cocina_display/subjects/subject.rb +32 -9
  49. data/lib/cocina_display/subjects/subject_value.rb +34 -16
  50. data/lib/cocina_display/title.rb +221 -0
  51. data/lib/cocina_display/utils.rb +4 -4
  52. data/lib/cocina_display/version.rb +1 -1
  53. data/lib/cocina_display.rb +32 -2
  54. metadata +46 -12
  55. data/lib/cocina_display/title_builder.rb +0 -397
  56. data/lib/cocina_display/vocabularies/marc_country_codes.rb +0 -393
  57. data/lib/cocina_display/vocabularies/marc_relator_codes.rb +0 -318
  58. data/lib/cocina_display/vocabularies/searchworks_languages.rb +0 -526
@@ -1,48 +1,77 @@
1
- require_relative "../title_builder"
2
-
3
1
  module CocinaDisplay
4
2
  module Concerns
5
3
  # Methods for finding and formatting titles.
6
4
  module Titles
7
5
  # The main title for the object, without subtitle, part name, etc.
8
- # If there are multiple titles, uses the first.
9
- # @see CocinaDisplay::TitleBuilder#main_title
10
- # @note This corresponds to the "short title" in MODS XML, or MARC 245$a only.
11
- # @return [String]
6
+ # If there are multiple primary titles, uses the first.
7
+ # @see CocinaDisplay::Title#main_title
8
+ # @return [String, nil]
12
9
  def main_title
13
- CocinaDisplay::TitleBuilder.main_title(cocina_titles).first
10
+ primary_title&.short_title
14
11
  end
15
12
 
16
13
  # The full title for the object, including subtitle, part name, etc.
17
- # If there are multiple titles, uses the first.
18
- # @see CocinaDisplay::TitleBuilder#full_title
19
- # @note This corresponds to the entire MARC 245 field.
20
- # @return [String]
14
+ # If there are multiple primary titles, uses the first.
15
+ # @see CocinaDisplay::Title#full_title
16
+ # @return [String, nil]
21
17
  def full_title
22
- CocinaDisplay::TitleBuilder.full_title(cocina_titles, catalog_links: catalog_links).first
18
+ primary_title&.full_title
23
19
  end
24
20
 
25
21
  # The full title, joined together with additional punctuation.
26
- # If there are multiple titles, uses the first.
27
- # @see CocinaDisplay::TitleBuilder#build
28
- # @return [String]
22
+ # If there are multiple primary titles, uses the first.
23
+ # @see CocinaDisplay::Title#display_title
24
+ # @return [String, nil]
29
25
  def display_title
30
- CocinaDisplay::TitleBuilder.build(cocina_titles, catalog_links: catalog_links)
26
+ primary_title&.display_title
27
+ end
28
+
29
+ # A string value for sorting by title that sorts missing values last.
30
+ # If there are multiple primary titles, uses the first.
31
+ # @see CocinaDisplay::Title#sort_title
32
+ # @return [String]
33
+ def sort_title
34
+ primary_title&.sort_title || "\u{10FFFF}"
31
35
  end
32
36
 
33
- # Any additional titles for the object excluding the main title.
37
+ # Any additional titles for the object excluding the primary title.
34
38
  # @return [Array<String>]
35
- # @see CocinaDisplay::TitleBuilder#additional_titles
39
+ # @see CocinaDisplay::Title#display_title
36
40
  def additional_titles
37
- CocinaDisplay::TitleBuilder.additional_titles(cocina_titles)
41
+ secondary_titles.map(&:display_title).compact_blank
38
42
  end
39
43
 
40
- # A string value for sorting by title that sorts missing values last.
41
- # Ignores punctuation, leading/trailing spaces, and non-sorting characters.
42
- # @see CocinaDisplay::TitleBuilder#sort_title
43
- # @return [String]
44
- def sort_title
45
- CocinaDisplay::TitleBuilder.sort_title(cocina_titles, catalog_links: catalog_links).first || "\u{10FFFF}"
44
+ # All {Title} objects, grouped by their label for display.
45
+ # @note All primary titles are included under "Title", not just the first.
46
+ # @return [Array<DisplayData>]
47
+ def title_display_data
48
+ DisplayData.from_objects(all_titles)
49
+ end
50
+
51
+ # The first title marked primary, or the first without a type.
52
+ # @return [Array<Title>]
53
+ def primary_title
54
+ all_titles.find { |title| title.primary? }.presence || all_titles.find { |title| !title.type? }
55
+ end
56
+
57
+ # All titles except the primary title.
58
+ # @return [Array<Title>]
59
+ def secondary_titles
60
+ all_titles - [primary_title]
61
+ end
62
+
63
+ # All {Title} objects built from the Cocina titles.
64
+ # Flattens parallel values into separate titles.
65
+ # @return [Array<Title>]
66
+ def all_titles
67
+ @all_titles ||= cocina_titles.flat_map do |cocina_title|
68
+ (Array(cocina_title["parallelValue"]).presence || [cocina_title]).map do |value|
69
+ Title.new(value, part_label: part_label, part_numbers: part_numbers).tap do |title|
70
+ title.type ||= cocina_title["type"]
71
+ title.status ||= cocina_title["status"]
72
+ end
73
+ end
74
+ end
46
75
  end
47
76
 
48
77
  private
@@ -59,6 +88,19 @@ module CocinaDisplay
59
88
  def catalog_links
60
89
  @catalog_links ||= Array(cocina_doc.dig("identification", "catalogLinks"))
61
90
  end
91
+
92
+ # Part label for digital serials display from FOLIO, if any.
93
+ # @return [String, nil]
94
+ def part_label
95
+ catalog_links.find { |link| link["catalog"] == "folio" }&.fetch("partLabel", nil)
96
+ end
97
+
98
+ # Part numbers from notes, if any.
99
+ # @note This is used for RelatedResource title display.
100
+ # @return [Array<String>]
101
+ def part_numbers
102
+ notes.filter(&:part?).flat_map { |note| note.values_by_type["number"] }.compact_blank
103
+ end
62
104
  end
63
105
  end
64
106
  end
@@ -1,13 +1,13 @@
1
1
  module CocinaDisplay
2
2
  module Concerns
3
3
  # Methods that generate URLs to access an object.
4
- module Access
4
+ module UrlHelpers
5
5
  # The PURL URL for this object.
6
6
  # @return [String]
7
7
  # @example
8
8
  # record.purl_url #=> "https://purl.stanford.edu/bx658jh7339"
9
9
  def purl_url
10
- cocina_doc.dig("description", "purl") || ("https://purl.stanford.edu/#{bare_druid}" if bare_druid)
10
+ cocina_doc.dig("description", "purl")
11
11
  end
12
12
 
13
13
  # The oEmbed URL for the object, optionally with additional parameters.
@@ -18,7 +18,7 @@ module CocinaDisplay
18
18
  # @example Generate an oEmbed URL for the viewer and hide the title
19
19
  # record.oembed_url(hide_title: true) #=> "https://purl.stanford.edu/bx658jh7339/embed.json?hide_title=true"
20
20
  def oembed_url(params: {})
21
- return if collection? || purl_url.blank?
21
+ return if (!is_a?(CocinaDisplay::RelatedResource) && collection?) || purl_url.blank?
22
22
 
23
23
  params[:url] ||= purl_url
24
24
  "#{purl_base_url}/embed.json?#{params.to_query}"
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CocinaDisplay
4
+ module Concerns
5
+ end
6
+ end
@@ -1,13 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support"
4
- require "active_support/core_ext/object/blank"
5
- require "active_support/core_ext/array/conversions"
6
-
7
- require_relative "../utils"
8
- require_relative "name"
9
- require_relative "role"
10
-
11
3
  module CocinaDisplay
12
4
  module Contributors
13
5
  # A contributor to a work, such as an author or publisher.
@@ -20,11 +12,22 @@ module CocinaDisplay
20
12
  @cocina = cocina
21
13
  end
22
14
 
23
- # String representation of the contributor, including name and role.
24
- # Used for debugging and logging.
25
- # @return [String]
15
+ # String representation of the contributor, using display name with date.
16
+ # @return [String, nil]
26
17
  def to_s
27
- Utils.compact_and_join([display_name, display_role], delimiter: ": ")
18
+ display_name(with_date: true)
19
+ end
20
+
21
+ # Support equality based on the underlying Cocina data.
22
+ # @param other [Object]
23
+ def ==(other)
24
+ other.is_a?(Contributor) && other.cocina == cocina
25
+ end
26
+
27
+ # Identifiers for the contributor.
28
+ # @return [Array<Identifier>]
29
+ def identifiers
30
+ Array(cocina["identifier"]).map { |id| Identifier.new(id) }
28
31
  end
29
32
 
30
33
  # Is this contributor a human?
@@ -75,39 +78,57 @@ module CocinaDisplay
75
78
  roles.any?
76
79
  end
77
80
 
78
- # The display name for the contributor as a string.
79
- # Uses the first name if multiple names are present.
81
+ # The primary display name for the contributor as a string.
80
82
  # @param with_date [Boolean] Include life dates, if present
81
- # @return [String]
83
+ # @return [String, nil]
82
84
  def display_name(with_date: false)
83
- names.map { |name| name.to_s(with_date: with_date) }.compact_blank.first
85
+ primary_name&.to_s(with_date: with_date)
84
86
  end
85
87
 
86
- # The full forename for the contributor from the first available name.
88
+ # String renderings of all names for the contributor.
89
+ # @param with_date [Boolean] Include life dates, if present
90
+ # @return [Array<String>]
91
+ def display_names(with_date: false)
92
+ names.map { |name| name.to_s(with_date: with_date) }.compact_blank
93
+ end
94
+
95
+ # A single primary name for the contributor.
96
+ # Prefers a name of type "display" or one marked primary.
97
+ # @return [Contributor::Name, nil]
98
+ def primary_name
99
+ names.find { |name| name.type == "display" }.presence ||
100
+ names.find(&:primary?).presence ||
101
+ names.first
102
+ end
103
+
104
+ # The forename for the contributor, if structured name info is available.
87
105
  # @see Contributor::Name::forename_str
88
106
  # @return [String, nil]
89
107
  def forename
90
108
  names.map(&:forename_str).first.presence
91
109
  end
92
110
 
93
- # The full surname for the contributor from the first available name.
111
+ # The surname for the contributor, if structured name info is available.
94
112
  # @see Contributor::Name::surname_str
95
113
  # @return [String, nil]
96
114
  def surname
97
115
  names.map(&:surname_str).first.presence
98
116
  end
99
117
 
100
- # A string representation of the contributor's roles, formatted for display.
101
- # If there are multiple roles, they are joined with commas.
102
- # @return [String]
103
- def display_role
104
- roles.map(&:to_s).to_sentence
105
- end
106
-
107
118
  # All names in the Cocina as Name objects.
119
+ # Flattens parallel values into separate Name objects.
108
120
  # @return [Array<Name>]
109
121
  def names
110
- @names ||= Array(cocina["name"]).map { |name| Name.new(name) }
122
+ @names ||= Array(cocina["name"]).flat_map do |name|
123
+ (Array(name["parallelValue"]).presence || [name]).filter_map do |name_value|
124
+ unless name_value.blank?
125
+ Name.new(name_value).tap do |name_obj|
126
+ name_obj.type ||= name["type"]
127
+ name_obj.status ||= name["status"]
128
+ end
129
+ end
130
+ end
131
+ end
111
132
  end
112
133
 
113
134
  # All roles in the Cocina structured data.
@@ -1,24 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../utils"
4
-
5
3
  module CocinaDisplay
6
4
  module Contributors
7
5
  # A name associated with a contributor.
8
6
  class Name
7
+ # The underlying Cocina structured data for the name.
8
+ # @return [Hash]
9
9
  attr_reader :cocina
10
10
 
11
+ # The type of name, if any (e.g., "personal", "corporate", etc.).
12
+ # @return [String, nil]
13
+ attr_accessor :type
14
+
15
+ # The status of the name, if any (e.g., "primary").
16
+ # @return [String, nil]
17
+ attr_accessor :status
18
+
11
19
  # Initialize a Name object with Cocina structured data.
12
20
  # @param cocina [Hash] The Cocina structured data for the name.
13
21
  def initialize(cocina)
14
22
  @cocina = cocina
23
+ @type = cocina["type"]
24
+ @status = cocina["status"]
15
25
  end
16
26
 
17
27
  # The display string for the name, optionally including life dates.
18
- # Uses these values in order, if present:
19
- # 1. Unstructured value
20
- # 2. Any structured/parallel values marked as "display"
21
- # 3. Joined structured values, optionally with life dates
22
28
  # @param with_date [Boolean] Include life dates, if present
23
29
  # @return [String]
24
30
  # @example no dates
@@ -28,8 +34,6 @@ module CocinaDisplay
28
34
  def to_s(with_date: false)
29
35
  if cocina["value"].present?
30
36
  cocina["value"]
31
- elsif display_name_str.present?
32
- display_name_str
33
37
  elsif dates_str.present? && with_date
34
38
  Utils.compact_and_join([full_name_str, dates_str], delimiter: ", ")
35
39
  else
@@ -43,12 +47,6 @@ module CocinaDisplay
43
47
  Utils.compact_and_join(name_components.push(terms_of_address_str), delimiter: ", ")
44
48
  end
45
49
 
46
- # Flattened form of any names explicitly marked as "display name".
47
- # @return [String]
48
- def display_name_str
49
- Utils.compact_and_join(Array(name_values["display"]), delimiter: ", ")
50
- end
51
-
52
50
  # List of all name components.
53
51
  # If any of forename, surname, or term of address are present, those are used.
54
52
  # Otherwise, fall back to any names explicitly marked as "name" or untyped.
@@ -81,6 +79,12 @@ module CocinaDisplay
81
79
  Utils.compact_and_join(Array(name_values["life dates"]) + Array(name_values["activity dates"]), delimiter: ", ")
82
80
  end
83
81
 
82
+ # Is this a primary name for the contributor?
83
+ # @return [Boolean]
84
+ def primary?
85
+ status == "primary"
86
+ end
87
+
84
88
  private
85
89
 
86
90
  # A hash mapping destructured name types to their values.
@@ -1,13 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../vocabularies/marc_relator_codes"
4
-
5
3
  module CocinaDisplay
6
4
  module Contributors
7
5
  # A role associated with a contributor.
8
6
  class Role
7
+ MARC_RELATORS_FILE_PATH = CocinaDisplay.root / "config" / "marc_relators.yml"
8
+
9
9
  attr_reader :cocina
10
10
 
11
+ # A hash mapping MARC relator codes to their names.
12
+ # @return [Hash{String => String}]
13
+ def self.marc_relators
14
+ @marc_relators ||= YAML.safe_load_file(MARC_RELATORS_FILE_PATH)
15
+ end
16
+
11
17
  # Initialize a Role object with Cocina structured data.
12
18
  # @param cocina [Hash] The Cocina structured data for the role.
13
19
  def initialize(cocina)
@@ -16,37 +22,49 @@ module CocinaDisplay
16
22
 
17
23
  # The name of the role.
18
24
  # Translates the MARC relator code if no value was present.
19
- # @return [String, nil]
25
+ # @return [String, nil] A nil role is typically displayed in the UI as an "Associated with" relationship
20
26
  def to_s
21
- cocina["value"] || (Vocabularies::MARC_RELATOR[code] if marc_relator?)
22
- end
23
-
24
- # A code associated with the role, e.g. a MARC relator code.
25
- # @return [String, nil]
26
- def code
27
- cocina["code"]
27
+ cocina.fetch("value") { marc_value }
28
28
  end
29
29
 
30
30
  # Does this role indicate the contributor is an author?
31
31
  # @return [Boolean]
32
32
  def author?
33
- to_s =~ /^(author|creator|primary investigator)/i
33
+ /^(author|creator|primary investigator)/i.match? to_s
34
34
  end
35
35
 
36
36
  # Does this role indicate the contributor is a publisher?
37
37
  # @return [Boolean]
38
38
  def publisher?
39
- to_s =~ /^publisher/i
39
+ /^publisher/i.match? to_s
40
40
  end
41
41
 
42
42
  # Does this role indicate the contributor is a funder?
43
43
  # @return [Boolean]
44
44
  def funder?
45
- to_s =~ /^funder/i
45
+ /^funder/i.match? to_s
46
46
  end
47
47
 
48
48
  private
49
49
 
50
+ # The name of the MARC relator role
51
+ # @raises [KeyError] if the role is not valid
52
+ # @return [String, nil]
53
+ def marc_value
54
+ return unless marc_relator?
55
+
56
+ Role.marc_relators.fetch(code)
57
+ rescue
58
+ CocinaDisplay.notifier&.notify("Invalid marc relator: #{code}")
59
+ nil
60
+ end
61
+
62
+ # A code associated with the role, e.g. a MARC relator code.
63
+ # @return [String, nil]
64
+ def code
65
+ cocina["code"]
66
+ end
67
+
50
68
  # Does this role have a MARC relator code?
51
69
  # @return [Boolean]
52
70
  def marc_relator?
@@ -11,6 +11,10 @@ module CocinaDisplay
11
11
  # List of values that we shouldn't even attempt to parse.
12
12
  UNPARSABLE_VALUES = ["0000-00-00", "9999", "uuuu", "[uuuu]"].freeze
13
13
 
14
+ def self.notifier
15
+ CocinaDisplay.notifier
16
+ end
17
+
14
18
  # Construct a Date from parsed Cocina data.
15
19
  # @param cocina [Hash] Cocina date data
16
20
  # @return [CocinaDisplay::Date]
@@ -75,6 +79,11 @@ module CocinaDisplay
75
79
  # @param value [String] the date value to modify
76
80
  # @return [String]
77
81
  def self.normalize_to_edtf(value)
82
+ unless value
83
+ notifier&.notify("Invalid date value: #{value}")
84
+ return
85
+ end
86
+
78
87
  sanitized = value.gsub(/^[\[]+/, "").gsub(/[\.\]]+$/, "")
79
88
  sanitized = value.rjust(4, "0") if /^\d{3}$/.match?(value)
80
89
 
@@ -110,6 +119,18 @@ module CocinaDisplay
110
119
  cocina["value"]
111
120
  end
112
121
 
122
+ # The string representation of the date for display.
123
+ # @return [String]
124
+ def to_s
125
+ qualified_value
126
+ end
127
+
128
+ # Label used to group the date for display.
129
+ # @return [String]
130
+ def label
131
+ cocina["displayLabel"].presence || type_label
132
+ end
133
+
113
134
  # The qualifier for this date, if any, such as "approximate", "inferred", etc.
114
135
  # @return [String, nil]
115
136
  def qualifier
@@ -183,14 +204,18 @@ module CocinaDisplay
183
204
  # @return [Symbol] :year, :month, :day, :decade, :century, or :unknown
184
205
  def precision
185
206
  return :unknown unless date_range || date
186
-
187
207
  if date_range.is_a? EDTF::Century
188
- :century
208
+ return :century
189
209
  elsif date_range.is_a? EDTF::Decade
190
- :decade
191
- elsif date.is_a? EDTF::Season
210
+ return :decade
211
+ end
212
+
213
+ case date
214
+ when EDTF::Season
192
215
  :month
193
- elsif date.is_a? EDTF::Interval
216
+ when EDTF::Unknown
217
+ :unknown
218
+ when EDTF::Interval
194
219
  date.precision
195
220
  else
196
221
  case date.precision
@@ -265,11 +290,11 @@ module CocinaDisplay
265
290
 
266
291
  # Decoded version of the date with "BCE" or "CE". Strips leading zeroes.
267
292
  # @param allowed_precisions [Array<Symbol>] List of allowed precisions for the output.
268
- # Defaults to [:day, :month, :year, :decade, :century].
293
+ # Defaults to [:day, :month, :year, :decade, :century, :unknown].
269
294
  # @param ignore_unparseable [Boolean] Return nil instead of the original value if it couldn't be parsed
270
295
  # @param display_original_value [Boolean] Return the original value if it was not encoded
271
296
  # @return [String]
272
- def decoded_value(allowed_precisions: [:day, :month, :year, :decade, :century], ignore_unparseable: false, display_original_value: true)
297
+ def decoded_value(allowed_precisions: [:day, :month, :year, :decade, :century, :unknown], ignore_unparseable: false, display_original_value: true)
273
298
  return if ignore_unparseable && !parsed_date?
274
299
  return value.strip unless parsed_date?
275
300
 
@@ -340,12 +365,13 @@ module CocinaDisplay
340
365
  # @param date [Date] The date to format.
341
366
  # @param precision [Symbol] The precision to format the date at, e.g. :month
342
367
  # @param allowed_precisions [Array<Symbol>] List of allowed precisions for the output.
343
- # Options are [:day, :month, :year, :decade, :century].
368
+ # Options are [:day, :month, :year, :decade, :century, :unknown].
344
369
  # @note allowed_precisions should be ordered by granularity, with most specific first.
345
370
  def format_date(date, precision, allowed_precisions)
346
371
  precision = allowed_precisions.first unless allowed_precisions.include?(precision)
347
-
348
372
  case precision
373
+ when :unknown
374
+ "Unknown"
349
375
  when :day
350
376
  date.strftime("%B %e, %Y")
351
377
  when :month
@@ -378,8 +404,6 @@ module CocinaDisplay
378
404
  return nil if date.nil?
379
405
 
380
406
  case date_range
381
- when EDTF::Unknown
382
- nil
383
407
  when EDTF::Epoch, EDTF::Interval, EDTF::Season
384
408
  date_range.min
385
409
  when EDTF::Set
@@ -400,8 +424,6 @@ module CocinaDisplay
400
424
  return nil if date.nil?
401
425
 
402
426
  case date_range
403
- when EDTF::Unknown
404
- nil
405
427
  when EDTF::Epoch, EDTF::Interval, EDTF::Season
406
428
  date_range.max
407
429
  when EDTF::Set
@@ -439,18 +461,37 @@ module CocinaDisplay
439
461
  [nil, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]
440
462
  end
441
463
  end
464
+
465
+ # Label for the date based on its type.
466
+ # @example "publication" becomes "Publication date"
467
+ # @return [String]
468
+ def type_label
469
+ I18n.t(
470
+ type || "untyped",
471
+ scope: "cocina_display.field_label.event.date",
472
+ type: type&.capitalize,
473
+ default: [:default]
474
+ )
475
+ end
442
476
  end
443
477
 
444
478
  # Strict ISO8601-encoded date parser.
445
479
  class Iso8601Format < Date
446
480
  def self.parse_date(value)
447
481
  ::Date.parse(normalize_to_edtf(value))
482
+ rescue ::Date::Error
483
+ notifier&.notify("Invalid date value \"#{value}\" for iso8601 encoding")
484
+ nil
448
485
  end
449
486
  end
450
487
 
451
488
  # Less strict W3CDTF-encoded date parser.
452
489
  class W3cdtfFormat < Date
453
490
  def self.normalize_to_edtf(value)
491
+ unless value
492
+ notifier&.notify("Invalid date value: #{value}")
493
+ return
494
+ end
454
495
  super.gsub("-00", "")
455
496
  end
456
497
  end
@@ -503,7 +544,7 @@ module CocinaDisplay
503
544
  # Base class for date formats that match using a regex.
504
545
  class ExtractorDateFormat < Date
505
546
  def self.supports?(value)
506
- value.match self::REGEX
547
+ self::REGEX.match?(value)
507
548
  end
508
549
  end
509
550
 
@@ -1,5 +1,3 @@
1
- require_relative "date"
2
-
3
1
  module CocinaDisplay
4
2
  module Dates
5
3
  # A date range parsed from Cocina structuredValues.
@@ -0,0 +1,41 @@
1
+ module CocinaDisplay
2
+ module Description
3
+ # Access information for a Cocina object.
4
+ class Access
5
+ attr_reader :cocina
6
+
7
+ # Create an Access object from Cocina structured data.
8
+ # @param cocina [Hash]
9
+ def initialize(cocina)
10
+ @cocina = cocina
11
+ end
12
+
13
+ # String representation of the access metadata.
14
+ # @return [String, nil]
15
+ def to_s
16
+ cocina["value"].presence
17
+ end
18
+
19
+ # The type of the access metadata, e.g. "repository".
20
+ # @return [String, nil]
21
+ def type
22
+ cocina["type"].presence
23
+ end
24
+
25
+ # The display label for the access metadata.
26
+ # @return [String]
27
+ def label
28
+ cocina["displayLabel"].presence ||
29
+ I18n.t(type&.parameterize&.underscore, default: :access, scope: "cocina_display.field_label.access")
30
+ end
31
+
32
+ # Whether the access info is a contact email.
33
+ # Always false, see CocinaDisplay::Description::AccessContact
34
+ # for cases when this is true
35
+ # @return [Boolean]
36
+ def contact_email?
37
+ false
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,11 @@
1
+ module CocinaDisplay
2
+ module Description
3
+ class AccessContact < Access
4
+ # Whether the access contact info is a contact email.
5
+ # @return [Boolean]
6
+ def contact_email?
7
+ type == "email"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ module CocinaDisplay
2
+ module Description
3
+ class Url < Access
4
+ # The display label for the URL access metadata.
5
+ # @return [String]
6
+ def label
7
+ I18n.t(:url, scope: "cocina_display.field_label.access")
8
+ end
9
+
10
+ # The link text for the URL access metadata.
11
+ # @return [String, nil]
12
+ def link_text
13
+ cocina["displayLabel"].presence
14
+ end
15
+ end
16
+ end
17
+ end