cocina_display 0.5.0 → 0.7.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 +24 -7
- data/lib/cocina_display/concerns/contributors.rb +64 -41
- data/lib/cocina_display/concerns/events.rb +37 -25
- data/lib/cocina_display/concerns/forms.rb +134 -0
- data/lib/cocina_display/concerns/languages.rb +20 -0
- data/lib/cocina_display/concerns/subjects.rb +63 -16
- data/lib/cocina_display/contributor.rb +57 -8
- data/lib/cocina_display/dates/date.rb +9 -8
- data/lib/cocina_display/dates/date_range.rb +29 -9
- data/lib/cocina_display/events/event.rb +78 -0
- data/lib/cocina_display/events/imprint.rb +100 -0
- data/lib/cocina_display/events/location.rb +56 -0
- data/lib/cocina_display/language.rb +47 -0
- data/lib/cocina_display/subjects/subject.rb +63 -0
- data/lib/cocina_display/subjects/subject_value.rb +104 -0
- data/lib/cocina_display/title_builder.rb +2 -1
- data/lib/cocina_display/utils.rb +30 -5
- data/lib/cocina_display/version.rb +1 -1
- data/lib/cocina_display/vocabularies/marc_country_codes.rb +393 -0
- data/lib/cocina_display/vocabularies/marc_relator_codes.rb +318 -0
- data/lib/cocina_display/vocabularies/searchworks_languages.rb +526 -0
- data/script/find_records.rb +85 -0
- metadata +42 -5
- data/lib/cocina_display/imprint.rb +0 -123
- data/lib/cocina_display/marc_country_codes.rb +0 -394
- data/lib/cocina_display/subject.rb +0 -127
@@ -1,45 +1,86 @@
|
|
1
|
-
require_relative "../subject"
|
1
|
+
require_relative "../subjects/subject"
|
2
|
+
require_relative "../subjects/subject_value"
|
2
3
|
|
3
4
|
module CocinaDisplay
|
4
5
|
module Concerns
|
5
6
|
# Methods for extracting and formatting subject information.
|
6
7
|
module Subjects
|
7
|
-
# All unique
|
8
|
+
# All unique subject values that are topics.
|
8
9
|
# @return [Array<String>]
|
9
10
|
def subject_topics
|
10
|
-
|
11
|
+
subject_values.filter { |s| s.type == "topic" }.map(&:display_str).uniq
|
11
12
|
end
|
12
13
|
|
13
|
-
# All unique
|
14
|
+
# All unique subject values that are genres.
|
14
15
|
# @return [Array<String>]
|
15
16
|
def subject_genres
|
16
|
-
|
17
|
+
subject_values.filter { |s| s.type == "genre" }.map(&:display_str).uniq
|
17
18
|
end
|
18
19
|
|
19
|
-
# All unique
|
20
|
+
# All unique subject values that are titles.
|
20
21
|
# @return [Array<String>]
|
21
22
|
def subject_titles
|
22
|
-
|
23
|
+
subject_values.filter { |s| s.type == "title" }.map(&:display_str).uniq
|
23
24
|
end
|
24
25
|
|
25
|
-
# All unique
|
26
|
+
# All unique subject values that are date/time info.
|
26
27
|
# @return [Array<String>]
|
27
28
|
def subject_temporal
|
28
|
-
|
29
|
+
subject_values.filter { |s| s.type == "time" }.map(&:display_str).uniq
|
29
30
|
end
|
30
31
|
|
31
|
-
# All unique
|
32
|
+
# All unique subject values that are occupations.
|
32
33
|
# @return [Array<String>]
|
33
34
|
def subject_occupations
|
34
|
-
|
35
|
+
subject_values.filter { |s| s.type == "occupation" }.map(&:display_str).uniq
|
35
36
|
end
|
36
37
|
|
37
|
-
# All unique
|
38
|
+
# All unique subject values that are names of entities.
|
38
39
|
# @note Multiple types are handled: person, family, organization, conference, etc.
|
39
|
-
# @see CocinaDisplay::
|
40
|
+
# @see CocinaDisplay::NameSubjectValue
|
40
41
|
# @return [Array<String>]
|
41
42
|
def subject_names
|
42
|
-
|
43
|
+
subject_values.filter { |s| s.is_a? CocinaDisplay::Subjects::NameSubjectValue }.map(&:display_str).uniq
|
44
|
+
end
|
45
|
+
|
46
|
+
# Combination of all subject values for searching.
|
47
|
+
# @see #subject_topics_other
|
48
|
+
# @see #subject_temporal_genre
|
49
|
+
# @return [Array<String>]
|
50
|
+
def subject_all
|
51
|
+
subject_topics_other + subject_temporal_genre
|
52
|
+
end
|
53
|
+
|
54
|
+
# Combination of topic, occupation, name, and title subject values for searching.
|
55
|
+
# @see #subject_topics
|
56
|
+
# @see #subject_other
|
57
|
+
# @return [Array<String>]
|
58
|
+
def subject_topics_other
|
59
|
+
subject_topics + subject_other
|
60
|
+
end
|
61
|
+
|
62
|
+
# Combination of occupation, name, and title subject values for searching.
|
63
|
+
# @see #subject_occupations
|
64
|
+
# @see #subject_names
|
65
|
+
# @see #subject_titles
|
66
|
+
# @return [Array<String>]
|
67
|
+
def subject_other
|
68
|
+
subject_occupations + subject_names + subject_titles
|
69
|
+
end
|
70
|
+
|
71
|
+
# Combination of temporal and genre subject values for searching.
|
72
|
+
# @see #subject_temporal
|
73
|
+
# @see #subject_genres
|
74
|
+
# @return [Array<String>]
|
75
|
+
def subject_temporal_genre
|
76
|
+
subject_temporal + subject_genres
|
77
|
+
end
|
78
|
+
|
79
|
+
# Combination of all subjects with nested values concatenated for display.
|
80
|
+
# @see Subject#display_str
|
81
|
+
# @return [Array<String>]
|
82
|
+
def subject_all_display
|
83
|
+
subjects.map(&:display_str).uniq
|
43
84
|
end
|
44
85
|
|
45
86
|
private
|
@@ -50,8 +91,14 @@ module CocinaDisplay
|
|
50
91
|
def subjects
|
51
92
|
@subjects ||= Enumerator::Chain.new(
|
52
93
|
path("$.description.subject[*]"),
|
53
|
-
path("$.description.geographic
|
54
|
-
).map { |s| Subject.
|
94
|
+
path("$.description.geographic.*.subject[*]")
|
95
|
+
).map { |s| CocinaDisplay::Subjects::Subject.new(s) }
|
96
|
+
end
|
97
|
+
|
98
|
+
# All subject values, flattened from all subjects.
|
99
|
+
# @return [Array<SubjectValue>]
|
100
|
+
def subject_values
|
101
|
+
@subject_values ||= subjects.flat_map(&:subject_values)
|
55
102
|
end
|
56
103
|
end
|
57
104
|
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 "vocabularies/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.
|
@@ -124,10 +129,10 @@ module CocinaDisplay
|
|
124
129
|
|
125
130
|
private
|
126
131
|
|
127
|
-
# The full name as a string, combining all name components.
|
132
|
+
# The full name as a string, combining all name components and terms of address.
|
128
133
|
# @return [String]
|
129
134
|
def full_name_str
|
130
|
-
Utils.compact_and_join(name_components, delimiter: ", ")
|
135
|
+
Utils.compact_and_join(name_components.push(terms_of_address_str), delimiter: ", ")
|
131
136
|
end
|
132
137
|
|
133
138
|
# Flattened form of any names explicitly marked as "display name".
|
@@ -141,7 +146,7 @@ module CocinaDisplay
|
|
141
146
|
# Otherwise, fall back to any names explicitly marked as "name" or untyped.
|
142
147
|
# @return [Array<String>]
|
143
148
|
def name_components
|
144
|
-
[surname_str, forename_ordinal_str
|
149
|
+
[surname_str, forename_ordinal_str].compact_blank.presence || Array(name_values["name"])
|
145
150
|
end
|
146
151
|
|
147
152
|
# Flatten all forenames and ordinals into a single string.
|
@@ -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"] || (Vocabularies::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.
|
@@ -62,7 +63,18 @@ module CocinaDisplay
|
|
62
63
|
start&.encoding || stop&.encoding || super
|
63
64
|
end
|
64
65
|
|
65
|
-
#
|
66
|
+
# The qualifier for the entire range.
|
67
|
+
# If both qualifiers match, uses that qualifier. If both are empty, falls
|
68
|
+
# back to the top level qualifier, if any.
|
69
|
+
# @see CocinaDisplay::Date#qualifier
|
70
|
+
# @return [String, nil]
|
71
|
+
def qualifier
|
72
|
+
if start&.qualifier == stop&.qualifier
|
73
|
+
start&.qualifier || stop&.qualifier || super
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Is either date in the range, or the range itself, qualified?
|
66
78
|
# @see CocinaDisplay::Date#qualified?
|
67
79
|
# @return [Boolean]
|
68
80
|
def qualified?
|
@@ -83,6 +95,13 @@ module CocinaDisplay
|
|
83
95
|
start&.parsed_date? || stop&.parsed_date? || false
|
84
96
|
end
|
85
97
|
|
98
|
+
# False if both dates in the range have a known unparsable value like "9999".
|
99
|
+
# @see CocinaDisplay::Date#parsable?
|
100
|
+
# @return [Boolean]
|
101
|
+
def parsable?
|
102
|
+
start&.parsable? || stop&.parsable? || false
|
103
|
+
end
|
104
|
+
|
86
105
|
# Decoded version of the range, if it was encoded. Strips leading zeroes.
|
87
106
|
# @see CocinaDisplay::Date#decoded_value
|
88
107
|
# @return [String]
|
@@ -97,14 +116,15 @@ module CocinaDisplay
|
|
97
116
|
# @see CocinaDisplay::Date#qualified_value
|
98
117
|
# @return [String]
|
99
118
|
def qualified_value
|
100
|
-
if
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
119
|
+
if qualifier
|
120
|
+
case qualifier
|
121
|
+
when "approximate"
|
122
|
+
"[ca. #{decoded_value}]"
|
123
|
+
when "questionable"
|
124
|
+
"[#{decoded_value}?]"
|
125
|
+
when "inferred"
|
126
|
+
"[#{decoded_value}]"
|
127
|
+
end
|
108
128
|
else
|
109
129
|
"#{start&.qualified_value} - #{stop&.qualified_value}"
|
110
130
|
end
|
@@ -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,100 @@
|
|
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 "../dates/date"
|
11
|
+
require_relative "../dates/date_range"
|
12
|
+
|
13
|
+
module CocinaDisplay
|
14
|
+
module Events
|
15
|
+
# Wrapper for Cocina events used to generate an imprint statement for display.
|
16
|
+
class Imprint < Event
|
17
|
+
# The entire imprint statement formatted as a string for display.
|
18
|
+
# @return [String]
|
19
|
+
def display_str
|
20
|
+
place_pub = Utils.compact_and_join([place_str, publisher_str], delimiter: " : ")
|
21
|
+
edition_place_pub = Utils.compact_and_join([edition_str, place_pub], delimiter: " - ")
|
22
|
+
Utils.compact_and_join([edition_place_pub, date_str], delimiter: ", ")
|
23
|
+
end
|
24
|
+
|
25
|
+
# Were any of the dates encoded?
|
26
|
+
# Used to detect which event(s) most likely represent the actual imprint(s).
|
27
|
+
def date_encoding?
|
28
|
+
dates.any?(&:encoding?)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# The date portion of the imprint statement, comprising all unique dates.
|
34
|
+
# @return [String]
|
35
|
+
def date_str
|
36
|
+
Utils.compact_and_join(unique_dates_for_display.map(&:qualified_value))
|
37
|
+
end
|
38
|
+
|
39
|
+
# The editions portion of the imprint statement, combining all edition notes.
|
40
|
+
# @return [String]
|
41
|
+
def edition_str
|
42
|
+
Utils.compact_and_join(Janeway.enum_for("$.note[?@.type == 'edition'].value", cocina))
|
43
|
+
end
|
44
|
+
|
45
|
+
# The place of publication, combining all location values.
|
46
|
+
# @return [String]
|
47
|
+
def place_str
|
48
|
+
Utils.compact_and_join(locations_for_display, delimiter: " : ")
|
49
|
+
end
|
50
|
+
|
51
|
+
# The publisher information, combining all name values for publishers.
|
52
|
+
# @return [String]
|
53
|
+
def publisher_str
|
54
|
+
Utils.compact_and_join(publishers.map(&:display_name), delimiter: " : ")
|
55
|
+
end
|
56
|
+
|
57
|
+
# All publishers associated with this imprint.
|
58
|
+
# @return [Array<CocinaDisplay::Contributor>]
|
59
|
+
# @see CocinaDisplay::Contributor#publisher?
|
60
|
+
def publishers
|
61
|
+
contributors.filter(&:publisher?)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Filter dates for uniqueness using base value according to predefined rules.
|
65
|
+
# 1. For a group of dates with the same base value, choose a single one
|
66
|
+
# 2. Prefer unencoded dates over encoded ones when choosing a single date
|
67
|
+
# 3. Remove date ranges that duplicate any unencoded non-range dates
|
68
|
+
# @return [Array<CocinaDisplay::Dates::Date>]
|
69
|
+
# @see CocinaDisplay::Dates::Date#base_value
|
70
|
+
# @see https://consul.stanford.edu/display/chimera/MODS+display+rules#MODSdisplayrules-3b.%3CoriginInfo%3E
|
71
|
+
def unique_dates_for_display
|
72
|
+
# Choose a single date for each group with the same base value
|
73
|
+
deduped_dates = dates.group_by(&:base_value).map do |base_value, group|
|
74
|
+
if (unencoded = group.reject(&:encoding?)).any?
|
75
|
+
unencoded.first
|
76
|
+
else
|
77
|
+
group.first
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Remove any ranges that duplicate part of an unencoded non-range date
|
82
|
+
ranges, singles = deduped_dates.partition { |date| date.is_a?(CocinaDisplay::Dates::DateRange) }
|
83
|
+
unencoded_singles_dates = singles.reject(&:encoding?).flat_map(&:to_a)
|
84
|
+
ranges.reject! { |range| unencoded_singles_dates.any? { |date| range.as_interval.include?(date) } }
|
85
|
+
|
86
|
+
(singles + ranges).sort
|
87
|
+
end
|
88
|
+
|
89
|
+
# Filter locations to display according to predefined rules.
|
90
|
+
# 1. Prefer unencoded locations (plain value) over encoded ones
|
91
|
+
# 2. If no unencoded locations but there are MARC country codes, decode them
|
92
|
+
# 3. Keep only unique locations after decoding
|
93
|
+
def locations_for_display
|
94
|
+
unencoded_locs, encoded_locs = locations.partition { |loc| loc.unencoded_value? }
|
95
|
+
locs_for_display = unencoded_locs.presence || encoded_locs
|
96
|
+
locs_for_display.map(&:display_str).compact_blank.uniq
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require_relative "../vocabularies/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
|
+
Vocabularies::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
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require "iso639"
|
2
|
+
require_relative "vocabularies/searchworks_languages"
|
3
|
+
|
4
|
+
module CocinaDisplay
|
5
|
+
# A language associated with part or all of a Cocina object.
|
6
|
+
class Language
|
7
|
+
attr_reader :cocina
|
8
|
+
|
9
|
+
# Create a Language object from Cocina structured data.
|
10
|
+
# @param cocina [Hash]
|
11
|
+
def initialize(cocina)
|
12
|
+
@cocina = cocina
|
13
|
+
end
|
14
|
+
|
15
|
+
# The language name for display.
|
16
|
+
# @return [String, nil]
|
17
|
+
def display_str
|
18
|
+
cocina["value"] || decoded_value
|
19
|
+
end
|
20
|
+
|
21
|
+
# The language code, e.g. an ISO 639 code like "eng" or "spa".
|
22
|
+
# @return [String, nil]
|
23
|
+
def code
|
24
|
+
cocina["code"]
|
25
|
+
end
|
26
|
+
|
27
|
+
# Decoded name of the language based on the code, if present.
|
28
|
+
# @return [String, nil]
|
29
|
+
def decoded_value
|
30
|
+
Vocabularies::SEARCHWORKS_LANGUAGES[code] || (Iso639[code] if iso_639?)
|
31
|
+
end
|
32
|
+
|
33
|
+
# True if the language is recognized by Searchworks.
|
34
|
+
# @see CocinaDisplay::Vocabularies::SEARCHWORKS_LANGUAGES
|
35
|
+
# @return [Boolean]
|
36
|
+
def searchworks_language?
|
37
|
+
Vocabularies::SEARCHWORKS_LANGUAGES.value?(display_str)
|
38
|
+
end
|
39
|
+
|
40
|
+
# True if the language has a code sourced from the ISO 639 vocabulary.
|
41
|
+
# @see https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
|
42
|
+
# @return [Boolean]
|
43
|
+
def iso_639?
|
44
|
+
cocina.dig("source", "code")&.start_with? "iso639"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require_relative "../utils"
|
2
|
+
require_relative "subject_value"
|
3
|
+
|
4
|
+
module CocinaDisplay
|
5
|
+
module Subjects
|
6
|
+
# Base class for subjects in Cocina structured data.
|
7
|
+
class Subject
|
8
|
+
attr_reader :cocina
|
9
|
+
|
10
|
+
# Initialize a Subject object with Cocina structured data.
|
11
|
+
# @param cocina [Hash] The Cocina structured data for the subject.
|
12
|
+
def initialize(cocina)
|
13
|
+
@cocina = cocina
|
14
|
+
end
|
15
|
+
|
16
|
+
# The top-level type of the subject.
|
17
|
+
# @see https://github.com/sul-dlss/cocina-models/blob/main/docs/description_types.md#subject-types
|
18
|
+
# @return [String, nil]
|
19
|
+
def type
|
20
|
+
cocina["type"]
|
21
|
+
end
|
22
|
+
|
23
|
+
# Array of display strings for each value in the subject.
|
24
|
+
# Used for search, where each value should be indexed separately.
|
25
|
+
# @return [Array<String>]
|
26
|
+
def display_values
|
27
|
+
subject_values.map(&:display_str).compact_blank
|
28
|
+
end
|
29
|
+
|
30
|
+
# A string representation of the entire subject, formatted for display.
|
31
|
+
# Concatenates the values with an appropriate delimiter.
|
32
|
+
# @return [String]
|
33
|
+
def display_str
|
34
|
+
Utils.compact_and_join(display_values, delimiter: delimiter)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Individual values composing this subject.
|
38
|
+
# Can be multiple if the Cocina featured nested data.
|
39
|
+
# If no type was specified on a value, uses the top-level subject type.
|
40
|
+
# @return [Array<SubjectValue>]
|
41
|
+
def subject_values
|
42
|
+
@subject_values ||= Utils.flatten_nested_values(cocina, atomic_types: SubjectValue.atomic_types).map do |value|
|
43
|
+
subject_value = SubjectValue.from_cocina(value)
|
44
|
+
subject_value.type ||= type
|
45
|
+
subject_value
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# Delimiter to use for joining structured subject values.
|
52
|
+
# LCSH uses a comma (the default); catalog headings use " > ".
|
53
|
+
# @return [String]
|
54
|
+
def delimiter
|
55
|
+
if cocina["displayLabel"]&.downcase == "catalog heading"
|
56
|
+
" > "
|
57
|
+
else
|
58
|
+
", "
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|