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.
- checksums.yaml +4 -4
- data/README.md +21 -16
- data/lib/cocina_display/cocina_record.rb +22 -7
- data/lib/cocina_display/concerns/contributors.rb +60 -41
- data/lib/cocina_display/concerns/events.rb +37 -25
- data/lib/cocina_display/concerns/forms.rb +134 -0
- data/lib/cocina_display/concerns/subjects.rb +34 -1
- data/lib/cocina_display/contributor.rb +54 -5
- data/lib/cocina_display/dates/date.rb +9 -8
- data/lib/cocina_display/dates/date_range.rb +8 -0
- data/lib/cocina_display/events/event.rb +78 -0
- data/lib/cocina_display/events/imprint.rb +101 -0
- data/lib/cocina_display/events/location.rb +56 -0
- data/lib/cocina_display/marc_relator_codes.rb +314 -0
- data/lib/cocina_display/title_builder.rb +2 -1
- data/lib/cocina_display/utils.rb +25 -2
- data/lib/cocina_display/version.rb +1 -1
- data/script/find_records.rb +85 -0
- metadata +22 -3
- data/lib/cocina_display/imprint.rb +0 -123
@@ -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?
|
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
|
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
|