cocina-models 0.93.1 → 0.94.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8cd049152e51891b2ab7fc1bd63fff3f95f8914f9939122ffb6990667f17ad98
4
- data.tar.gz: 59645a00ccb0d8b21ae4830563f037dba0655a85d4f1b268ba21da66e6406d43
3
+ metadata.gz: 38c6748d5c7659224f4139fc5d96b6b58732183bf133bb8ea32657a81fadc9ff
4
+ data.tar.gz: 29615bf905de450af973bd8a7ee00d0cd45810a8d2bd2b55166b2e686e0e048f
5
5
  SHA512:
6
- metadata.gz: df8635b1b7318cd957f2071aac2e46cce144119d5c497d166d2e0152db0269fdccd611ea72872e80217a9e1e07ed354393add1e440cc580f86e8493953684767
7
- data.tar.gz: f12b79669c54e657aa0956412227a18f7445ba4940d4a6a5d27f9d6d9a79fe72aa408df096114e636ff8bd99d76bdfeb99f017efef989f12a27ef891bb4a9033
6
+ metadata.gz: 3d6192a5bc54d0133483649d0bf421a4ce32f85b328d9cda131a6ce8aff7df180b045de1b58d5c78836a7ec53e326b04a141e3a616046febfc31650ec8ef4e80
7
+ data.tar.gz: 93069e72c38d812ddd493c0de9abcb80a0733fad353a36721821250cbf8c36482b1711748760a8f9a1cbc8fa4915654cf4236496f4a99eaa4ab8e687fee74cbe
data/.rspec CHANGED
@@ -1,3 +1,2 @@
1
- --format documentation
2
1
  --color
3
2
  --require spec_helper
data/.rubocop.yml CHANGED
@@ -458,4 +458,39 @@ Style/YAMLFileRead: # new in 1.53
458
458
  RSpec/ReceiveMessages: # new in 2.23
459
459
  Enabled: true
460
460
  RSpec/Rails/NegationBeValid: # new in 2.23
461
- Enabled: true
461
+ Enabled: true
462
+
463
+ Lint/ItWithoutArgumentsInBlock: # new in 1.59
464
+ Enabled: true
465
+ Lint/LiteralAssignmentInCondition: # new in 1.58
466
+ Enabled: true
467
+ Style/SingleLineDoEndBlock: # new in 1.57
468
+ Enabled: true
469
+ Style/SuperWithArgsParentheses: # new in 1.58
470
+ Enabled: true
471
+ Capybara/ClickLinkOrButtonStyle: # new in 2.19
472
+ Enabled: true
473
+ Capybara/RedundantWithinFind: # new in 2.20
474
+ Enabled: true
475
+ Capybara/RSpec/HaveSelector: # new in 2.19
476
+ Enabled: true
477
+ Capybara/RSpec/PredicateMatcher: # new in 2.19
478
+ Enabled: true
479
+ FactoryBot/ExcessiveCreateList: # new in 2.25
480
+ Enabled: true
481
+ FactoryBot/IdSequence: # new in 2.24
482
+ Enabled: true
483
+ RSpec/EmptyMetadata: # new in 2.24
484
+ Enabled: true
485
+ RSpec/Eq: # new in 2.24
486
+ Enabled: true
487
+ RSpec/MetadataStyle: # new in 2.24
488
+ Enabled: true
489
+ RSpec/RedundantPredicateMatcher: # new in 2.26
490
+ Enabled: true
491
+ RSpec/RemoveConst: # new in 2.26
492
+ Enabled: true
493
+ RSpec/SpecFilePathFormat: # new in 2.24
494
+ Enabled: true
495
+ RSpec/SpecFilePathSuffix: # new in 2.24
496
+ Enabled: true
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cocina-models (0.93.1)
4
+ cocina-models (0.94.1)
5
5
  activesupport
6
6
  deprecation
7
7
  dry-struct (~> 1.0)
@@ -21,7 +21,7 @@ PATH
21
21
  GEM
22
22
  remote: https://rubygems.org/
23
23
  specs:
24
- activesupport (7.1.2)
24
+ activesupport (7.1.3)
25
25
  base64
26
26
  bigdecimal
27
27
  concurrent-ruby (~> 1.0, >= 1.0.2)
@@ -34,14 +34,14 @@ GEM
34
34
  ast (2.4.2)
35
35
  attr_extras (7.1.0)
36
36
  base64 (0.2.0)
37
- bigdecimal (3.1.5)
37
+ bigdecimal (3.1.6)
38
38
  byebug (11.1.3)
39
39
  committee (5.0.0)
40
40
  json_schema (~> 0.14, >= 0.14.3)
41
41
  openapi_parser (~> 1.0)
42
42
  rack (>= 1.5)
43
43
  commonmarker (0.23.10)
44
- concurrent-ruby (1.2.2)
44
+ concurrent-ruby (1.2.3)
45
45
  connection_pool (2.4.1)
46
46
  deprecation (1.1.0)
47
47
  activesupport
@@ -62,7 +62,8 @@ GEM
62
62
  dry-types (>= 1.7, < 2)
63
63
  ice_nine (~> 0.11)
64
64
  zeitwerk (~> 2.6)
65
- dry-types (1.7.1)
65
+ dry-types (1.7.2)
66
+ bigdecimal (~> 3.0)
66
67
  concurrent-ruby (~> 1.0)
67
68
  dry-core (~> 1.0)
68
69
  dry-inflector (~> 1.0)
@@ -81,7 +82,7 @@ GEM
81
82
  multi_json
82
83
  language_server-protocol (3.17.0.3)
83
84
  mini_portile2 (2.8.5)
84
- minitest (5.20.0)
85
+ minitest (5.21.2)
85
86
  multi_json (1.15.0)
86
87
  mutex_m (0.2.0)
87
88
  nokogiri (1.16.0)
@@ -92,7 +93,7 @@ GEM
92
93
  openapi_parser (1.0.0)
93
94
  optimist (3.1.0)
94
95
  parallel (1.24.0)
95
- parser (3.2.2.4)
96
+ parser (3.3.0.5)
96
97
  ast (~> 2.4.1)
97
98
  racc
98
99
  patience_diff (1.2.0)
@@ -101,7 +102,7 @@ GEM
101
102
  rack (3.0.8)
102
103
  rainbow (3.1.1)
103
104
  rake (13.1.0)
104
- regexp_parser (2.8.3)
105
+ regexp_parser (2.9.0)
105
106
  rexml (3.2.6)
106
107
  rspec (3.12.0)
107
108
  rspec-core (~> 3.12.0)
@@ -120,11 +121,11 @@ GEM
120
121
  rspec-core (>= 2, < 4, != 2.12.0)
121
122
  rss (0.3.0)
122
123
  rexml
123
- rubocop (1.59.0)
124
+ rubocop (1.60.1)
124
125
  json (~> 2.3)
125
126
  language_server-protocol (>= 3.17.0)
126
127
  parallel (~> 1.10)
127
- parser (>= 3.2.2.4)
128
+ parser (>= 3.3.0.2)
128
129
  rainbow (>= 2.2.2, < 4.0)
129
130
  regexp_parser (>= 1.8, < 3.0)
130
131
  rexml (>= 3.2.5, < 4.0)
@@ -133,13 +134,13 @@ GEM
133
134
  unicode-display_width (>= 2.4.0, < 3.0)
134
135
  rubocop-ast (1.30.0)
135
136
  parser (>= 3.2.1.0)
136
- rubocop-capybara (2.19.0)
137
+ rubocop-capybara (2.20.0)
138
+ rubocop (~> 1.41)
139
+ rubocop-factory_bot (2.25.1)
137
140
  rubocop (~> 1.41)
138
- rubocop-factory_bot (2.24.0)
139
- rubocop (~> 1.33)
140
141
  rubocop-rake (0.6.0)
141
142
  rubocop (~> 1.0)
142
- rubocop-rspec (2.25.0)
143
+ rubocop-rspec (2.26.1)
143
144
  rubocop (~> 1.40)
144
145
  rubocop-capybara (~> 2.17)
145
146
  rubocop-factory_bot (~> 2.22)
data/README.md CHANGED
@@ -143,7 +143,7 @@ which pushes the gem to rubygems.org.
143
143
 
144
144
  ### Step 2: Update client gems coupled to the models
145
145
 
146
- Release new versions of [sdr-client](https://github.com/sul-dlss/sdr-client) and [dor-services-client](https://github.com/sul-dlss/dor-services-client/) pinned to use the new cocina-models version because applications such as [Argo](https://github.com/sul-dlss/argo) depend on both of these gems using the same models.
146
+ Release new versions of [sdr-client](https://github.com/sul-dlss/sdr-client), [dor-services-client](https://github.com/sul-dlss/dor-services-client/), and [dor_indexing](https://github.com/sul-dlss/dor_indexing/) pinned to use the new cocina-models version because applications such as [Argo](https://github.com/sul-dlss/argo) depend on both of these gems using the same models.
147
147
 
148
148
  ### Step 3: Update services directly coupled to the models
149
149
 
@@ -356,6 +356,7 @@ identifier:
356
356
  - value: DOI
357
357
  code: doi
358
358
  - value: druid
359
+ - value: EUR-OP
359
360
  - value: FOLIO
360
361
  description: FOLIO HRID for the source record of the metadata.
361
362
  - value: GTIN-14 ID
@@ -391,6 +392,8 @@ identifier:
391
392
  - value: PMCID
392
393
  - value: PMID
393
394
  - value: record id
395
+ - value: ROR
396
+ code: ror
394
397
  - value: Senate Number
395
398
  - value: Series
396
399
  - value: SICI
@@ -424,6 +427,8 @@ note:
424
427
  - value: access note
425
428
  status: deprecated
426
429
  use: access
430
+ - value: access restriction
431
+ description: Information about restrictions on who may or how to access a resource.
427
432
  - value: acquisition
428
433
  description: The transfer of a resource to a repository.
429
434
  - value: action
@@ -432,6 +437,9 @@ note:
432
437
  - value: additions
433
438
  description: Resources added after initial acquisition.
434
439
  - value: admin
440
+ status: deprecated
441
+ use: administrative
442
+ - value: administrative
435
443
  description: Administrative or internal use.
436
444
  - value: affiliation
437
445
  description: Institution with which a person or other entity is associated.
@@ -542,6 +550,9 @@ note:
542
550
  - value: reproduction
543
551
  - value: research
544
552
  - value: restriction
553
+ - value: restriction on access
554
+ status: deprecated
555
+ use: access restriction
545
556
  - value: rubric
546
557
  - value: scope and content
547
558
  - value: secfol
@@ -561,6 +572,7 @@ note:
561
572
  - value: table of contents
562
573
  - value: target audience
563
574
  - value: technical note
575
+ description: Technological requirements for accessing or using the resource.
564
576
  - value: thesis
565
577
  - value: transcript
566
578
  - value: translation
@@ -712,6 +724,8 @@ subject.structuredValue:
712
724
  description: An individual identity.
713
725
  - value: place
714
726
  description: A geographic location associated with the content of a resource.
727
+ - value: point coordinates
728
+ description: The latitude and longitude of a place associated with the content of a resource.
715
729
  - value: region
716
730
  description: An area that incorporates more than one first-order jurisdiction.
717
731
  - value: south
@@ -363,6 +363,7 @@ _Path: identifier.type_
363
363
  * document number
364
364
  * DOI
365
365
  * druid
366
+ * EUR-OP
366
367
  * FOLIO
367
368
  * FOLIO HRID for the source record of the metadata.
368
369
  * GTIN-14 ID
@@ -385,6 +386,7 @@ _Path: identifier.type_
385
386
  * PMCID
386
387
  * PMID
387
388
  * record id
389
+ * ROR
388
390
  * Senate Number
389
391
  * Series
390
392
  * SICI
@@ -409,6 +411,8 @@ _Path: note.type_
409
411
  * Information about gaining access to a resource.
410
412
  * access note
411
413
  * Deprecated. Preferred usage: access
414
+ * access restriction
415
+ * Information about restrictions on who may or how to access a resource.
412
416
  * acquisition
413
417
  * The transfer of a resource to a repository.
414
418
  * action
@@ -417,6 +421,8 @@ _Path: note.type_
417
421
  * additions
418
422
  * Resources added after initial acquisition.
419
423
  * admin
424
+ * Deprecated. Preferred usage: administrative
425
+ * administrative
420
426
  * Administrative or internal use.
421
427
  * affiliation
422
428
  * Institution with which a person or other entity is associated.
@@ -522,6 +528,8 @@ _Path: note.type_
522
528
  * reproduction
523
529
  * research
524
530
  * restriction
531
+ * restriction on access
532
+ * Deprecated. Preferred usage: access restriction
525
533
  * rubric
526
534
  * scope and content
527
535
  * secfol
@@ -539,6 +547,7 @@ _Path: note.type_
539
547
  * table of contents
540
548
  * target audience
541
549
  * technical note
550
+ * Technological requirements for accessing or using the resource.
542
551
  * thesis
543
552
  * transcript
544
553
  * translation
@@ -695,6 +704,8 @@ _Path: subject.structuredValue.type_
695
704
  * An individual identity.
696
705
  * place
697
706
  * A geographic location associated with the content of a resource.
707
+ * point coordinates
708
+ * The latitude and longitude of a place associated with the content of a resource.
698
709
  * region
699
710
  * An area that incorporates more than one first-order jurisdiction.
700
711
  * south
@@ -12,7 +12,8 @@ module Cocina
12
12
  # @param [Symbol] strategy ":first" is the strategy for selection when primary or display
13
13
  # title are missing
14
14
  # @param [Boolean] add_punctuation determines if the title should be formmated with punctuation
15
- # @return [String] the title value for Solr
15
+ # @return [String, Array] the title value for Solr - for :first strategy, a string; for :all strategy, an array
16
+ # (e.g. title displayed in blacklight search results vs boosting values for search result rankings)
16
17
  def self.build(titles, strategy: :first, add_punctuation: true)
17
18
  if titles.respond_to?(:description)
18
19
  Deprecation.warn(self,
@@ -27,7 +28,7 @@ module Cocina
27
28
  # we can boost matches on it in search results (boost matching this string higher than matching full title string)
28
29
  # e.g. "The Hobbit" (main_title) vs "The Hobbit, or, There and Back Again (full_title)
29
30
  # @param [[Array<Cocina::Models::Title,Cocina::Models::DescriptiveValue>] titles the titles to consider
30
- # @return [String] the main title value for Solr
31
+ # @return [Array<String>] the main title value(s) for Solr - array due to possible parallelValue
31
32
  def self.main_title(titles)
32
33
  new(strategy: :first, add_punctuation: false).main_title(titles)
33
34
  end
@@ -35,9 +36,9 @@ module Cocina
35
36
  # the "full title" is the title WITH subtitle, part name, etc. We want to able able to index it separately so
36
37
  # we can boost matches on it in search results (boost matching this string higher than other titles present)
37
38
  # @param [[Array<Cocina::Models::Title,Cocina::Models::DescriptiveValue>] titles the titles to consider
38
- # @return [String] the title value for Solr
39
+ # @return [Array<String>] the full title value(s) for Solr - array due to possible parallelValue
39
40
  def self.full_title(titles)
40
- new(strategy: :first, add_punctuation: false).build(titles)
41
+ [new(strategy: :first, add_punctuation: false, only_one_parallel_value: false).build(titles)].flatten.compact
41
42
  end
42
43
 
43
44
  # "additional titles" are all title data except for full_title. We want to able able to index it separately so
@@ -45,16 +46,28 @@ module Cocina
45
46
  # @param [[Array<Cocina::Models::Title,Cocina::Models::DescriptiveValue>] titles the titles to consider
46
47
  # @return [Array<String>] the values for Solr
47
48
  def self.additional_titles(titles)
48
- new(strategy: :all, add_punctuation: false).build(titles) - [full_title(titles)]
49
+ [new(strategy: :all, add_punctuation: false).build(titles)].flatten - full_title(titles)
49
50
  end
50
51
 
51
- def initialize(strategy:, add_punctuation:)
52
+ # @param strategy [Symbol] ":first" selects a single title value based on precedence of
53
+ # primary, untyped, first occurrence. ":all" returns an array containing all the values.
54
+ # @param add_punctuation [boolean] whether the title should be formmated with punctuation (think of a structured
55
+ # value coming from a MARC record, which is designed for catalog cards.)
56
+ # @param only_one_parallel_value [boolean] when true, choose one of the parallel values according to precedence
57
+ # of primary, untyped, first occurrence. When false, return an array containing all the parallel values.
58
+ # Why? Think of e.g. title displayed in blacklight search results vs boosting values for ranking of search
59
+ # results
60
+ def initialize(strategy:, add_punctuation:, only_one_parallel_value: true)
52
61
  @strategy = strategy
53
62
  @add_punctuation = add_punctuation
63
+ @only_one_parallel_value = only_one_parallel_value
54
64
  end
55
65
 
56
66
  # @param [[Array<Cocina::Models::Title>] cocina_titles the titles to consider
57
- # @return [String] the title value for Solr
67
+ # @return [String, Array] the title value for Solr - for :first strategy, a string; for :all strategy, an array
68
+ # (e.g. title displayed in blacklight search results vs boosting values for search result rankings)
69
+ #
70
+ # rubocop:disable Metrics/PerceivedComplexity
58
71
  def build(cocina_titles)
59
72
  cocina_title = primary_title(cocina_titles) || untyped_title(cocina_titles)
60
73
  cocina_title = other_title(cocina_titles) if cocina_title.blank?
@@ -62,15 +75,23 @@ module Cocina
62
75
  if strategy == :first
63
76
  extract_title(cocina_title)
64
77
  else
65
- cocina_titles.map { |ctitle| extract_title(ctitle) }.flatten
78
+ result = cocina_titles.map { |ctitle| extract_title(ctitle) }.flatten
79
+ if only_one_parallel_value? && result.length == 1
80
+ result.first
81
+ else
82
+ result
83
+ end
66
84
  end
67
85
  end
86
+ # rubocop:enable Metrics/PerceivedComplexity
68
87
 
88
+ # this is the single "short title" - the title without subtitle, part name, etc.
89
+ # this may be useful for boosting and exact matching for search results
90
+ # @return [Array<String>] the main title value(s) for Solr - can be array due to parallel titles
69
91
  def main_title(titles)
70
92
  cocina_title = primary_title(titles) || untyped_title(titles)
71
93
  cocina_title = other_title(titles) if cocina_title.blank?
72
94
 
73
- cocina_title = cocina_title.first if cocina_title.is_a?(Array)
74
95
  extract_main_title(cocina_title)
75
96
  end
76
97
 
@@ -79,40 +100,71 @@ module Cocina
79
100
  attr_reader :strategy
80
101
 
81
102
  def extract_title(cocina_title)
103
+ title_values = if cocina_title.value
104
+ cocina_title.value
105
+ elsif cocina_title.structuredValue.present?
106
+ rebuild_structured_value(cocina_title)
107
+ elsif cocina_title.parallelValue.present?
108
+ extract_title_parallel_values(cocina_title)
109
+ end
110
+ result = [title_values].flatten.compact.map { |val| remove_trailing_punctuation(val.strip) }
111
+ result.length == 1 ? result.first : result
112
+ end
113
+
114
+ # stategy :first says to return a single value (default: true)
115
+ # only_one_parallel_value? says to return a single value, even if that value is a parallelValue (default: false)
116
+ #
117
+ # rubocop:disable Metrics/PerceivedComplexity
118
+ def extract_title_parallel_values(cocina_title)
119
+ primary = cocina_title.parallelValue.find { |pvalue| pvalue.status == 'primary' }
120
+ if primary && only_one_parallel_value? && strategy == :first
121
+ # we have a primary title and we know we want a single value
122
+ extract_title(primary)
123
+ elsif only_one_parallel_value? && strategy == :first
124
+ # no primary value; algorithm says prefer an untyped value over a typed value for single value
125
+ untyped = cocina_title.parallelValue.find { |pvalue| pvalue.type.blank? }
126
+ extract_title(untyped || cocina_title.parallelValue.first)
127
+ else
128
+ cocina_title.parallelValue.map { |pvalue| extract_title(pvalue) }
129
+ end
130
+ end
131
+ # rubocop:enable Metrics/PerceivedComplexity
132
+
133
+ # @return [Array<String>] the main title value(s) for Solr - can be array due to parallel titles
134
+ def extract_main_title(cocina_title) # rubocop:disable Metrics/PerceivedComplexity
82
135
  result = if cocina_title.value
83
- cocina_title.value
136
+ cocina_title.value # covers both title and main title types
84
137
  elsif cocina_title.structuredValue.present?
85
- title_from_structured_values(cocina_title)
138
+ main_title_from_structured_values(cocina_title)
86
139
  elsif cocina_title.parallelValue.present?
87
- return build(cocina_title.parallelValue)
140
+ primary = cocina_title.parallelValue.find { |pvalue| pvalue.status == 'primary' }
141
+ if primary
142
+ extract_main_title(primary)
143
+ else
144
+ cocina_title.parallelValue.map { |pvalue| extract_main_title(pvalue) }
145
+ end
88
146
  end
89
- remove_trailing_punctuation(result.strip) if result.present?
90
- end
147
+ return [] if result.blank?
91
148
 
92
- def extract_main_title(cocina_title)
93
- if cocina_title.value
94
- cocina_title.value # covers both title and main title types
95
- elsif cocina_title.structuredValue.present?
96
- main_title_from_structured_values(cocina_title)
97
- elsif cocina_title.parallelValue.present?
98
- main_title(cocina_title.parallelValue)
99
- end
149
+ [result].flatten.compact.map { |val| remove_trailing_punctuation(val) }
100
150
  end
101
151
 
102
152
  def add_punctuation?
103
153
  @add_punctuation
104
154
  end
105
155
 
156
+ def only_one_parallel_value?
157
+ @only_one_parallel_value
158
+ end
159
+
106
160
  # @return [Cocina::Models::Title, nil] title that has status=primary
107
- def primary_title(titles)
108
- primary_title = titles.find do |title|
109
- title.status == 'primary'
110
- end
161
+ def primary_title(cocina_titles)
162
+ primary_title = cocina_titles.find { |title| title.status == 'primary' }
111
163
  return primary_title if primary_title.present?
112
164
 
113
165
  # NOTE: structuredValues would only have status primary assigned as a sibling, not as an attribute
114
166
 
115
- titles.find do |title|
167
+ cocina_titles.find do |title|
116
168
  title.parallelValue&.find do |parallel_title|
117
169
  parallel_title.status == 'primary'
118
170
  end
@@ -149,56 +201,63 @@ module Cocina
149
201
  # @return [String] the title value from combining the pieces of the structured_values by type and order
150
202
  # with desired punctuation per specs
151
203
  #
204
+ # for punctuaion funky town, thank MARC and catalog cards
205
+ #
206
+ # rubocop:disable Metrics/AbcSize
152
207
  # rubocop:disable Metrics/CyclomaticComplexity
153
208
  # rubocop:disable Metrics/MethodLength
154
209
  # rubocop:disable Metrics/PerceivedComplexity
155
- def title_from_structured_values(title)
156
- # parse out the parts
157
- main_title = ''
158
- subtitle = ''
159
- non_sort_value = ''
210
+ def rebuild_structured_value(cocina_title)
211
+ result = ''
160
212
  part_name_number = ''
161
- title.structuredValue.each do |structured_value|
162
- # There can be a structuredValue inside a structuredValue. For example,
213
+ cocina_title.structuredValue.each do |structured_value| # rubocop:disable Metrics/BlockLength
214
+ # There can be a structuredValue inside a structuredValue, for example,
163
215
  # a uniform title where both the name and the title have internal StructuredValue
164
- return title_from_structured_values(structured_value) if structured_value.structuredValue.present?
216
+ return rebuild_structured_value(structured_value) if structured_value.structuredValue.present?
165
217
 
166
218
  value = structured_value.value&.strip
167
219
  next unless value
168
220
 
169
- # additional types: name, uniform ...
221
+ # additional types ignored here, e.g. name, uniform ...
170
222
  case structured_value.type&.downcase
171
223
  when 'nonsorting characters'
172
- non_sort_value = "#{value}#{non_sorting_padding(title, value)}"
224
+ padding = non_sorting_padding(cocina_title, value)
225
+ result = add_non_sorting_value(result, value, padding)
173
226
  when 'part name', 'part number'
174
- part_name_number = part_name_number(title.structuredValue) if part_name_number.blank?
175
- when 'main title', 'title'
176
- main_title = value
177
- when 'subtitle'
178
- # combine multiple subtitles into a single string
179
- subtitle = if !add_punctuation?
180
- if subtitle.present?
181
- [subtitle, value].join(' ')
182
- else
183
- value
184
- end
185
- elsif subtitle.present?
186
- # subtitle is preceded by space colon space, unless it is at the beginning of the title string
187
- "#{subtitle.sub(/[. :]+$/, '')} : #{value.sub(/^:/, '').strip}"
227
+ if part_name_number.blank?
228
+ part_name_number = part_name_number(cocina_title.structuredValue)
229
+ result = if !add_punctuation?
230
+ [result, part_name_number].join(' ')
231
+ elsif result.present?
232
+ # part name/number is preceded by period space, unless it is at the beginning of the title string
233
+ "#{result.sub(/[ .,]*$/, '')}. #{part_name_number}. "
188
234
  else
189
- value.sub(/^:/, '').strip
235
+ "#{part_name_number}. "
190
236
  end
237
+ end
238
+ when 'main title', 'title'
239
+ # nonsorting characters ending with hyphen, apostrophe or space should be slammed against the main title,
240
+ # even if we are not adding punctuation
241
+ result = if add_punctuation? || result.ends_with?(' ') || result.ends_with?('-') || result.ends_with?('\'')
242
+ [result, value].join
243
+ else
244
+ [remove_trailing_punctuation(result), remove_trailing_punctuation(value)].select(&:presence).join(' ')
245
+ end
246
+ when 'subtitle'
247
+ result = if !add_punctuation?
248
+ [result, value].select(&:presence).join(' ')
249
+ elsif result.present?
250
+ # subtitle is preceded by space colon space, unless it is at the beginning of the title string
251
+ "#{result.sub(/[. :]+$/, '')} : #{value.sub(/^:/, '').strip}"
252
+ else
253
+ result = value.sub(/^:/, '').strip
254
+ end
191
255
  end
192
256
  end
193
257
 
194
- # combine the parts into a single title string
195
- if add_punctuation?
196
- combine_with_punctuation(non_sort_value: non_sort_value, main_title: main_title, subtitle: subtitle,
197
- part_name_number: part_name_number)
198
- else
199
- ["#{non_sort_value}#{main_title}", subtitle, part_name_number].select(&:presence).join(' ')
200
- end
258
+ result
201
259
  end
260
+ # rubocop:enable Metrics/AbcSize
202
261
  # rubocop:enable Metrics/CyclomaticComplexity
203
262
  # rubocop:enable Metrics/MethodLength
204
263
  # rubocop:enable Metrics/PerceivedComplexity
@@ -206,11 +265,14 @@ module Cocina
206
265
  # main_title is title.structuredValue.value with type 'main title' (or just title.value)
207
266
  # @param [Cocina::Models::Title] title with structured values
208
267
  # @return [String] the main title value
209
- def main_title_from_structured_values(cocina_title) # rubocop:disable Metrics/MethodLength
268
+ #
269
+ # rubocop:disable Metrics/MethodLength
270
+ # rubocop:disable Metrics/PerceivedComplexity
271
+ def main_title_from_structured_values(cocina_title)
210
272
  result = ''
211
273
  # combine pieces of the cocina structuredValue into a single title
212
274
  cocina_title.structuredValue.each do |structured_value|
213
- # There can be a structuredValue inside a structuredValue. For example,
275
+ # There can be a structuredValue inside a structuredValue, for example,
214
276
  # a uniform title where both the name and the title have internal StructuredValue
215
277
  return main_title_from_structured_values(structured_value) if structured_value.structuredValue.present?
216
278
 
@@ -219,43 +281,36 @@ module Cocina
219
281
 
220
282
  case structured_value.type&.downcase
221
283
  when 'nonsorting characters'
222
- non_sort_value = "#{value}#{non_sorting_padding(cocina_title, value)}"
223
- result = "#{non_sort_value}#{result}" # non-sorting characters are at the beginning of the title
224
- when 'main title'
225
- result = "#{result}#{value}"
226
- when 'title'
227
- result = value
284
+ padding = non_sorting_padding(cocina_title, value)
285
+ result = add_non_sorting_value(result, value, padding)
286
+ when 'main title', 'title'
287
+ result = if ['\'', '-'].include?(result.last)
288
+ [result, value].join
289
+ else
290
+ [remove_trailing_punctuation(result).strip, remove_trailing_punctuation(value).strip].select(&:presence).join(' ')
291
+ end
228
292
  end
229
293
  end
230
- result
231
- end
232
294
 
233
- # Thank MARC and catalog cards for this mess. We need to add punctuation.
234
- # rubocop:disable Metrics/MethodLength
235
- def combine_with_punctuation(non_sort_value:, main_title:, subtitle:, part_name_number:)
236
- result = "#{non_sort_value}#{main_title}"
237
- if subtitle.present?
238
- result = if result.present?
239
- "#{result.sub(/[. :]+$/, '')} : #{subtitle.sub(/^:/, '').strip}"
240
- else
241
- result = subtitle
242
- end
243
- end
244
- if part_name_number.present?
245
- result = if result.present?
246
- "#{result.sub(/[ .,]*$/, '')}. #{part_name_number}."
247
- else
248
- "#{part_name_number}."
249
- end
250
- end
251
295
  result
252
296
  end
253
297
  # rubocop:enable Metrics/MethodLength
298
+ # rubocop:enable Metrics/PerceivedComplexity
254
299
 
300
+ # Thank MARC and catalog cards for this mess.
255
301
  def remove_trailing_punctuation(title)
256
302
  title.sub(%r{[ .,;:/\\]+$}, '')
257
303
  end
258
304
 
305
+ def add_non_sorting_value(title_so_far, non_sorting_value, padding)
306
+ non_sort_value = "#{non_sorting_value}#{padding}"
307
+ if title_so_far.present?
308
+ [title_so_far.strip, padding, non_sort_value].join
309
+ else
310
+ non_sort_value
311
+ end
312
+ end
313
+
259
314
  def non_sorting_padding(title, non_sorting_value)
260
315
  non_sort_note = title.note&.find { |note| note.type&.downcase == 'nonsorting character count' }
261
316
  if non_sort_note
@@ -11,9 +11,7 @@ module Cocina
11
11
  PurlValidator,
12
12
  CatalogLinksValidator,
13
13
  AssociatedNameValidator,
14
- # Removing until production data can be remediated and/or additional types can be added to configuration.
15
- # See also spec/cocina/models/validatable_spec.rb:59
16
- # DescriptionTypesValidator,
14
+ DescriptionTypesValidator,
17
15
  DescriptionValuesValidator,
18
16
  DateTimeValidator,
19
17
  LanguageTagValidator
@@ -27,12 +25,12 @@ module Cocina
27
25
  VALIDATORS.each { |validator| validator.validate(clazz, attributes_hash) }
28
26
  end
29
27
 
30
- def self.deep_transform_values(object, &block)
28
+ def self.deep_transform_values(object, ...)
31
29
  case object
32
30
  when Hash
33
- object.transform_values { |value| deep_transform_values(value, &block) }
31
+ object.transform_values { |value| deep_transform_values(value, ...) }
34
32
  when Array
35
- object.map { |e| deep_transform_values(e, &block) }
33
+ object.map { |e| deep_transform_values(e, ...) }
36
34
  else
37
35
  yield(object)
38
36
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
- VERSION = '0.93.1'
5
+ VERSION = '0.94.1'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cocina-models
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.93.1
4
+ version: 0.94.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Coyne
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-01-04 00:00:00.000000000 Z
11
+ date: 2024-01-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport