cocina_display 0.3.0 → 0.5.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 +1 -1
- data/lib/cocina_display/cocina_record.rb +17 -142
- data/lib/cocina_display/concerns/access.rb +71 -0
- data/lib/cocina_display/concerns/contributors.rb +94 -0
- data/lib/cocina_display/concerns/events.rb +14 -0
- data/lib/cocina_display/concerns/identifiers.rb +68 -0
- data/lib/cocina_display/concerns/subjects.rb +58 -0
- data/lib/cocina_display/concerns/titles.rb +64 -0
- data/lib/cocina_display/contributor.rb +185 -0
- data/lib/cocina_display/imprint.rb +9 -25
- data/lib/cocina_display/subject.rb +127 -0
- data/lib/cocina_display/title_builder.rb +27 -7
- data/lib/cocina_display/utils.rb +49 -0
- data/lib/cocina_display/version.rb +1 -1
- metadata +12 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5662a1512975323d0324c31021fcef9cd4ff337ea8acfb5f6a3352d4abf0e303
|
4
|
+
data.tar.gz: fe0444d5c104e393718a996260346f98691f2e84585483bb7a2062acab347bfb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8ff64dadfe8b2492f23d38bcc320e8de4bc6100af09adfd93d25df0aeb84c91585072dbcbb6cafc297bd503d773ee7b6bf35281fdda1d573e65f94fcb866a5a0
|
7
|
+
data.tar.gz: 29c42ee65d59dbac907159435f8c9924742cc4d598e656a787dee8cbfceaab8231f2bbf8cb87d14ee180ed586fc51b4d498e8009b6fd5442442d8b0932ebd30d
|
data/README.md
CHANGED
@@ -40,7 +40,7 @@ There is also a helper method to fetch the Cocina JSON for a given DRUID and imm
|
|
40
40
|
The `CocinaRecord` class provides some methods to access common fields, as well as an underlying hash representation parsed from the JSON.
|
41
41
|
|
42
42
|
```ruby
|
43
|
-
> record.
|
43
|
+
> record.main_title
|
44
44
|
=> "Bugatti Type 51A. Road & Track Salon January 1957"
|
45
45
|
> record.content_type
|
46
46
|
=> "image"
|
@@ -7,13 +7,22 @@ require "active_support"
|
|
7
7
|
require "active_support/core_ext/object/blank"
|
8
8
|
require "active_support/core_ext/hash/conversions"
|
9
9
|
|
10
|
-
require_relative "title_builder"
|
11
10
|
require_relative "concerns/events"
|
11
|
+
require_relative "concerns/contributors"
|
12
|
+
require_relative "concerns/identifiers"
|
13
|
+
require_relative "concerns/titles"
|
14
|
+
require_relative "concerns/access"
|
15
|
+
require_relative "concerns/subjects"
|
12
16
|
|
13
17
|
module CocinaDisplay
|
14
18
|
# Public Cocina metadata for an SDR object, as fetched from PURL.
|
15
19
|
class CocinaRecord
|
16
20
|
include CocinaDisplay::Concerns::Events
|
21
|
+
include CocinaDisplay::Concerns::Contributors
|
22
|
+
include CocinaDisplay::Concerns::Identifiers
|
23
|
+
include CocinaDisplay::Concerns::Titles
|
24
|
+
include CocinaDisplay::Concerns::Access
|
25
|
+
include CocinaDisplay::Concerns::Subjects
|
17
26
|
|
18
27
|
# Fetch a public Cocina document from PURL and create a CocinaRecord.
|
19
28
|
# @note This is intended to be used in development or testing only.
|
@@ -45,60 +54,6 @@ module CocinaDisplay
|
|
45
54
|
Janeway.enum_for(path_expression, cocina_doc)
|
46
55
|
end
|
47
56
|
|
48
|
-
# The DRUID for the object, with the +druid:+ prefix.
|
49
|
-
# @return [String]
|
50
|
-
# @example
|
51
|
-
# record.druid #=> "druid:bb099mt5053"
|
52
|
-
def druid
|
53
|
-
cocina_doc["externalIdentifier"]
|
54
|
-
end
|
55
|
-
|
56
|
-
# The DRUID for the object, without the +druid:+ prefix.
|
57
|
-
# @return [String]
|
58
|
-
# @example
|
59
|
-
# record.bare_druid #=> "bb099mt5053"
|
60
|
-
def bare_druid
|
61
|
-
druid.delete_prefix("druid:")
|
62
|
-
end
|
63
|
-
|
64
|
-
# The DOI for the object, if there is one – just the identifier part.
|
65
|
-
# @return [String, nil]
|
66
|
-
# @example
|
67
|
-
# record.doi #=> "10.25740/ppax-bf07"
|
68
|
-
def doi
|
69
|
-
doi_id = path("$.identification.doi").first ||
|
70
|
-
path("$.description.identifier[?match(@.type, 'doi|DOI')].value").first ||
|
71
|
-
path("$.description.identifier[?search(@.uri, 'doi.org')].uri").first
|
72
|
-
|
73
|
-
URI(doi_id).path.delete_prefix("/") if doi_id.present?
|
74
|
-
end
|
75
|
-
|
76
|
-
# The DOI as a URL, if there is one. Any valid DOI should resolve via doi.org.
|
77
|
-
# @return [String, nil]
|
78
|
-
# @example
|
79
|
-
# record.doi_url #=> "https://doi.org/10.25740/ppax-bf07"
|
80
|
-
def doi_url
|
81
|
-
URI.join("https://doi.org", doi).to_s if doi.present?
|
82
|
-
end
|
83
|
-
|
84
|
-
# The HRID of the item in FOLIO, if defined.
|
85
|
-
# @note This doesn't imply the object is available in Searchworks at this ID.
|
86
|
-
# @return [String, nil]
|
87
|
-
# @example
|
88
|
-
# record.folio_hrid #=> "a12845814"
|
89
|
-
def folio_hrid
|
90
|
-
path("$.identification.catalogLinks[?(@.catalog == 'folio')].catalogRecordId").first
|
91
|
-
end
|
92
|
-
|
93
|
-
# The FOLIO HRID if defined, otherwise the bare DRUID.
|
94
|
-
# @note This doesn't imply the object is available in Searchworks at this ID.
|
95
|
-
# @see folio_hrid
|
96
|
-
# @see bare_druid
|
97
|
-
# @return [String]
|
98
|
-
def searchworks_id
|
99
|
-
folio_hrid || bare_druid
|
100
|
-
end
|
101
|
-
|
102
57
|
# Timestamp when the Cocina was created.
|
103
58
|
# @note This is for the metadata itself, not the object.
|
104
59
|
# @return [Time]
|
@@ -122,34 +77,19 @@ module CocinaDisplay
|
|
122
77
|
cocina_doc["type"].split("/").last
|
123
78
|
end
|
124
79
|
|
80
|
+
# Primary processing label for the object.
|
81
|
+
# @note This may or may not be the same as the title.
|
82
|
+
# @return [String, nil]
|
83
|
+
def label
|
84
|
+
cocina_doc["label"]
|
85
|
+
end
|
86
|
+
|
125
87
|
# True if the object is a collection.
|
126
88
|
# @return [Boolean]
|
127
89
|
def collection?
|
128
90
|
content_type == "collection"
|
129
91
|
end
|
130
92
|
|
131
|
-
# The main title for the object.
|
132
|
-
# @note If you need more formatting control, consider using {CocinaDisplay::TitleBuilder} directly.
|
133
|
-
# @return [String]
|
134
|
-
# @example
|
135
|
-
# record.title #=> "Bugatti Type 51A. Road & Track Salon January 1957"
|
136
|
-
def title
|
137
|
-
CocinaDisplay::TitleBuilder.build(
|
138
|
-
cocina_doc.dig("description", "title"),
|
139
|
-
catalog_links: cocina_doc.dig("identification", "catalogLinks")
|
140
|
-
)
|
141
|
-
end
|
142
|
-
|
143
|
-
# Alternative or translated titles for the object. Does not include the main title.
|
144
|
-
# @return [Array<String>]
|
145
|
-
# @example
|
146
|
-
# record.additional_titles #=> ["Alternate title 1", "Alternate title 2"]
|
147
|
-
def additional_titles
|
148
|
-
CocinaDisplay::TitleBuilder.additional_titles(
|
149
|
-
cocina_doc.dig("description", "title")
|
150
|
-
)
|
151
|
-
end
|
152
|
-
|
153
93
|
# Traverse nested FileSets and return an enumerator over their files.
|
154
94
|
# Each file is a +Hash+.
|
155
95
|
# @return [Enumerator] Enumerator over file hashes
|
@@ -161,70 +101,5 @@ module CocinaDisplay
|
|
161
101
|
def files
|
162
102
|
path("$.structural.contains[*].structural.contains[*]")
|
163
103
|
end
|
164
|
-
|
165
|
-
# The PURL URL for this object.
|
166
|
-
# @return [String]
|
167
|
-
# @example
|
168
|
-
# record.purl_url #=> "https://purl.stanford.edu/bx658jh7339"
|
169
|
-
def purl_url
|
170
|
-
cocina_doc.dig("description", "purl") || "https://purl.stanford.edu/#{bare_druid}"
|
171
|
-
end
|
172
|
-
|
173
|
-
# The URL to the PURL environment this object is from.
|
174
|
-
# @note Objects accessed via UAT will still have a production PURL base URL.
|
175
|
-
# @return [String]
|
176
|
-
# @example
|
177
|
-
# record.purl_base_url #=> "https://purl.stanford.edu"
|
178
|
-
def purl_base_url
|
179
|
-
URI(purl_url).origin
|
180
|
-
end
|
181
|
-
|
182
|
-
# The URL to the stacks environment this object is shelved in.
|
183
|
-
# Corresponds to the PURL environment.
|
184
|
-
# @see purl_base_url
|
185
|
-
# @return [String]
|
186
|
-
# @example
|
187
|
-
# record.stacks_base_url #=> "https://stacks.stanford.edu"
|
188
|
-
def stacks_base_url
|
189
|
-
if purl_base_url == "https://sul-purl-stage.stanford.edu"
|
190
|
-
"https://sul-stacks-stage.stanford.edu"
|
191
|
-
else
|
192
|
-
"https://stacks.stanford.edu"
|
193
|
-
end
|
194
|
-
end
|
195
|
-
|
196
|
-
# The oEmbed URL for the object, optionally with additional parameters.
|
197
|
-
# Corresponds to the PURL environment.
|
198
|
-
# @param params [Hash] Additional parameters to include in the oEmbed URL.
|
199
|
-
# @return [String]
|
200
|
-
# @return [nil] if the object is a collection.
|
201
|
-
# @example Generate an oEmbed URL for the viewer and hide the title
|
202
|
-
# record.oembed_url(hide_title: true) #=> "https://purl.stanford.edu/bx658jh7339/embed.json?hide_title=true"
|
203
|
-
def oembed_url(params: {})
|
204
|
-
return if collection?
|
205
|
-
|
206
|
-
params[:url] ||= purl_url
|
207
|
-
"#{purl_base_url}/embed.json?#{params.to_query}"
|
208
|
-
end
|
209
|
-
|
210
|
-
# The download URL to get the entire object as a .zip file.
|
211
|
-
# Stacks generates the .zip for the object on request.
|
212
|
-
# @return [String]
|
213
|
-
# @example
|
214
|
-
# record.download_url #=> "https://stacks.stanford.edu/object/bx658jh7339"
|
215
|
-
def download_url
|
216
|
-
"#{stacks_base_url}/object/#{bare_druid}"
|
217
|
-
end
|
218
|
-
|
219
|
-
# The IIIF manifest URL for the object.
|
220
|
-
# PURL generates the IIIF manifest.
|
221
|
-
# @param version [Integer] The IIIF presentation spec version to use (3 or 2).
|
222
|
-
# @return [String]
|
223
|
-
# @example
|
224
|
-
# record.iiif_manifest_url #=> "https://purl.stanford.edu/bx658jh7339/iiif3/manifest"
|
225
|
-
def iiif_manifest_url(version: 3)
|
226
|
-
iiif_path = (version == 3) ? "iiif3" : "iiif"
|
227
|
-
"#{purl_url}/#{iiif_path}/manifest"
|
228
|
-
end
|
229
104
|
end
|
230
105
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module CocinaDisplay
|
2
|
+
module Concerns
|
3
|
+
# Methods that generate URLs to access an object.
|
4
|
+
module Access
|
5
|
+
# The PURL URL for this object.
|
6
|
+
# @return [String]
|
7
|
+
# @example
|
8
|
+
# record.purl_url #=> "https://purl.stanford.edu/bx658jh7339"
|
9
|
+
def purl_url
|
10
|
+
cocina_doc.dig("description", "purl") || "https://purl.stanford.edu/#{bare_druid}"
|
11
|
+
end
|
12
|
+
|
13
|
+
# The URL to the PURL environment this object is from.
|
14
|
+
# @note Objects accessed via UAT will still have a production PURL base URL.
|
15
|
+
# @return [String]
|
16
|
+
# @example
|
17
|
+
# record.purl_base_url #=> "https://purl.stanford.edu"
|
18
|
+
def purl_base_url
|
19
|
+
URI(purl_url).origin
|
20
|
+
end
|
21
|
+
|
22
|
+
# The URL to the stacks environment this object is shelved in.
|
23
|
+
# Corresponds to the PURL environment.
|
24
|
+
# @see purl_base_url
|
25
|
+
# @return [String]
|
26
|
+
# @example
|
27
|
+
# record.stacks_base_url #=> "https://stacks.stanford.edu"
|
28
|
+
def stacks_base_url
|
29
|
+
if purl_base_url == "https://sul-purl-stage.stanford.edu"
|
30
|
+
"https://sul-stacks-stage.stanford.edu"
|
31
|
+
else
|
32
|
+
"https://stacks.stanford.edu"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# The oEmbed URL for the object, optionally with additional parameters.
|
37
|
+
# Corresponds to the PURL environment.
|
38
|
+
# @param params [Hash] Additional parameters to include in the oEmbed URL.
|
39
|
+
# @return [String]
|
40
|
+
# @return [nil] if the object is a collection.
|
41
|
+
# @example Generate an oEmbed URL for the viewer and hide the title
|
42
|
+
# record.oembed_url(hide_title: true) #=> "https://purl.stanford.edu/bx658jh7339/embed.json?hide_title=true"
|
43
|
+
def oembed_url(params: {})
|
44
|
+
return if collection?
|
45
|
+
|
46
|
+
params[:url] ||= purl_url
|
47
|
+
"#{purl_base_url}/embed.json?#{params.to_query}"
|
48
|
+
end
|
49
|
+
|
50
|
+
# The download URL to get the entire object as a .zip file.
|
51
|
+
# Stacks generates the .zip for the object on request.
|
52
|
+
# @return [String]
|
53
|
+
# @example
|
54
|
+
# record.download_url #=> "https://stacks.stanford.edu/object/bx658jh7339"
|
55
|
+
def download_url
|
56
|
+
"#{stacks_base_url}/object/#{bare_druid}"
|
57
|
+
end
|
58
|
+
|
59
|
+
# The IIIF manifest URL for the object.
|
60
|
+
# PURL generates the IIIF manifest.
|
61
|
+
# @param version [Integer] The IIIF presentation spec version to use (3 or 2).
|
62
|
+
# @return [String]
|
63
|
+
# @example
|
64
|
+
# record.iiif_manifest_url #=> "https://purl.stanford.edu/bx658jh7339/iiif3/manifest"
|
65
|
+
def iiif_manifest_url(version: 3)
|
66
|
+
iiif_path = (version == 3) ? "iiif3" : "iiif"
|
67
|
+
"#{purl_url}/#{iiif_path}/manifest"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require_relative "../contributor"
|
2
|
+
|
3
|
+
module CocinaDisplay
|
4
|
+
module Concerns
|
5
|
+
# Methods for finding and formatting names for contributors
|
6
|
+
module Contributors
|
7
|
+
# The main author's name, formatted for display.
|
8
|
+
# @param with_date [Boolean] Include life dates, if present
|
9
|
+
# @return [String]
|
10
|
+
# @return [nil] if no main author is found
|
11
|
+
# @example
|
12
|
+
# record.main_author #=> "Smith, John"
|
13
|
+
# @example with date
|
14
|
+
# record.main_author(with_date: true) #=> "Smith, John, 1970-2020"
|
15
|
+
def main_author(with_date: false)
|
16
|
+
main_author_contributor&.display_name(with_date: with_date)
|
17
|
+
end
|
18
|
+
|
19
|
+
# All author names except the main one, formatted for display.
|
20
|
+
# @param with_date [Boolean] Include life dates, if present
|
21
|
+
# @return [Array<String>]
|
22
|
+
def additional_authors(with_date: false)
|
23
|
+
additional_author_contributors.map { |c| c.display_name(with_date: with_date) }
|
24
|
+
end
|
25
|
+
|
26
|
+
# All names of authors who are people, formatted for display.
|
27
|
+
# @param with_date [Boolean] Include life dates, if present
|
28
|
+
# @return [Array<String>]
|
29
|
+
def person_authors(with_date: false)
|
30
|
+
authors.filter(&:person?).map { |c| c.display_name(with_date: with_date) }
|
31
|
+
end
|
32
|
+
|
33
|
+
# All names of non-person authors, formatted for display.
|
34
|
+
# This includes organizations, conferences, families, etc.
|
35
|
+
# @return [Array<String>]
|
36
|
+
# @see https://github.com/sul-dlss/cocina-models/blob/main/docs/description_types.md#contributor-types
|
37
|
+
def impersonal_authors
|
38
|
+
authors.reject(&:person?).map(&:display_name)
|
39
|
+
end
|
40
|
+
|
41
|
+
# All names of authors that are organizations, formatted for display.
|
42
|
+
# @return [Array<String>]
|
43
|
+
def organization_authors
|
44
|
+
authors.filter(&:organization?).map(&:display_name)
|
45
|
+
end
|
46
|
+
|
47
|
+
# All names of authors that are conferences, formatted for display.
|
48
|
+
# @return [Array<String>]
|
49
|
+
def conference_authors
|
50
|
+
authors.filter(&:conference?).map(&:display_name)
|
51
|
+
end
|
52
|
+
|
53
|
+
# A string value for sorting by author that sorts missing values last.
|
54
|
+
# Ignores punctuation and leading/trailing spaces.
|
55
|
+
# @return [String]
|
56
|
+
def sort_author
|
57
|
+
(main_author_contributor&.display_name || "\u{10FFFF}").gsub(/[[:punct:]]*/, "").strip
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# All contributors for the object, including authors, editors, etc.
|
63
|
+
# @return [Array<Contributor>]
|
64
|
+
def contributors
|
65
|
+
@contributors ||= path("$.description.contributor[*]").map { |c| Contributor.new(c) }
|
66
|
+
end
|
67
|
+
|
68
|
+
# All contributors with a "creator" or "author" role.
|
69
|
+
# @return [Array<Contributor>]
|
70
|
+
# @see Contributor#author?
|
71
|
+
def authors
|
72
|
+
contributors.filter(&:author?)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Contributor object representing the primary author.
|
76
|
+
# Selected according to the following rules:
|
77
|
+
# 1. If there is a primary author or creator, use that.
|
78
|
+
# 2. If there are no primary authors or creators, use the first one.
|
79
|
+
# 3. If there are none at all, use the first contributor without any role.
|
80
|
+
# @return [Contributor]
|
81
|
+
# @return [nil] if no suitable contributor is found
|
82
|
+
def main_author_contributor
|
83
|
+
authors.find(&:primary?).presence || authors.first || contributors.find { |c| !c.role? }.presence
|
84
|
+
end
|
85
|
+
|
86
|
+
# All author/creator contributors except the main one.
|
87
|
+
# @return [Array<Contributor>]
|
88
|
+
def additional_author_contributors
|
89
|
+
return [] if authors.empty? || authors.one? || !authors.include?(main_author_contributor)
|
90
|
+
authors - [main_author_contributor]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -41,6 +41,20 @@ module CocinaDisplay
|
|
41
41
|
pub_date_edtf(ignore_qualified: ignore_qualified)&.year
|
42
42
|
end
|
43
43
|
|
44
|
+
# The range of preferred publication years as an array of integers.
|
45
|
+
# Considers publication, creation, and capture dates in that order.
|
46
|
+
# Prefers dates marked as primary and those with a declared encoding.
|
47
|
+
# @param ignore_qualified [Boolean] Reject qualified dates (e.g. approximate)
|
48
|
+
# @return [Array<Integer>, nil]
|
49
|
+
# @note 6 BCE will appear as -5; 4 CE will appear as 4.
|
50
|
+
def pub_year_int_range(ignore_qualified: false)
|
51
|
+
date = pub_date(ignore_qualified: ignore_qualified)
|
52
|
+
return unless date
|
53
|
+
|
54
|
+
date = date.as_interval if date.is_a? CocinaDisplay::Dates::DateRange
|
55
|
+
date.to_a.map(&:year).compact.uniq.sort
|
56
|
+
end
|
57
|
+
|
44
58
|
# String for displaying the earliest preferred publication year or range.
|
45
59
|
# Considers publication, creation, and capture dates in that order.
|
46
60
|
# Prefers dates marked as primary and those with a declared encoding.
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module CocinaDisplay
|
2
|
+
module Concerns
|
3
|
+
# Methods for extracting and formatting identifiers from Cocina records.
|
4
|
+
module Identifiers
|
5
|
+
# The DRUID for the object, with the +druid:+ prefix.
|
6
|
+
# @return [String]
|
7
|
+
# @example
|
8
|
+
# record.druid #=> "druid:bb099mt5053"
|
9
|
+
def druid
|
10
|
+
cocina_doc["externalIdentifier"]
|
11
|
+
end
|
12
|
+
|
13
|
+
# The DRUID for the object, without the +druid:+ prefix.
|
14
|
+
# @return [String]
|
15
|
+
# @example
|
16
|
+
# record.bare_druid #=> "bb099mt5053"
|
17
|
+
def bare_druid
|
18
|
+
druid.delete_prefix("druid:")
|
19
|
+
end
|
20
|
+
|
21
|
+
# The DOI for the object, if there is one – just the identifier part.
|
22
|
+
# @return [String, nil]
|
23
|
+
# @example
|
24
|
+
# record.doi #=> "10.25740/ppax-bf07"
|
25
|
+
def doi
|
26
|
+
doi_id = path("$.identification.doi").first ||
|
27
|
+
path("$.description.identifier[?match(@.type, 'doi|DOI')].value").first ||
|
28
|
+
path("$.description.identifier[?search(@.uri, 'doi.org')].uri").first
|
29
|
+
|
30
|
+
URI(doi_id).path.delete_prefix("/") if doi_id.present?
|
31
|
+
end
|
32
|
+
|
33
|
+
# The DOI as a URL, if there is one. Any valid DOI should resolve via doi.org.
|
34
|
+
# @return [String, nil]
|
35
|
+
# @example
|
36
|
+
# record.doi_url #=> "https://doi.org/10.25740/ppax-bf07"
|
37
|
+
def doi_url
|
38
|
+
URI.join("https://doi.org", doi).to_s if doi.present?
|
39
|
+
end
|
40
|
+
|
41
|
+
# The HRID of the item in FOLIO, if defined.
|
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.
|
44
|
+
# @return [String, nil]
|
45
|
+
# @example With a link regardless of refresh:
|
46
|
+
# record.folio_hrid #=> "a12845814"
|
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
|
56
|
+
end
|
57
|
+
|
58
|
+
# The FOLIO HRID if defined, otherwise the bare DRUID.
|
59
|
+
# @note This doesn't imply the object is available in Searchworks at this ID.
|
60
|
+
# @see folio_hrid
|
61
|
+
# @see bare_druid
|
62
|
+
# @return [String]
|
63
|
+
def searchworks_id
|
64
|
+
folio_hrid || bare_druid
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,58 @@
|
|
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
|
+
private
|
46
|
+
|
47
|
+
# All subjects, accessible as Subject objects.
|
48
|
+
# Checks both description.subject and description.geographic.subject.
|
49
|
+
# @return [Array<Subject>]
|
50
|
+
def subjects
|
51
|
+
@subjects ||= Enumerator::Chain.new(
|
52
|
+
path("$.description.subject[*]"),
|
53
|
+
path("$.description.geographic[*].subject[*]")
|
54
|
+
).map { |s| Subject.from_cocina(s) }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require_relative "../title_builder"
|
2
|
+
|
3
|
+
module CocinaDisplay
|
4
|
+
module Concerns
|
5
|
+
# Methods for finding and formatting titles.
|
6
|
+
module Titles
|
7
|
+
# The main title for the object, without subtitle, part name, etc.
|
8
|
+
# If there are multiple titles, uses the first.
|
9
|
+
# @see CocinaDisplay::TitleBuilder#main_title
|
10
|
+
# @note This corresponds to the "short title" in MODS XML, or MARC 245$a only.
|
11
|
+
# @return [String]
|
12
|
+
def main_title
|
13
|
+
CocinaDisplay::TitleBuilder.main_title(cocina_titles).first
|
14
|
+
end
|
15
|
+
|
16
|
+
# The full title for the object, including subtitle, part name, etc.
|
17
|
+
# If there are multiple titles, uses the first.
|
18
|
+
# @see CocinaDisplay::TitleBuilder#full_title
|
19
|
+
# @note This corresponds to the entire MARC 245 field.
|
20
|
+
# @return [String]
|
21
|
+
def full_title
|
22
|
+
CocinaDisplay::TitleBuilder.full_title(cocina_titles, catalog_links: catalog_links).first
|
23
|
+
end
|
24
|
+
|
25
|
+
# The full title, joined together with additional punctuation.
|
26
|
+
# If there are multiple titles, uses the first.
|
27
|
+
# @see CocinaDisplay::TitleBuilder#build
|
28
|
+
# @return [String]
|
29
|
+
def display_title
|
30
|
+
CocinaDisplay::TitleBuilder.build(cocina_titles, catalog_links: catalog_links)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Any additional titles for the object excluding the main title.
|
34
|
+
# @return [Array<String>]
|
35
|
+
# @see CocinaDisplay::TitleBuilder#additional_titles
|
36
|
+
def additional_titles
|
37
|
+
CocinaDisplay::TitleBuilder.additional_titles(cocina_titles)
|
38
|
+
end
|
39
|
+
|
40
|
+
# A string value for sorting by title that sorts missing values last.
|
41
|
+
# Ignores punctuation, leading/trailing spaces, and non-sorting characters.
|
42
|
+
# @see CocinaDisplay::TitleBuilder#sort_title
|
43
|
+
# @return [String]
|
44
|
+
def sort_title
|
45
|
+
CocinaDisplay::TitleBuilder.sort_title(cocina_titles, catalog_links: catalog_links).first || "\u{10FFFF}"
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# The titles from the Cocina document, as an array of hashes.
|
51
|
+
# @return [Array<Hash>]
|
52
|
+
def cocina_titles
|
53
|
+
@cocina_titles ||= Array(cocina_doc.dig("description", "title"))
|
54
|
+
end
|
55
|
+
|
56
|
+
# The catalog links from the Cocina document, as an array of hashes.
|
57
|
+
# These link to FOLIO and can include part labels used to construct titles.
|
58
|
+
# @return [Array<Hash>]
|
59
|
+
def catalog_links
|
60
|
+
@catalog_links ||= Array(cocina_doc.dig("identification", "catalogLinks"))
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support"
|
4
|
+
require "active_support/core_ext/object/blank"
|
5
|
+
require "active_support/core_ext/array/conversions"
|
6
|
+
|
7
|
+
require_relative "utils"
|
8
|
+
|
9
|
+
module CocinaDisplay
|
10
|
+
# A contributor to a work, such as an author or publisher.
|
11
|
+
class Contributor
|
12
|
+
attr_reader :cocina
|
13
|
+
|
14
|
+
# Initialize a Contributor object with Cocina structured data.
|
15
|
+
# @param cocina [Hash] The Cocina structured data for the contributor.
|
16
|
+
def initialize(cocina)
|
17
|
+
@cocina = cocina
|
18
|
+
end
|
19
|
+
|
20
|
+
# String representation of the contributor, including name and role.
|
21
|
+
# Used for debugging and logging.
|
22
|
+
# @return [String]
|
23
|
+
def to_s
|
24
|
+
Utils.compact_and_join([display_name, display_role], delimiter: ": ")
|
25
|
+
end
|
26
|
+
|
27
|
+
# Is this contributor a human?
|
28
|
+
# @return [Boolean]
|
29
|
+
def person?
|
30
|
+
cocina["type"] == "person"
|
31
|
+
end
|
32
|
+
|
33
|
+
# Is this contributor an organization?
|
34
|
+
# @return [Boolean]
|
35
|
+
def organization?
|
36
|
+
cocina["type"] == "organization"
|
37
|
+
end
|
38
|
+
|
39
|
+
# Is this contributor a conference?
|
40
|
+
# @return [Boolean]
|
41
|
+
def conference?
|
42
|
+
cocina["type"] == "conference"
|
43
|
+
end
|
44
|
+
|
45
|
+
# Is this contributor marked as primary?
|
46
|
+
# @return [Boolean]
|
47
|
+
def primary?
|
48
|
+
cocina["status"] == "primary"
|
49
|
+
end
|
50
|
+
|
51
|
+
# Does this contributor have a role that indicates they are an author?
|
52
|
+
# @return [Boolean]
|
53
|
+
def author?
|
54
|
+
roles.any? { |role| role["value"] =~ /(author|creator)/i }
|
55
|
+
end
|
56
|
+
|
57
|
+
# Does this contributor have any roles defined?
|
58
|
+
# @return [Boolean]
|
59
|
+
def role?
|
60
|
+
roles.any?
|
61
|
+
end
|
62
|
+
|
63
|
+
# The display name for the contributor as a string.
|
64
|
+
# Uses the first name if multiple names are present.
|
65
|
+
# @param with_date [Boolean] Include life dates, if present
|
66
|
+
# @return [String]
|
67
|
+
def display_name(with_date: false)
|
68
|
+
names.map { |name| name.display_str(with_date: with_date) }.first
|
69
|
+
end
|
70
|
+
|
71
|
+
# A string representation of the contributor's roles, formatted for display.
|
72
|
+
# If there are multiple roles, they are joined with commas.
|
73
|
+
# @return [String]
|
74
|
+
def display_role
|
75
|
+
roles.map { |role| role["value"] }.to_sentence
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
# All names in the Cocina as Name objects.
|
81
|
+
# @return [Array<Name>]
|
82
|
+
def names
|
83
|
+
@names ||= Array(cocina["name"]).map { |name| Name.new(name) }
|
84
|
+
end
|
85
|
+
|
86
|
+
# All roles in the Cocina structured data.
|
87
|
+
# @return [Array<Hash>]
|
88
|
+
def roles
|
89
|
+
Array(cocina["role"])
|
90
|
+
end
|
91
|
+
|
92
|
+
# A name associated with a contributor.
|
93
|
+
class Name
|
94
|
+
attr_reader :cocina
|
95
|
+
|
96
|
+
# Initialize a Name object with Cocina structured data.
|
97
|
+
# @param cocina [Hash] The Cocina structured data for the name.
|
98
|
+
def initialize(cocina)
|
99
|
+
@cocina = cocina
|
100
|
+
end
|
101
|
+
|
102
|
+
# The display string for the name, optionally including life dates.
|
103
|
+
# Uses these values in order, if present:
|
104
|
+
# 1. Unstructured value
|
105
|
+
# 2. Any structured/parallel values marked as "display"
|
106
|
+
# 3. Joined structured values, optionally with life dates
|
107
|
+
# @param with_date [Boolean] Include life dates, if present
|
108
|
+
# @return [String]
|
109
|
+
# @example no dates
|
110
|
+
# name.display_name # => "King, Martin Luther, Jr."
|
111
|
+
# @example with dates
|
112
|
+
# name.display_name(with_date: true) # => "King, Martin Luther, Jr., 1929-1968"
|
113
|
+
def display_str(with_date: false)
|
114
|
+
if cocina["value"].present?
|
115
|
+
cocina["value"]
|
116
|
+
elsif display_name_str.present?
|
117
|
+
display_name_str
|
118
|
+
elsif dates_str.present? && with_date
|
119
|
+
Utils.compact_and_join([full_name_str, dates_str], delimiter: ", ")
|
120
|
+
else
|
121
|
+
full_name_str
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
# The full name as a string, combining all name components.
|
128
|
+
# @return [String]
|
129
|
+
def full_name_str
|
130
|
+
Utils.compact_and_join(name_components, delimiter: ", ")
|
131
|
+
end
|
132
|
+
|
133
|
+
# Flattened form of any names explicitly marked as "display name".
|
134
|
+
# @return [String]
|
135
|
+
def display_name_str
|
136
|
+
Utils.compact_and_join(Array(name_values["display"]), delimiter: ", ")
|
137
|
+
end
|
138
|
+
|
139
|
+
# List of all name components.
|
140
|
+
# If any of forename, surname, or term of address are present, those are used.
|
141
|
+
# Otherwise, fall back to any names explicitly marked as "name" or untyped.
|
142
|
+
# @return [Array<String>]
|
143
|
+
def name_components
|
144
|
+
[surname_str, forename_ordinal_str, terms_of_address_str].compact_blank.presence || Array(name_values["name"])
|
145
|
+
end
|
146
|
+
|
147
|
+
# Flatten all forenames and ordinals into a single string.
|
148
|
+
# @return [String]
|
149
|
+
def forename_ordinal_str
|
150
|
+
Utils.compact_and_join(Array(name_values["forename"]) + Array(name_values["ordinal"]), delimiter: " ")
|
151
|
+
end
|
152
|
+
|
153
|
+
# Flatten all terms of address into a single string.
|
154
|
+
# @return [String]
|
155
|
+
def terms_of_address_str
|
156
|
+
Utils.compact_and_join(Array(name_values["term of address"]), delimiter: ", ")
|
157
|
+
end
|
158
|
+
|
159
|
+
# Flatten all surnames into a single string.
|
160
|
+
# @return [String]
|
161
|
+
def surname_str
|
162
|
+
Utils.compact_and_join(Array(name_values["surname"]), delimiter: " ")
|
163
|
+
end
|
164
|
+
|
165
|
+
# Flatten all life and activity dates into a single string.
|
166
|
+
# @return [String]
|
167
|
+
def dates_str
|
168
|
+
Utils.compact_and_join(Array(name_values["life dates"]) + Array(name_values["activity dates"]), delimiter: ", ")
|
169
|
+
end
|
170
|
+
|
171
|
+
# A hash mapping destructured name types to their values.
|
172
|
+
# Name values with no type are grouped under "name".
|
173
|
+
# @return [Hash<String, Array<String>>]
|
174
|
+
# @see https://github.com/sul-dlss/cocina-models/blob/main/docs/description_types.md#contributor-name-part-types-for-structured-value
|
175
|
+
# @note Currently we do nothing with "alternative", "inverted full name", "pseudonym", and "transliteration" types.
|
176
|
+
def name_values
|
177
|
+
Utils.flatten_nested_values(cocina).each_with_object({}) do |node, hash|
|
178
|
+
type = node["type"] || "name"
|
179
|
+
hash[type] ||= []
|
180
|
+
hash[type] << node["value"]
|
181
|
+
end.compact_blank
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -5,9 +5,10 @@ require "active_support"
|
|
5
5
|
require "active_support/core_ext/enumerable"
|
6
6
|
require "active_support/core_ext/object/blank"
|
7
7
|
|
8
|
+
require_relative "utils"
|
9
|
+
require_relative "marc_country_codes"
|
8
10
|
require_relative "dates/date"
|
9
11
|
require_relative "dates/date_range"
|
10
|
-
require_relative "marc_country_codes"
|
11
12
|
|
12
13
|
module CocinaDisplay
|
13
14
|
# Wrapper for Cocina events used to generate an imprint statement for display.
|
@@ -20,23 +21,6 @@ module CocinaDisplay
|
|
20
21
|
cocina_dates.map { |cd| CocinaDisplay::Dates::Date.from_cocina(cd) }.filter(&:parsable?).compact
|
21
22
|
end
|
22
23
|
|
23
|
-
# Join non-empty values into a string using provided delimiter.
|
24
|
-
# If values already end in delimiter (ignoring whitespace), join with a space instead.
|
25
|
-
# @param values [Array<String>] The values to compact and join
|
26
|
-
# @param delimiter [String] The delimiter to use for joining, default is space
|
27
|
-
def self.compact_and_join(values, delimiter: " ")
|
28
|
-
compacted_values = values.compact_blank.map(&:strip)
|
29
|
-
return compacted_values.first if compacted_values.one?
|
30
|
-
|
31
|
-
compacted_values.reduce(+"") do |result, value|
|
32
|
-
result << if value.end_with?(delimiter.strip)
|
33
|
-
value + " "
|
34
|
-
else
|
35
|
-
value + delimiter
|
36
|
-
end
|
37
|
-
end.delete_suffix(delimiter)
|
38
|
-
end
|
39
|
-
|
40
24
|
attr_reader :cocina, :dates
|
41
25
|
|
42
26
|
# Initialize the imprint with Cocina event data.
|
@@ -49,9 +33,9 @@ module CocinaDisplay
|
|
49
33
|
# The entire imprint statement formatted as a string for display.
|
50
34
|
# @return [String]
|
51
35
|
def display_str
|
52
|
-
place_pub =
|
53
|
-
edition_place_pub =
|
54
|
-
|
36
|
+
place_pub = Utils.compact_and_join([place_str, publisher_str], delimiter: " : ")
|
37
|
+
edition_place_pub = Utils.compact_and_join([edition_str, place_pub], delimiter: " - ")
|
38
|
+
Utils.compact_and_join([edition_place_pub, date_str], delimiter: ", ")
|
55
39
|
end
|
56
40
|
|
57
41
|
# Were any of the dates encoded?
|
@@ -65,25 +49,25 @@ module CocinaDisplay
|
|
65
49
|
# The date portion of the imprint statement, comprising all unique dates.
|
66
50
|
# @return [String]
|
67
51
|
def date_str
|
68
|
-
|
52
|
+
Utils.compact_and_join(unique_dates_for_display.map(&:qualified_value))
|
69
53
|
end
|
70
54
|
|
71
55
|
# The editions portion of the imprint statement, combining all edition notes.
|
72
56
|
# @return [String]
|
73
57
|
def edition_str
|
74
|
-
|
58
|
+
Utils.compact_and_join(Janeway.enum_for("$.note[?@.type == 'edition'].value", cocina))
|
75
59
|
end
|
76
60
|
|
77
61
|
# The place of publication, combining all location values.
|
78
62
|
# @return [String]
|
79
63
|
def place_str
|
80
|
-
|
64
|
+
Utils.compact_and_join(locations_for_display, delimiter: " : ")
|
81
65
|
end
|
82
66
|
|
83
67
|
# The publisher information, combining all name values for publishers.
|
84
68
|
# @return [String]
|
85
69
|
def publisher_str
|
86
|
-
|
70
|
+
Utils.compact_and_join(Janeway.enum_for("$.contributor[?@.role[?@.value == 'publisher']].name[*].value", cocina), delimiter: " : ")
|
87
71
|
end
|
88
72
|
|
89
73
|
# Get the place name for a location, decoding from MARC if necessary.
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require_relative "utils"
|
2
|
+
require_relative "contributor"
|
3
|
+
require_relative "title_builder"
|
4
|
+
require_relative "dates/date"
|
5
|
+
|
6
|
+
module CocinaDisplay
|
7
|
+
# Base class for subjects in Cocina structured data.
|
8
|
+
class Subject
|
9
|
+
attr_reader :cocina
|
10
|
+
|
11
|
+
# Extract the type of the subject from the Cocina structured data.
|
12
|
+
# If no top-level type, uses the first structuredValue type.
|
13
|
+
# @param cocina [Hash] The Cocina structured data for the subject.
|
14
|
+
# @return [String, nil] The type of the subject, or nil if none
|
15
|
+
# @see https://github.com/sul-dlss/cocina-models/blob/main/docs/description_types.md#subject-types
|
16
|
+
def self.detect_type(cocina)
|
17
|
+
cocina["type"] || Utils.flatten_nested_values(cocina).pick("type")
|
18
|
+
end
|
19
|
+
|
20
|
+
# Choose and create the appropriate Subject subclass based on type.
|
21
|
+
# @param cocina [Hash] The Cocina structured data for the subject.
|
22
|
+
# @return [Subject]
|
23
|
+
# @see detect_type
|
24
|
+
def self.from_cocina(cocina)
|
25
|
+
case detect_type(cocina)
|
26
|
+
when "person", "family", "organization", "conference", "event", "name"
|
27
|
+
NameSubject.new(cocina)
|
28
|
+
when "title"
|
29
|
+
TitleSubject.new(cocina)
|
30
|
+
when "time"
|
31
|
+
TemporalSubject.new(cocina)
|
32
|
+
# TODO: special handling for geospatial subjects
|
33
|
+
# when "map coordinates", "bounding box coordinates", "point coordinates"
|
34
|
+
else
|
35
|
+
Subject.new(cocina)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Initialize a Subject object with Cocina structured data.
|
40
|
+
# @param cocina [Hash] The Cocina structured data for the subject.
|
41
|
+
def initialize(cocina)
|
42
|
+
@cocina = cocina
|
43
|
+
end
|
44
|
+
|
45
|
+
# The type of the subject.
|
46
|
+
# If no top-level type, uses the first structuredValue type.
|
47
|
+
# @return [String, nil] The type of the subject, or nil if none
|
48
|
+
# @see detect_type
|
49
|
+
def type
|
50
|
+
self.class.detect_type(cocina)
|
51
|
+
end
|
52
|
+
|
53
|
+
# A string representation of the subject, formatted for display.
|
54
|
+
# Concatenates any structured values with an appropriate delimiter.
|
55
|
+
# Subclasses may override this for more specific formatting.
|
56
|
+
# @return [String]
|
57
|
+
def display_str
|
58
|
+
Utils.compact_and_join(descriptive_values, delimiter: delimiter)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Flatten any structured values into an array of Hashes with "value" keys.
|
64
|
+
# If no structured values, will return the top-level cocina data.
|
65
|
+
# @see Utils.flatten_nested_values
|
66
|
+
# @return [Array<Hash>] An array of Hashes representing all values.
|
67
|
+
def descriptive_values
|
68
|
+
Utils.flatten_nested_values(cocina).pluck("value")
|
69
|
+
end
|
70
|
+
|
71
|
+
# Delimiter to use for joining structured subject values.
|
72
|
+
# LCSH uses a comma (the default); catalog headings use " > ".
|
73
|
+
# @return [String]
|
74
|
+
def delimiter
|
75
|
+
if cocina["displayLabel"]&.downcase == "catalog heading"
|
76
|
+
" > "
|
77
|
+
else
|
78
|
+
", "
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# A subject representing a named entity.
|
84
|
+
class NameSubject < Subject
|
85
|
+
attr_reader :name
|
86
|
+
|
87
|
+
# Initialize a NameSubject object with Cocina structured data.
|
88
|
+
# @param cocina [Hash] The Cocina structured data for the subject.
|
89
|
+
def initialize(cocina)
|
90
|
+
super
|
91
|
+
@name = Contributor::Name.new(cocina)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Use the contributor name formatting rules for display.
|
95
|
+
# @return [String] The formatted name string, including life dates
|
96
|
+
# @see CocinaDisplay::Contributor::Name#display_str
|
97
|
+
def display_str
|
98
|
+
@name.display_str(with_date: true)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# A subject representing an entity with a title.
|
103
|
+
class TitleSubject < Subject
|
104
|
+
# Construct a title string to use for display.
|
105
|
+
# @see CocinaDisplay::TitleBuilder.build
|
106
|
+
# @note Unclear how often structured title subjects occur "in the wild".
|
107
|
+
# @return [String]
|
108
|
+
def display_str
|
109
|
+
TitleBuilder.build([cocina])
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# A subject representing a date and/or time.
|
114
|
+
class TemporalSubject < Subject
|
115
|
+
attr_reader :date
|
116
|
+
|
117
|
+
def initialize(cocina)
|
118
|
+
super
|
119
|
+
@date = Dates::Date.from_cocina(cocina)
|
120
|
+
end
|
121
|
+
|
122
|
+
# @return [String] The formatted date/time string for display
|
123
|
+
def display_str
|
124
|
+
@date.qualified_value
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -46,6 +46,16 @@ module CocinaDisplay
|
|
46
46
|
[new(strategy: :all, add_punctuation: false).build(titles)].flatten - full_title(titles)
|
47
47
|
end
|
48
48
|
|
49
|
+
# Like the full title, but with any non-sorting characters and punctuation removed.
|
50
|
+
# @param titles [Array<Hash>] The titles to consider.
|
51
|
+
# @param catalog_links [Array<Hash>] The folio catalog links to check for digital serials part labels.
|
52
|
+
# @return [Array<String>] The sort title value(s) for Solr - array due to possible parallelValue
|
53
|
+
def self.sort_title(titles, catalog_links: [])
|
54
|
+
part_label = catalog_links.find { |link| link["catalog"] == "folio" }&.fetch("partLabel", nil)
|
55
|
+
[new(strategy: :first, add_punctuation: false, only_one_parallel_value: false, part_label: part_label, sortable: true).build(titles)]
|
56
|
+
.flatten.compact.map { |title| title.gsub(/[[:punct:]]*/, "").strip }
|
57
|
+
end
|
58
|
+
|
49
59
|
# @param strategy [Symbol] ":first" selects a single title value based on precedence of
|
50
60
|
# primary, untyped, first occurrence. ":all" returns an array containing all the values.
|
51
61
|
# @param add_punctuation [boolean] whether the title should be formatted with punctuation (think of a structured
|
@@ -54,11 +64,13 @@ module CocinaDisplay
|
|
54
64
|
# of primary, untyped, first occurrence. When false, return an array containing all the parallel values.
|
55
65
|
# Why? Think of e.g. title displayed in blacklight search results vs boosting values for ranking of search results
|
56
66
|
# @param part_label [String] the partLabel to add for digital serials display
|
57
|
-
|
67
|
+
# @param sortable [boolean] whether the title is intended for sorting, and should have non-sorting parts removed
|
68
|
+
def initialize(strategy:, add_punctuation:, only_one_parallel_value: true, part_label: nil, sortable: false)
|
58
69
|
@strategy = strategy
|
59
70
|
@add_punctuation = add_punctuation
|
60
71
|
@only_one_parallel_value = only_one_parallel_value
|
61
72
|
@part_label = part_label
|
73
|
+
@sortable = sortable
|
62
74
|
end
|
63
75
|
|
64
76
|
# @param [Array<Hash>] cocina_titles the titles to consider
|
@@ -161,6 +173,10 @@ module CocinaDisplay
|
|
161
173
|
@only_one_parallel_value
|
162
174
|
end
|
163
175
|
|
176
|
+
def sortable?
|
177
|
+
@sortable
|
178
|
+
end
|
179
|
+
|
164
180
|
# @return [Hash, nil] title that has status=primary
|
165
181
|
def primary_title(cocina_titles)
|
166
182
|
primary_title = cocina_titles.find { |title| title["status"] == "primary" }
|
@@ -221,7 +237,7 @@ module CocinaDisplay
|
|
221
237
|
# rubocop:disable Metrics/CyclomaticComplexity
|
222
238
|
# rubocop:disable Metrics/MethodLength
|
223
239
|
# rubocop:disable Metrics/PerceivedComplexity
|
224
|
-
def rebuild_structured_value(cocina_title)
|
240
|
+
def rebuild_structured_value(cocina_title, sortable: false)
|
225
241
|
result = ""
|
226
242
|
part_name_number = ""
|
227
243
|
cocina_title["structuredValue"].each do |structured_value| # rubocop:disable Metrics/BlockLength
|
@@ -235,8 +251,10 @@ module CocinaDisplay
|
|
235
251
|
# additional types ignored here, e.g. name, uniform ...
|
236
252
|
case structured_value["type"]&.downcase
|
237
253
|
when "nonsorting characters"
|
238
|
-
|
239
|
-
|
254
|
+
unless sortable?
|
255
|
+
padding = non_sorting_padding(cocina_title, value)
|
256
|
+
result = add_non_sorting_value(result, value, padding)
|
257
|
+
end
|
240
258
|
when "part name", "part number"
|
241
259
|
# even if there is a partLabel, use any existing structuredValue
|
242
260
|
# part name/number that remains for non-digital serials purposes
|
@@ -286,7 +304,7 @@ module CocinaDisplay
|
|
286
304
|
# rubocop:disable Metrics/PerceivedComplexity
|
287
305
|
# rubocop:disable Metrics/AbcSize
|
288
306
|
# rubocop:disable Metrics/CyclomaticComplexity
|
289
|
-
def main_title_from_structured_values(cocina_title)
|
307
|
+
def main_title_from_structured_values(cocina_title, sortable: false)
|
290
308
|
result = ""
|
291
309
|
# combine pieces of the cocina structuredValue into a single title
|
292
310
|
cocina_title["structuredValue"].each do |structured_value|
|
@@ -299,8 +317,10 @@ module CocinaDisplay
|
|
299
317
|
|
300
318
|
case structured_value["type"]&.downcase
|
301
319
|
when "nonsorting characters"
|
302
|
-
|
303
|
-
|
320
|
+
unless sortable?
|
321
|
+
padding = non_sorting_padding(cocina_title, value)
|
322
|
+
result = add_non_sorting_value(result, value, padding)
|
323
|
+
end
|
304
324
|
when "main title", "title"
|
305
325
|
result = if ["'", "-"].include?(result.last)
|
306
326
|
[result, value].join
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module CocinaDisplay
|
2
|
+
# Helper methods for string formatting, etc.
|
3
|
+
module Utils
|
4
|
+
# Join non-empty values into a string using provided delimiter.
|
5
|
+
# If values already end in delimiter (ignoring whitespace), join with a space instead.
|
6
|
+
# @param values [Array<String>] The values to compact and join
|
7
|
+
# @param delimiter [String] The delimiter to use for joining, default is space
|
8
|
+
# @return [String] The compacted and joined string
|
9
|
+
def self.compact_and_join(values, delimiter: " ")
|
10
|
+
compacted_values = values.compact_blank.map(&:strip)
|
11
|
+
return compacted_values.first if compacted_values.one?
|
12
|
+
|
13
|
+
compacted_values.reduce(+"") do |result, value|
|
14
|
+
result << if value.end_with?(delimiter.strip)
|
15
|
+
value + " "
|
16
|
+
else
|
17
|
+
value + delimiter
|
18
|
+
end
|
19
|
+
end.delete_suffix(delimiter)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Recursively flatten structured and parallel values in Cocina metadata.
|
23
|
+
# Returns a list of hashes representing the "leaf" nodes with +value+ key.
|
24
|
+
# @return [Array<Hash>] List of node hashes with "value" present
|
25
|
+
# @param cocina [Hash] The Cocina structured data to flatten
|
26
|
+
# @param output [Array] Used for recursion, should be empty on first call
|
27
|
+
# @example simple value
|
28
|
+
# cocina = { "value" => "John Doe", "type" => "name" }
|
29
|
+
# Utils.flatten_nested_values(cocina)
|
30
|
+
# #=> [{"value" => "John Doe", "type" => "name"}]
|
31
|
+
# @example structured values
|
32
|
+
# cocina = { "structuredValue" => [{"value" => "foo"}, {"value" => "bar"}] }
|
33
|
+
# Utils.flatten_nested_values(cocina)
|
34
|
+
# #=> [{"value" => "foo"}, {"value" => "bar"}]
|
35
|
+
# @example parallel structured and simple values
|
36
|
+
# cocina = { "parallelValue" => [{"value" => "foo" }, { "structuredValue" => [{"value" => "bar"}, {"value" => "baz"}] }] }
|
37
|
+
# Utils.flatten_nested_values(cocina)
|
38
|
+
# #=> [{"value" => "foo"}, {"value" => "foo"}, {"value" => "baz"}]
|
39
|
+
def self.flatten_nested_values(cocina, output = [])
|
40
|
+
return [cocina] if cocina["value"].present?
|
41
|
+
return cocina.flat_map { |node| flatten_nested_values(node, output) } if cocina.is_a?(Array)
|
42
|
+
|
43
|
+
nested_values = Array(cocina["structuredValue"]) + Array(cocina["parallelValue"])
|
44
|
+
return output unless nested_values.any?
|
45
|
+
|
46
|
+
nested_values.flat_map { |node| flatten_nested_values(node, output) }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cocina_display
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nick Budak
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-07-
|
11
|
+
date: 2025-07-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: janeway-jsonpath
|
@@ -28,22 +28,16 @@ dependencies:
|
|
28
28
|
name: activesupport
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "~>"
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '8.0'
|
34
31
|
- - ">="
|
35
32
|
- !ruby/object:Gem::Version
|
36
|
-
version:
|
33
|
+
version: '7'
|
37
34
|
type: :runtime
|
38
35
|
prerelease: false
|
39
36
|
version_requirements: !ruby/object:Gem::Requirement
|
40
37
|
requirements:
|
41
|
-
- - "~>"
|
42
|
-
- !ruby/object:Gem::Version
|
43
|
-
version: '8.0'
|
44
38
|
- - ">="
|
45
39
|
- !ruby/object:Gem::Version
|
46
|
-
version:
|
40
|
+
version: '7'
|
47
41
|
- !ruby/object:Gem::Dependency
|
48
42
|
name: edtf
|
49
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -176,12 +170,20 @@ files:
|
|
176
170
|
- Rakefile
|
177
171
|
- lib/cocina_display.rb
|
178
172
|
- lib/cocina_display/cocina_record.rb
|
173
|
+
- lib/cocina_display/concerns/access.rb
|
174
|
+
- lib/cocina_display/concerns/contributors.rb
|
179
175
|
- lib/cocina_display/concerns/events.rb
|
176
|
+
- lib/cocina_display/concerns/identifiers.rb
|
177
|
+
- lib/cocina_display/concerns/subjects.rb
|
178
|
+
- lib/cocina_display/concerns/titles.rb
|
179
|
+
- lib/cocina_display/contributor.rb
|
180
180
|
- lib/cocina_display/dates/date.rb
|
181
181
|
- lib/cocina_display/dates/date_range.rb
|
182
182
|
- lib/cocina_display/imprint.rb
|
183
183
|
- lib/cocina_display/marc_country_codes.rb
|
184
|
+
- lib/cocina_display/subject.rb
|
184
185
|
- lib/cocina_display/title_builder.rb
|
186
|
+
- lib/cocina_display/utils.rb
|
185
187
|
- lib/cocina_display/version.rb
|
186
188
|
- sig/cocina_display.rbs
|
187
189
|
homepage: https://sul-dlss.github.io/cocina_display/
|