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.
@@ -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 subjects that are topics, formatted as strings for display.
8
+ # All unique subject values that are topics.
8
9
  # @return [Array<String>]
9
10
  def subject_topics
10
- subjects.filter { |s| s.type == "topic" }.map(&:display_str).uniq
11
+ subject_values.filter { |s| s.type == "topic" }.map(&:display_str).uniq
11
12
  end
12
13
 
13
- # All unique subjects that are genres, formatted as strings for display.
14
+ # All unique subject values that are genres.
14
15
  # @return [Array<String>]
15
16
  def subject_genres
16
- subjects.filter { |s| s.type == "genre" }.map(&:display_str).uniq
17
+ subject_values.filter { |s| s.type == "genre" }.map(&:display_str).uniq
17
18
  end
18
19
 
19
- # All unique subjects that are titles, formatted as strings for display.
20
+ # All unique subject values that are titles.
20
21
  # @return [Array<String>]
21
22
  def subject_titles
22
- subjects.filter { |s| s.type == "title" }.map(&:display_str).uniq
23
+ subject_values.filter { |s| s.type == "title" }.map(&:display_str).uniq
23
24
  end
24
25
 
25
- # All unique subjects that are date/time info, formatted as strings for display.
26
+ # All unique subject values that are date/time info.
26
27
  # @return [Array<String>]
27
28
  def subject_temporal
28
- subjects.filter { |s| s.type == "time" }.map(&:display_str).uniq
29
+ subject_values.filter { |s| s.type == "time" }.map(&:display_str).uniq
29
30
  end
30
31
 
31
- # All unique subjects that are occupations, formatted as strings for display.
32
+ # All unique subject values that are occupations.
32
33
  # @return [Array<String>]
33
34
  def subject_occupations
34
- subjects.filter { |s| s.type == "occupation" }.map(&:display_str).uniq
35
+ subject_values.filter { |s| s.type == "occupation" }.map(&:display_str).uniq
35
36
  end
36
37
 
37
- # All unique subjects that are names of entities, formatted as strings for display.
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::NameSubject
40
+ # @see CocinaDisplay::NameSubjectValue
40
41
  # @return [Array<String>]
41
42
  def subject_names
42
- subjects.filter { |s| s.is_a? NameSubject }.map(&:display_str).uniq
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[*].subject[*]")
54
- ).map { |s| Subject.from_cocina(s) }
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? { |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.
@@ -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, terms_of_address_str].compact_blank.presence || Array(name_values["name"])
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
- # Is either date in the range qualified in any way?
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 start&.qualifier == stop&.qualifier
101
- qualifier = start&.qualifier || stop&.qualifier
102
- date = decoded_value
103
- return "[ca. #{date}]" if qualifier == "approximate"
104
- return "[#{date}?]" if qualifier == "questionable"
105
- return "[#{date}]" if qualifier == "inferred"
106
-
107
- date
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