cocina_display 0.4.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 +33 -72
- data/lib/cocina_display/concerns/access.rb +71 -0
- data/lib/cocina_display/concerns/contributors.rb +60 -41
- data/lib/cocina_display/concerns/events.rb +51 -25
- data/lib/cocina_display/concerns/forms.rb +134 -0
- data/lib/cocina_display/concerns/identifiers.rb +11 -3
- data/lib/cocina_display/concerns/subjects.rb +91 -0
- data/lib/cocina_display/contributor.rb +66 -11
- 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/subject.rb +127 -0
- data/lib/cocina_display/title_builder.rb +2 -1
- data/lib/cocina_display/utils.rb +45 -6
- data/lib/cocina_display/version.rb +1 -1
- data/script/find_records.rb +85 -0
- metadata +27 -11
- data/lib/cocina_display/imprint.rb +0 -123
@@ -0,0 +1,134 @@
|
|
1
|
+
require "active_support/core_ext/enumerable"
|
2
|
+
|
3
|
+
module CocinaDisplay
|
4
|
+
module Concerns
|
5
|
+
# Methods for extracting format/genre information from a Cocina object
|
6
|
+
module Forms
|
7
|
+
# Resource types of the object, expressed in SearchWorks controlled vocabulary.
|
8
|
+
# @return [Array<String>]
|
9
|
+
def resource_types
|
10
|
+
mapped_values = resource_type_values.flat_map { |resource_type| searchworks_resource_type(resource_type) }
|
11
|
+
mapped_values << "Dataset" if dataset?
|
12
|
+
mapped_values.uniq
|
13
|
+
end
|
14
|
+
|
15
|
+
# Physical or digital forms of the object.
|
16
|
+
# @return [Array<String>]
|
17
|
+
# @example GIS dataset (nz187ct8959)
|
18
|
+
# record.forms #=> ["map", "optical disc", "electronic resource"]
|
19
|
+
def forms
|
20
|
+
path("$.description.form..[?@.type == 'form'].value").uniq
|
21
|
+
end
|
22
|
+
|
23
|
+
# Extent of the object, such as "1 audiotape" or "1 map".
|
24
|
+
# @return [Array<String>]
|
25
|
+
# @example Oral history interview (sw705fr7011)
|
26
|
+
# record.extents #=> ["1 audiotape", "1 transcript"]
|
27
|
+
def extents
|
28
|
+
path("$.description.form..[?@.type == 'extent'].value").uniq
|
29
|
+
end
|
30
|
+
|
31
|
+
# Genres of the object, capitalized for display.
|
32
|
+
# @return [Array<String>]
|
33
|
+
# @example GIS dataset (nz187ct8959)
|
34
|
+
# record.genres #=> ["Cartographic dataset", "Geospatial data", "Geographic information systems data"]
|
35
|
+
def genres
|
36
|
+
path("$.description.form..[?@.type == 'genre'].value").map(&:upcase_first).uniq
|
37
|
+
end
|
38
|
+
|
39
|
+
# Genres of the object, with additional values added for search/faceting.
|
40
|
+
# @note These values are added for discovery in SearchWorks but not for display.
|
41
|
+
# @return [Array<String>]
|
42
|
+
def genres_search
|
43
|
+
genres.tap do |values|
|
44
|
+
values << "Thesis/Dissertation" if values.include?("Thesis")
|
45
|
+
values << "Conference proceedings" if values.include?("Conference publication")
|
46
|
+
values << "Government document" if values.include?("Government publication")
|
47
|
+
end.uniq
|
48
|
+
end
|
49
|
+
|
50
|
+
# Is the object a periodical or serial?
|
51
|
+
# @return [Boolean]
|
52
|
+
def periodical?
|
53
|
+
issuance_terms.include?("periodical") || issuance_terms.include?("serial") || frequency.any?
|
54
|
+
end
|
55
|
+
|
56
|
+
# Is the object a cartographic resource?
|
57
|
+
# @return [Boolean]
|
58
|
+
def cartographic?
|
59
|
+
resource_type_values.include?("cartographic")
|
60
|
+
end
|
61
|
+
|
62
|
+
# Is the object a web archive?
|
63
|
+
# @return [Boolean]
|
64
|
+
def archived_website?
|
65
|
+
genres.include?("Archived website")
|
66
|
+
end
|
67
|
+
|
68
|
+
# Is the object a dataset?
|
69
|
+
# @return [Boolean]
|
70
|
+
def dataset?
|
71
|
+
genres.include?("Dataset")
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
# Map a resource type to SearchWorks format value(s).
|
77
|
+
# @param resource_type [String] The resource type to map.
|
78
|
+
# @return [Array<String>]
|
79
|
+
def searchworks_resource_type(resource_type)
|
80
|
+
values = []
|
81
|
+
|
82
|
+
case resource_type
|
83
|
+
when "cartographic"
|
84
|
+
values << "Map"
|
85
|
+
when "manuscript", "mixed material"
|
86
|
+
values << "Archive/Manuscript"
|
87
|
+
when "moving image"
|
88
|
+
values << "Video"
|
89
|
+
when "notated music"
|
90
|
+
values << "Music score"
|
91
|
+
when "software, multimedia"
|
92
|
+
# Prevent GIS datasets from being labeled as "Software"
|
93
|
+
values << "Software/Multimedia" unless cartographic? || dataset?
|
94
|
+
when "sound recording-musical"
|
95
|
+
values << "Music recording"
|
96
|
+
when "sound recording-nonmusical", "sound recording"
|
97
|
+
values << "Sound recording"
|
98
|
+
when "still image"
|
99
|
+
values << "Image"
|
100
|
+
when "text"
|
101
|
+
# Can potentially map to periodical AND website if both are true. Only
|
102
|
+
# 2 records currently (2025) in Searchworks do this, but it is real.
|
103
|
+
if periodical? || archived_website?
|
104
|
+
values << "Journal/Periodical" if periodical?
|
105
|
+
values << "Archived website" if archived_website?
|
106
|
+
else
|
107
|
+
values << "Book"
|
108
|
+
end
|
109
|
+
when "three dimensional object"
|
110
|
+
values << "Object"
|
111
|
+
end
|
112
|
+
|
113
|
+
values.compact_blank
|
114
|
+
end
|
115
|
+
|
116
|
+
# Issuance terms for a work, drawn from the event notes.
|
117
|
+
# @return [Array<String>]
|
118
|
+
def issuance_terms
|
119
|
+
path("$.description.event.*.note[?@.type == 'issuance'].value").map(&:downcase).uniq
|
120
|
+
end
|
121
|
+
|
122
|
+
# Frequency terms for a periodical, drawn from the event notes.
|
123
|
+
# @return [Array<String>]
|
124
|
+
def frequency
|
125
|
+
path("$.description.event.*.note[?@.type == 'frequency'].value").map(&:downcase).uniq
|
126
|
+
end
|
127
|
+
|
128
|
+
# Values of the resource type form field prior to mapping.
|
129
|
+
def resource_type_values
|
130
|
+
path("$.description.form..[?@.type == 'resource type'].value").uniq
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -40,11 +40,19 @@ module CocinaDisplay
|
|
40
40
|
|
41
41
|
# The HRID of the item in FOLIO, if defined.
|
42
42
|
# @note This doesn't imply the object is available in Searchworks at this ID.
|
43
|
+
# @param [refresh] [Boolean] Filter to links with refresh set to this value.
|
43
44
|
# @return [String, nil]
|
44
|
-
# @example
|
45
|
+
# @example With a link regardless of refresh:
|
45
46
|
# record.folio_hrid #=> "a12845814"
|
46
|
-
|
47
|
-
|
47
|
+
# @example With a link that is not refreshed:
|
48
|
+
# record.folio_hrid(refresh: true) #=> nil
|
49
|
+
def folio_hrid(refresh: nil)
|
50
|
+
link = path("$.identification.catalogLinks[?(@.catalog == 'folio')]").first
|
51
|
+
hrid = link&.dig("catalogRecordId")
|
52
|
+
return if hrid.blank?
|
53
|
+
return hrid if refresh.nil?
|
54
|
+
|
55
|
+
(link["refresh"] == refresh) ? hrid : nil
|
48
56
|
end
|
49
57
|
|
50
58
|
# The FOLIO HRID if defined, otherwise the bare DRUID.
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require_relative "../subject"
|
2
|
+
|
3
|
+
module CocinaDisplay
|
4
|
+
module Concerns
|
5
|
+
# Methods for extracting and formatting subject information.
|
6
|
+
module Subjects
|
7
|
+
# All unique subjects that are topics, formatted as strings for display.
|
8
|
+
# @return [Array<String>]
|
9
|
+
def subject_topics
|
10
|
+
subjects.filter { |s| s.type == "topic" }.map(&:display_str).uniq
|
11
|
+
end
|
12
|
+
|
13
|
+
# All unique subjects that are genres, formatted as strings for display.
|
14
|
+
# @return [Array<String>]
|
15
|
+
def subject_genres
|
16
|
+
subjects.filter { |s| s.type == "genre" }.map(&:display_str).uniq
|
17
|
+
end
|
18
|
+
|
19
|
+
# All unique subjects that are titles, formatted as strings for display.
|
20
|
+
# @return [Array<String>]
|
21
|
+
def subject_titles
|
22
|
+
subjects.filter { |s| s.type == "title" }.map(&:display_str).uniq
|
23
|
+
end
|
24
|
+
|
25
|
+
# All unique subjects that are date/time info, formatted as strings for display.
|
26
|
+
# @return [Array<String>]
|
27
|
+
def subject_temporal
|
28
|
+
subjects.filter { |s| s.type == "time" }.map(&:display_str).uniq
|
29
|
+
end
|
30
|
+
|
31
|
+
# All unique subjects that are occupations, formatted as strings for display.
|
32
|
+
# @return [Array<String>]
|
33
|
+
def subject_occupations
|
34
|
+
subjects.filter { |s| s.type == "occupation" }.map(&:display_str).uniq
|
35
|
+
end
|
36
|
+
|
37
|
+
# All unique subjects that are names of entities, formatted as strings for display.
|
38
|
+
# @note Multiple types are handled: person, family, organization, conference, etc.
|
39
|
+
# @see CocinaDisplay::NameSubject
|
40
|
+
# @return [Array<String>]
|
41
|
+
def subject_names
|
42
|
+
subjects.filter { |s| s.is_a? NameSubject }.map(&:display_str).uniq
|
43
|
+
end
|
44
|
+
|
45
|
+
# Combination of all subject values for searching.
|
46
|
+
# @see #subject_topics_other
|
47
|
+
# @see #subject_temporal_genre
|
48
|
+
# @return [Array<String>]
|
49
|
+
def subject_all
|
50
|
+
subject_topics_other + subject_temporal_genre
|
51
|
+
end
|
52
|
+
|
53
|
+
# Combination of topic, occupation, name, and title subject values for searching.
|
54
|
+
# @see #subject_topics
|
55
|
+
# @see #subject_other
|
56
|
+
# @return [Array<String>]
|
57
|
+
def subject_topics_other
|
58
|
+
subject_topics + subject_other
|
59
|
+
end
|
60
|
+
|
61
|
+
# Combination of occupation, name, and title subject values for searching.
|
62
|
+
# @see #subject_occupations
|
63
|
+
# @see #subject_names
|
64
|
+
# @see #subject_titles
|
65
|
+
# @return [Array<String>]
|
66
|
+
def subject_other
|
67
|
+
subject_occupations + subject_names + subject_titles
|
68
|
+
end
|
69
|
+
|
70
|
+
# Combination of temporal and genre subject values for searching.
|
71
|
+
# @see #subject_temporal
|
72
|
+
# @see #subject_genres
|
73
|
+
# @return [Array<String>]
|
74
|
+
def subject_temporal_genre
|
75
|
+
subject_temporal + subject_genres
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
# All subjects, accessible as Subject objects.
|
81
|
+
# Checks both description.subject and description.geographic.subject.
|
82
|
+
# @return [Array<Subject>]
|
83
|
+
def subjects
|
84
|
+
@subjects ||= Enumerator::Chain.new(
|
85
|
+
path("$.description.subject[*]"),
|
86
|
+
path("$.description.geographic.*.subject[*]")
|
87
|
+
).map { |s| Subject.from_cocina(s) }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -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.
|
@@ -100,6 +105,10 @@ module CocinaDisplay
|
|
100
105
|
end
|
101
106
|
|
102
107
|
# The display string for the name, optionally including life dates.
|
108
|
+
# Uses these values in order, if present:
|
109
|
+
# 1. Unstructured value
|
110
|
+
# 2. Any structured/parallel values marked as "display"
|
111
|
+
# 3. Joined structured values, optionally with life dates
|
103
112
|
# @param with_date [Boolean] Include life dates, if present
|
104
113
|
# @return [String]
|
105
114
|
# @example no dates
|
@@ -107,7 +116,11 @@ module CocinaDisplay
|
|
107
116
|
# @example with dates
|
108
117
|
# name.display_name(with_date: true) # => "King, Martin Luther, Jr., 1929-1968"
|
109
118
|
def display_str(with_date: false)
|
110
|
-
if
|
119
|
+
if cocina["value"].present?
|
120
|
+
cocina["value"]
|
121
|
+
elsif display_name_str.present?
|
122
|
+
display_name_str
|
123
|
+
elsif dates_str.present? && with_date
|
111
124
|
Utils.compact_and_join([full_name_str, dates_str], delimiter: ", ")
|
112
125
|
else
|
113
126
|
full_name_str
|
@@ -116,12 +129,10 @@ module CocinaDisplay
|
|
116
129
|
|
117
130
|
private
|
118
131
|
|
119
|
-
# The full name as a string.
|
120
|
-
# If any names were marked as "display", prefer those.
|
121
|
-
# Otherwise, combine all name components.
|
132
|
+
# The full name as a string, combining all name components.
|
122
133
|
# @return [String]
|
123
134
|
def full_name_str
|
124
|
-
|
135
|
+
Utils.compact_and_join(name_components, delimiter: ", ")
|
125
136
|
end
|
126
137
|
|
127
138
|
# Flattened form of any names explicitly marked as "display name".
|
@@ -168,12 +179,56 @@ module CocinaDisplay
|
|
168
179
|
# @see https://github.com/sul-dlss/cocina-models/blob/main/docs/description_types.md#contributor-name-part-types-for-structured-value
|
169
180
|
# @note Currently we do nothing with "alternative", "inverted full name", "pseudonym", and "transliteration" types.
|
170
181
|
def name_values
|
171
|
-
Utils.
|
182
|
+
Utils.flatten_nested_values(cocina).each_with_object({}) do |node, hash|
|
172
183
|
type = node["type"] || "name"
|
173
184
|
hash[type] ||= []
|
174
185
|
hash[type] << node["value"]
|
175
186
|
end.compact_blank
|
176
187
|
end
|
177
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
|
178
233
|
end
|
179
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
|