cocina-models 0.93.1 → 0.94.1

Sign up to get free protection for your applications and to get access to all the features.
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