cocina_display 0.5.0 → 0.6.0

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.
@@ -5,6 +5,7 @@ require "active_support/core_ext/object/blank"
5
5
  require "active_support/core_ext/array/conversions"
6
6
 
7
7
  require_relative "utils"
8
+ require_relative "marc_relator_codes"
8
9
 
9
10
  module CocinaDisplay
10
11
  # A contributor to a work, such as an author or publisher.
@@ -51,7 +52,13 @@ module CocinaDisplay
51
52
  # Does this contributor have a role that indicates they are an author?
52
53
  # @return [Boolean]
53
54
  def author?
54
- roles.any? { |role| role["value"] =~ /(author|creator)/i }
55
+ roles.any?(&:author?)
56
+ end
57
+
58
+ # Does this contributor have a role that indicates they are a publisher?
59
+ # @return [Boolean]
60
+ def publisher?
61
+ roles.any?(&:publisher?)
55
62
  end
56
63
 
57
64
  # Does this contributor have any roles defined?
@@ -72,11 +79,9 @@ module CocinaDisplay
72
79
  # If there are multiple roles, they are joined with commas.
73
80
  # @return [String]
74
81
  def display_role
75
- roles.map { |role| role["value"] }.to_sentence
82
+ roles.map(&:display_str).to_sentence
76
83
  end
77
84
 
78
- private
79
-
80
85
  # All names in the Cocina as Name objects.
81
86
  # @return [Array<Name>]
82
87
  def names
@@ -86,7 +91,7 @@ module CocinaDisplay
86
91
  # All roles in the Cocina structured data.
87
92
  # @return [Array<Hash>]
88
93
  def roles
89
- Array(cocina["role"])
94
+ @roles ||= Array(cocina["role"]).map { |role| Role.new(role) }
90
95
  end
91
96
 
92
97
  # A name associated with a contributor.
@@ -181,5 +186,49 @@ module CocinaDisplay
181
186
  end.compact_blank
182
187
  end
183
188
  end
189
+
190
+ # A role associated with a contributor.
191
+ class Role
192
+ attr_reader :cocina
193
+
194
+ # Initialize a Role object with Cocina structured data.
195
+ # @param cocina [Hash] The Cocina structured data for the role.
196
+ def initialize(cocina)
197
+ @cocina = cocina
198
+ end
199
+
200
+ # The name of the role.
201
+ # Translates the MARC relator code if no value was present.
202
+ # @return [String, nil]
203
+ def display_str
204
+ cocina["value"] || (MARC_RELATOR[code] if marc_relator?)
205
+ end
206
+
207
+ # A code associated with the role, e.g. a MARC relator code.
208
+ # @return [String, nil]
209
+ def code
210
+ cocina["code"]
211
+ end
212
+
213
+ # Does this role indicate the contributor is an author?
214
+ # @return [Boolean]
215
+ def author?
216
+ display_str =~ /^(author|creator)/i
217
+ end
218
+
219
+ # Does this role indicate the contributor is a publisher?
220
+ # @return [Boolean]
221
+ def publisher?
222
+ display_str =~ /^publisher/i
223
+ end
224
+
225
+ private
226
+
227
+ # Does this role have a MARC relator code?
228
+ # @return [Boolean]
229
+ def marc_relator?
230
+ cocina.dig("source", "code") == "marcrelator"
231
+ end
232
+ end
184
233
  end
185
234
  end
@@ -82,9 +82,14 @@ module CocinaDisplay
82
82
 
83
83
  attr_reader :cocina, :date
84
84
 
85
+ # The type of this date, if any, such as "creation", "publication", etc.
86
+ # @return [String, nil]
87
+ attr_accessor :type
88
+
85
89
  def initialize(cocina)
86
90
  @cocina = cocina
87
91
  @date = self.class.parse_date(cocina["value"])
92
+ @type = cocina["type"] unless ["start", "end"].include?(cocina["type"])
88
93
  end
89
94
 
90
95
  # Compare this date to another {Date} or {DateRange} using its {sort_key}.
@@ -98,12 +103,6 @@ module CocinaDisplay
98
103
  cocina["value"]
99
104
  end
100
105
 
101
- # The type of this date, if any, such as "creation", "publication", etc.
102
- # @return [String, nil]
103
- def type
104
- cocina["type"]
105
- end
106
-
107
106
  # The qualifier for this date, if any, such as "approximate", "inferred", etc.
108
107
  # @return [String, nil]
109
108
  def qualifier
@@ -132,14 +131,16 @@ module CocinaDisplay
132
131
 
133
132
  # Is this the start date in a range?
134
133
  # @return [Boolean]
134
+ # @note The Cocina will mark start dates with "type": "start".
135
135
  def start?
136
- type == "start"
136
+ cocina["type"] == "start"
137
137
  end
138
138
 
139
139
  # Is this the end date in a range?
140
140
  # @return [Boolean]
141
+ # @note The Cocina will mark end dates with "type": "end".
141
142
  def end?
142
- type == "end"
143
+ cocina["type"] == "end"
143
144
  end
144
145
 
145
146
  # Was the date marked as approximate?
@@ -30,6 +30,7 @@ module CocinaDisplay
30
30
  @cocina = cocina
31
31
  @start = start
32
32
  @stop = stop
33
+ @type = cocina["type"]
33
34
  end
34
35
 
35
36
  # The values of the start and stop dates as an array.
@@ -83,6 +84,13 @@ module CocinaDisplay
83
84
  start&.parsed_date? || stop&.parsed_date? || false
84
85
  end
85
86
 
87
+ # False if both dates in the range have a known unparsable value like "9999".
88
+ # @see CocinaDisplay::Date#parsable?
89
+ # @return [Boolean]
90
+ def parsable?
91
+ start&.parsable? || stop&.parsable? || false
92
+ end
93
+
86
94
  # Decoded version of the range, if it was encoded. Strips leading zeroes.
87
95
  # @see CocinaDisplay::Date#decoded_value
88
96
  # @return [String]
@@ -0,0 +1,78 @@
1
+ require_relative "location"
2
+ require_relative "../dates/date"
3
+ require_relative "../contributor"
4
+
5
+ module CocinaDisplay
6
+ module Events
7
+ # An event associated with an object, like publication.
8
+ class Event
9
+ attr_reader :cocina
10
+
11
+ # Initialize the event with Cocina event data.
12
+ # @param cocina [Hash] Cocina structured data for a single event
13
+ def initialize(cocina)
14
+ @cocina = cocina
15
+ end
16
+
17
+ # The declared type of the event, like "publication" or "creation".
18
+ # @see https://github.com/sul-dlss/cocina-models/blob/main/docs/description_types.md#event-types
19
+ # @note This can differ from the contained date types.
20
+ # @return [String, nil]
21
+ def type
22
+ cocina["type"]
23
+ end
24
+
25
+ # All types of dates associated with this event.
26
+ # @see https://github.com/sul-dlss/cocina-models/blob/main/docs/description_types.md#event-date-types
27
+ # @note This can differ from the top-level event type.
28
+ # @return [Array<String>]
29
+ def date_types
30
+ dates.map(&:type).uniq
31
+ end
32
+
33
+ # True if either the event type or any date type matches the given type.
34
+ # @param match_type [String] The type to check against
35
+ # @return [Boolean]
36
+ def has_type?(match_type)
37
+ [type, *date_types].compact.include?(match_type)
38
+ end
39
+
40
+ # True if the event or its dates have any of the provided types.
41
+ # @param match_types [Array<String>] The types to check against
42
+ # @return [Boolean]
43
+ def has_any_type?(*match_types)
44
+ match_types.any? { |type| has_type?(type) }
45
+ end
46
+
47
+ # All dates associated with this event.
48
+ # Ignores known unparsable date values like "9999".
49
+ # If the date is untyped, uses this event's type as the date type.
50
+ # @note The date types may differ from the underlying event type.
51
+ # @return [Array<CocinaDisplay::Dates::Date>]
52
+ def dates
53
+ @dates ||= Array(cocina["date"]).filter_map do |date|
54
+ CocinaDisplay::Dates::Date.from_cocina(date)
55
+ end.filter(&:parsable?).map do |date|
56
+ date.type ||= type
57
+ date
58
+ end
59
+ end
60
+
61
+ # All contributors associated with this event.
62
+ # @return [Array<CocinaDisplay::Contributor>]
63
+ def contributors
64
+ @contributors ||= Array(cocina["contributor"]).map do |contributor|
65
+ CocinaDisplay::Contributor.new(contributor)
66
+ end
67
+ end
68
+
69
+ # All locations associated with this event.
70
+ # @return [Array<CocinaDisplay::Events::Location>]
71
+ def locations
72
+ @locations ||= Array(cocina["location"]).map do |location|
73
+ CocinaDisplay::Events::Location.new(location)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "edtf"
4
+ require "active_support"
5
+ require "active_support/core_ext/enumerable"
6
+ require "active_support/core_ext/object/blank"
7
+
8
+ require_relative "event"
9
+ require_relative "../utils"
10
+ require_relative "../marc_country_codes"
11
+ require_relative "../dates/date"
12
+ require_relative "../dates/date_range"
13
+
14
+ module CocinaDisplay
15
+ module Events
16
+ # Wrapper for Cocina events used to generate an imprint statement for display.
17
+ class Imprint < Event
18
+ # The entire imprint statement formatted as a string for display.
19
+ # @return [String]
20
+ def display_str
21
+ place_pub = Utils.compact_and_join([place_str, publisher_str], delimiter: " : ")
22
+ edition_place_pub = Utils.compact_and_join([edition_str, place_pub], delimiter: " - ")
23
+ Utils.compact_and_join([edition_place_pub, date_str], delimiter: ", ")
24
+ end
25
+
26
+ # Were any of the dates encoded?
27
+ # Used to detect which event(s) most likely represent the actual imprint(s).
28
+ def date_encoding?
29
+ dates.any?(&:encoding?)
30
+ end
31
+
32
+ private
33
+
34
+ # The date portion of the imprint statement, comprising all unique dates.
35
+ # @return [String]
36
+ def date_str
37
+ Utils.compact_and_join(unique_dates_for_display.map(&:qualified_value))
38
+ end
39
+
40
+ # The editions portion of the imprint statement, combining all edition notes.
41
+ # @return [String]
42
+ def edition_str
43
+ Utils.compact_and_join(Janeway.enum_for("$.note[?@.type == 'edition'].value", cocina))
44
+ end
45
+
46
+ # The place of publication, combining all location values.
47
+ # @return [String]
48
+ def place_str
49
+ Utils.compact_and_join(locations_for_display, delimiter: " : ")
50
+ end
51
+
52
+ # The publisher information, combining all name values for publishers.
53
+ # @return [String]
54
+ def publisher_str
55
+ Utils.compact_and_join(publishers.map(&:display_name), delimiter: " : ")
56
+ end
57
+
58
+ # All publishers associated with this imprint.
59
+ # @return [Array<CocinaDisplay::Contributor>]
60
+ # @see CocinaDisplay::Contributor#publisher?
61
+ def publishers
62
+ contributors.filter(&:publisher?)
63
+ end
64
+
65
+ # Filter dates for uniqueness using base value according to predefined rules.
66
+ # 1. For a group of dates with the same base value, choose a single one
67
+ # 2. Prefer unencoded dates over encoded ones when choosing a single date
68
+ # 3. Remove date ranges that duplicate any unencoded non-range dates
69
+ # @return [Array<CocinaDisplay::Dates::Date>]
70
+ # @see CocinaDisplay::Dates::Date#base_value
71
+ # @see https://consul.stanford.edu/display/chimera/MODS+display+rules#MODSdisplayrules-3b.%3CoriginInfo%3E
72
+ def unique_dates_for_display
73
+ # Choose a single date for each group with the same base value
74
+ deduped_dates = dates.group_by(&:base_value).map do |base_value, group|
75
+ if (unencoded = group.reject(&:encoding?)).any?
76
+ unencoded.first
77
+ else
78
+ group.first
79
+ end
80
+ end
81
+
82
+ # Remove any ranges that duplicate part of an unencoded non-range date
83
+ ranges, singles = deduped_dates.partition { |date| date.is_a?(CocinaDisplay::Dates::DateRange) }
84
+ unencoded_singles_dates = singles.reject(&:encoding?).flat_map(&:to_a)
85
+ ranges.reject! { |range| unencoded_singles_dates.any? { |date| range.as_interval.include?(date) } }
86
+
87
+ (singles + ranges).sort
88
+ end
89
+
90
+ # Filter locations to display according to predefined rules.
91
+ # 1. Prefer unencoded locations (plain value) over encoded ones
92
+ # 2. If no unencoded locations but there are MARC country codes, decode them
93
+ # 3. Keep only unique locations after decoding
94
+ def locations_for_display
95
+ unencoded_locs, encoded_locs = locations.partition { |loc| loc.unencoded_value? }
96
+ locs_for_display = unencoded_locs.presence || encoded_locs
97
+ locs_for_display.map(&:display_str).compact_blank.uniq
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,56 @@
1
+ require_relative "../marc_country_codes"
2
+
3
+ module CocinaDisplay
4
+ module Events
5
+ # A single location represented in a Cocina event, like a publication place.
6
+ class Location
7
+ attr_reader :cocina
8
+
9
+ # Initialize a Location object with Cocina structured data.
10
+ # @param cocina [Hash] The Cocina structured data for the location.
11
+ def initialize(cocina)
12
+ @cocina = cocina
13
+ end
14
+
15
+ # The name of the location.
16
+ # Decodes a MARC country code if present and no value was present.
17
+ # @return [String, nil]
18
+ def display_str
19
+ cocina["value"] || decoded_country
20
+ end
21
+
22
+ # Is there an unencoded value (name) for this location?
23
+ # @return [Boolean]
24
+ def unencoded_value?
25
+ cocina["value"].present?
26
+ end
27
+
28
+ private
29
+
30
+ # A code, like a MARC country code, representing the location.
31
+ # @return [String, nil]
32
+ def code
33
+ cocina["code"]
34
+ end
35
+
36
+ # Decoded country name if the location is encoded with a MARC country code.
37
+ # @return [String, nil]
38
+ def decoded_country
39
+ MARC_COUNTRY[code] if marc_country? && valid_country_code?
40
+ end
41
+
42
+ # Is this a decodable country code?
43
+ # Excludes blank values and "xx" (unknown) and "vp" (various places).
44
+ # @return [Boolean]
45
+ def valid_country_code?
46
+ code.present? && ["xx", "vp"].exclude?(code)
47
+ end
48
+
49
+ # Is this location encoded with a MARC country code?
50
+ # @return [Boolean]
51
+ def marc_country?
52
+ cocina.dig("source", "code") == "marccountry"
53
+ end
54
+ end
55
+ end
56
+ end