• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pulibrary / bibdata / 474603db-3a91-41f8-a417-232df02b6c05

19 Aug 2025 09:09PM UTC coverage: 89.972% (-2.1%) from 92.112%
474603db-3a91-41f8-a417-232df02b6c05

Pull #2870

circleci

christinach
Refactor items_by_852 method
Extract code and write non_private_items in Rust

Co-authored-by: Jane Sandberg <sandbergja@users.noreply.github.com>
Pull Request #2870: Refactor items_by_852 method

1 of 15 new or added lines in 3 files covered. (6.67%)

153 existing lines in 7 files now uncovered.

6980 of 7758 relevant lines covered (89.97%)

329.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.15
/marc_to_solr/lib/princeton_marc.rb
1
require 'active_support'
1✔
2
require 'library_standard_numbers'
1✔
3
require 'lightly'
1✔
4
require 'uri'
1✔
5
require_relative 'cache_adapter'
1✔
6
require_relative 'cache_manager'
1✔
7
require_relative 'cache_map'
1✔
8
require_relative 'composite_cache_map'
1✔
9
require_relative 'electronic_access_link'
1✔
10
require_relative 'electronic_access_link_factory'
1✔
11
require_relative 'hierarchical_heading'
1✔
12
require_relative 'iiif_manifest_url_builder'
1✔
13
require_relative 'linked_fields_extractor'
1✔
14
require_relative 'orangelight_url_builder'
1✔
15
require_relative 'process_holdings_helpers'
1✔
16

17
module MARC
1✔
18
  class Record
1✔
19
    # Taken from pul-store marc.rb lib extension
20
    # Shamelessly lifted from SolrMARC, with a few changes; no doubt there will
21
    # be more.
22
    @@THREE_OR_FOUR_DIGITS = /^(20|19|18|17|16|15|14|13|12|11|10|9|8|7|6|5|4|3|2|1)(\d{2})\.?$/
1✔
23
    @@FOUR_DIGIT_PATTERN_BRACES = /^\[([12]\d{3})\??\]\.?$/
1✔
24
    @@FOUR_DIGIT_PATTERN_ONE_BRACE = /^\[(20|19|18|17|16|15|14|13|12|11|10)(\d{2})/
1✔
25
    @@FOUR_DIGIT_PATTERN_OTHER_1 = /^l(\d{3})/
1✔
26
    @@FOUR_DIGIT_PATTERN_OTHER_2 = /^\[(20|19|18|17|16|15|14|13|12|11|10)\](\d{2})/
1✔
27
    @@FOUR_DIGIT_PATTERN_OTHER_3 = /^\[?(20|19|18|17|16|15|14|13|12|11|10)(\d)[^\d]\]?/
1✔
28
    @@FOUR_DIGIT_PATTERN_OTHER_4 = /i\.e\.,? (20|19|18|17|16|15|14|13|12|11|10)(\d{2})/
1✔
29
    @@FOUR_DIGIT_PATTERN_OTHER_5 = /^\[?(\d{2})--\??\]?/
1✔
30
    @@BC_DATE_PATTERN = /[0-9]+ [Bb]\.?[Cc]\.?/
1✔
31
    def best_date
1✔
32
      date = nil
×
33
      if self['260'] && (self['260']['c'])
×
34
        field_260c = self['260']['c']
×
35
        case field_260c
×
36
        when @@THREE_OR_FOUR_DIGITS
37
          date = "#{$1}#{$2}"
×
38
        when @@FOUR_DIGIT_PATTERN_BRACES
39
          date = $1
×
40
        when @@FOUR_DIGIT_PATTERN_ONE_BRACE
41
          date = $1
×
42
        when @@FOUR_DIGIT_PATTERN_OTHER_1
43
          date = "1#{$1}"
×
44
        when @@FOUR_DIGIT_PATTERN_OTHER_2
45
          date = "#{$1}#{$2}"
×
46
        when @@FOUR_DIGIT_PATTERN_OTHER_3
47
          date = "#{$1}#{$2}0"
×
48
        when @@FOUR_DIGIT_PATTERN_OTHER_4
49
          date = "#{$1}#{$2}"
×
50
        when @@FOUR_DIGIT_PATTERN_OTHER_5
51
          date = "#{$1}00"
×
52
        when @@BC_DATE_PATTERN
53
          date = nil
×
54
        end
55
      end
56
      date ||= self.date_from_008
×
57
    end
58

59
    def date_from_008
1✔
60
      if self['008']
82✔
61
        d = self['008'].value[7, 4]
28✔
62
        d = d.tr 'u', '0' unless d == 'uuuu'
28✔
63
        d = d.tr ' ', '0' unless d == '    '
28✔
64
        d if /^[0-9]{4}$/.match?(d)
28✔
65
      end
66
    end
67

68
    def end_date_from_008
1✔
69
      if self['008']
43✔
70
        d = self['008'].value[11, 4]
16✔
71
        d = d.tr 'u', '9' unless d == 'uuuu'
16✔
72
        d = d.tr ' ', '9' unless d == '    '
16✔
73
        d if /^[0-9]{4}$/.match?(d)
16✔
74
      end
75
    end
76

77
    def date_display
1✔
78
      date = nil
×
79
      date = self['260']['c'] if self['260'] && (self['260']['c'])
×
80
      date ||= self.date_from_008
×
81
    end
82
  end
83
end
84

85
FALLBACK_STANDARD_NO = 'Other standard number'
1✔
86
def map_024_indicators_to_labels i
1✔
87
  case i
8✔
88
  when '0' then 'International Standard Recording Code'
×
UNCOV
89
  when '1' then 'Universal Product Code'
×
90
  when '2' then 'International Standard Music Number'
×
91
  when '3' then 'International Article Number'
1✔
92
  when '4' then 'Serial Item and Contribution Identifier'
2✔
93
  when '7' then '$2'
3✔
94
  else FALLBACK_STANDARD_NO
2✔
95
  end
96
end
97

98
def indicator_label_246 i
1✔
99
  case i
×
100
  when '0' then 'Portion of title'
×
101
  when '1' then 'Parallel title'
×
102
  when '2' then 'Distinctive title'
×
103
  when '3' then 'Other title'
×
104
  when '4' then 'Cover title'
×
105
  when '5' then 'Added title page title'
×
106
  when '6' then 'Caption title'
×
107
  when '7' then 'Running title'
×
108
  when '8' then 'Spine title'
×
109
  end
110
end
111

112
def subfield_specified_hash_key subfield_value, fallback
1✔
113
  key = subfield_value.capitalize.gsub(/[[:punct:]]?$/, '')
2✔
114
  key.empty? ? fallback : key
2✔
115
end
116

117
def standard_no_hash record
1✔
118
  standard_no = {}
41✔
119
  Traject::MarcExtractor.cached('024').collect_matching_lines(record) do |field, _spec, _extractor|
41✔
120
    standard_label = map_024_indicators_to_labels(field.indicator1)
8✔
121
    standard_number = nil
8✔
122
    field.subfields.each do |s_field|
8✔
123
      standard_number = s_field.value if s_field.code == 'a'
10✔
124
      if (s_field.code == '2') && (standard_label == '$2')
10✔
125
        standard_label = subfield_specified_hash_key(s_field.value, FALLBACK_STANDARD_NO)
2✔
126
      end
127
    end
128
    standard_label = FALLBACK_STANDARD_NO if standard_label == '$2'
8✔
129
    unless standard_number.nil?
8✔
130
      standard_no[standard_label] ? standard_no[standard_label] << standard_number : standard_no[standard_label] = [standard_number]
8✔
131
    end
132
  end
133
  standard_no
41✔
134
end
135

136
# Handles ISBNs, ISSNs, and OCLCs
137
# ISBN: 020a, 020z, 776z
138
# ISSN: 022a, 022l, 022y, 022z, 776x
139
# OCLC: 035a, 776w, 787w
140
# BIB: 776w, 787w (adds BIB prefix so Blacklight can detect whether to search id field)
141
def other_versions record
1✔
142
  linked_nums = []
41✔
143
  Traject::MarcExtractor.cached('020az:022alyz:035a:776wxz:787w').collect_matching_lines(record) do |field, _spec, _extractor|
41✔
144
    field.subfields.each do |s_field|
20✔
145
      if (field.tag == '020') || ((field.tag == '776') && (s_field.code == 'z'))
30✔
146
        linked_nums << LibraryStandardNumbers::ISBN.normalize(s_field.value)
9✔
147
      end
148
      if (field.tag == '022') || ((field.tag == '776') && (s_field.code == 'x'))
30✔
149
        linked_nums << LibraryStandardNumbers::ISSN.normalize(s_field.value)
3✔
150
      end
151
      linked_nums << oclc_normalize(s_field.value, prefix: true) if (field.tag == '035') && oclc_number?(s_field.value)
30✔
152
      if ((field.tag == '776') && (s_field.code == 'w')) || ((field.tag == '787') && (s_field.code == 'w'))
30✔
153
        linked_nums << oclc_normalize(s_field.value, prefix: true) if oclc_number?(s_field.value)
4✔
154
        linked_nums << ('BIB' + BibdataRs::Marc.strip_non_numeric(s_field.value)) unless s_field.value.include?('(')
4✔
155
        if s_field.value.include?('(') && !s_field.value.start_with?('(')
4✔
156
          logger.error "#{record['001']} - linked field formatting: #{s_field.value}"
×
157
        end
158
      end
159
    end
160
  end
161
  linked_nums.compact.uniq
41✔
162
end
163

164
# only includes values before $t
165
def process_names record
1✔
166
  Traject::MarcExtractor.cached('100aqbcdk:110abcdfgkln:111abcdfgklnpq:700aqbcdk:710abcdfgkln:711abcdfgklnpq').collect_matching_lines(record) do |field, spec, extractor|
42✔
167
    name = extractor.collect_subfields(field, spec).first
11✔
168
    unless name.nil?
11✔
169
      remove = ''
11✔
170
      after_t = false
11✔
171
      field.subfields.each do |s_field|
11✔
172
        remove << " #{s_field.value}" if after_t && spec.includes_subfield_code?(s_field.code)
46✔
173
        after_t = true if s_field.code == 't'
46✔
174
      end
175
      name = name.chomp(remove)
11✔
176
      Traject::Macros::Marc21.trim_punctuation(name)
11✔
177
    end
178
  end.compact.uniq
179
end
180

181
# only includes values before $t
182
def process_alt_script_names record
1✔
183
  names = []
42✔
184
  Traject::MarcExtractor.cached('100aqbcdk:110abcdfgkln:111abcdfgklnpq:700aqbcdk:710abcdfgkln:711abcdfgklnpq').collect_matching_lines(record) do |field, spec, extractor|
42✔
185
    next unless field.tag == '880'
11✔
186

187
    name = extractor.collect_subfields(field, spec).first
3✔
188
    unless name.nil?
3✔
189
      remove = ''
3✔
190
      after_t = false
3✔
191
      field.subfields.each do |s_field|
3✔
192
        remove << " #{s_field.value}" if after_t && spec.includes_subfield_code?(s_field.code)
12✔
193
        after_t = true if s_field.code == 't'
12✔
194
      end
195
      name = name.chomp(remove)
3✔
196
      names << Traject::Macros::Marc21.trim_punctuation(name)
3✔
197
    end
198
  end
199
  names.uniq
42✔
200
end
201

202
##
203
# Get hash of authors grouped by role
204
# @param [MARC::Record]
205
# @return [Hash]
206
def process_author_roles record
1✔
207
  author_roles = {
208
    'TRL' => 'translators',
42✔
209
    'EDT' => 'editors',
210
    'COM' => 'compilers',
211
    'TRANSLATOR' => 'translators',
212
    'EDITOR' => 'editors',
213
    'COMPILER' => 'compilers'
214
  }
215

216
  names = {}
42✔
217
  names['secondary_authors'] = []
42✔
218
  names['translators'] = []
42✔
219
  names['editors'] = []
42✔
220
  names['compilers'] = []
42✔
221

222
  Traject::MarcExtractor.cached('100a:110a:111a:700a:710a:711a').collect_matching_lines(record) do |field, spec, extractor|
42✔
223
    name = extractor.collect_subfields(field, spec).first
16✔
224
    unless name.nil?
16✔
225
      name = Traject::Macros::Marc21.trim_punctuation(name)
16✔
226

227
      # If name is from 1xx field, it is the primary author.
228
      if /1../.match?(field.tag)
16✔
229
        names['primary_author'] = name
6✔
230
      else
231
        relator = ''
10✔
232
        field.subfields.each do |s_field|
10✔
233
          # relator code (subfield 4)
234
          if s_field.code == '4'
28✔
235
            relator = s_field.value.upcase.gsub(/[[:punct:]]?$/, '')
4✔
236
          # relator term (subfield e)
237
          elsif s_field.code == 'e'
24✔
238
            relator = s_field.value.upcase.gsub(/[[:punct:]]?$/, '')
5✔
239
          end
240
        end
241

242
        # Set role from relator value.
243
        role = author_roles[relator] || 'secondary_authors'
10✔
244
        names[role] << name
10✔
245
      end
246
    end
247
  end
248
  names
42✔
249
end
250

251
##
252
# Process publication information for citations.
253
# @param [MARC::Record]
254
# @return [Array] pub info strings from fields 260 and 264.
255
def set_pub_citation(record)
1✔
256
  Traject::MarcExtractor.cached('260:264').collect_matching_lines(record) do |field, _spec, _extractor|
44✔
257
    a_pub_info = nil
15✔
258
    b_pub_info = nil
15✔
259
    pub_info = ''
15✔
260
    field.subfields.each do |s_field|
15✔
261
      a_pub_info = Traject::Macros::Marc21.trim_punctuation(s_field.value).strip if s_field.code == 'a'
25✔
262
      b_pub_info = Traject::Macros::Marc21.trim_punctuation(s_field.value).strip if s_field.code == 'b'
25✔
263
    end
264

265
    # Build publication info string and add to citation array.
266
    pub_info += a_pub_info unless a_pub_info.nil?
15✔
267
    pub_info += ': ' if !a_pub_info.nil? && !b_pub_info.nil?
15✔
268
    pub_info += b_pub_info unless b_pub_info.nil?
15✔
269
    pub_info if !pub_info.empty?
15✔
270
  end.compact
271
end
272

273
SEPARATOR = '—'
1✔
274

275
# for the hierarchical subject/genre display
276
# split with em dash along t,v,x,y,z
277
# optionally pass a block to only allow fields that match certain criteria
278
# For example, if you only want subject headings from the Bilindex vocabulary,
279
# you could use `process_hierarchy(record, '650|*7|abcvxyz') { |field| field['2'] == 'bidex' }`
280
def process_hierarchy(record, fields)
1✔
281
  split_on_subfield = %w[t v x y z]
564✔
282
  Traject::MarcExtractor.cached(fields).collect_matching_lines(record) do |field, spec, extractor|
564✔
283
    include_heading = block_given? ? yield(field) : true
247✔
284
    next unless include_heading && extractor.collect_subfields(field, spec).first
247✔
285

286
    HierarchicalHeading.new(field:, spec:, split_on_subfield:).to_s
162✔
287
  end.compact
288
end
289

290
def accumulate_hierarchy_per_field(record, fields)
1✔
291
  split_on_subfield = %w[t v x y z]
360✔
292
  Traject::MarcExtractor.cached(fields).collect_matching_lines(record) do |field, spec, extractor|
360✔
293
    include_heading = block_given? ? yield(field) : true
150✔
294
    next unless include_heading && extractor.collect_subfields(field, spec).first
150✔
295

296
    hierarchical_heading = HierarchicalHeading.new(field:, spec:, split_on_subfield:).to_s
79✔
297

298
    heading_split_on_separator = hierarchical_heading.split(SEPARATOR)
79✔
299
    accumulate_subheading(heading_split_on_separator)
79✔
300
  end.compact
301
end
302

303
def accumulate_subheading(heading_split_on_separator)
1✔
304
  heading_split_on_separator.reduce([]) do |accumulator, subheading|
79✔
305
    # accumulator.last ? "#{accumulator.last}#{SEPARATOR}#{subsubject}" : subsubject
306
    accumulator.append([accumulator.last, subheading].compact.join(SEPARATOR))
169✔
307
  end
308
end
309

310
# for the split subject facet
311
# split with em dash along x,z
312
def process_subject_topic_facet record
1✔
313
  lcsh_subjects = Traject::MarcExtractor.cached('600|*0|abcdfklmnopqrtxz:610|*0|abfklmnoprstxz:611|*0|abcdefgklnpqstxz:630|*0|adfgklmnoprstxz:650|*0|abcxz:651|*0|axz').collect_matching_lines(record) do |field, spec, extractor|
41✔
314
    subject = extractor.collect_subfields(field, spec).first
37✔
315
    unless subject.nil?
37✔
316
      hierarchical_string = HierarchicalHeading.new(field:, spec:, split_on_subfield: %w[x z]).to_s
37✔
317
      hierarchical_string.split(SEPARATOR)
37✔
318
    end
319
  end.compact
320
  other_thesaurus_subjects = Traject::MarcExtractor.cached('650|*7|abcxz').collect_matching_lines(record) do |field, spec, extractor|
41✔
321
    subject = extractor.collect_subfields(field, spec).first
2✔
322
    should_include = siku_heading?(field) || local_heading?(field) || any_thesaurus_match?(field, %w[homoit])
2✔
323
    if should_include && !subject.nil?
2✔
324
      hierarchical_string = HierarchicalHeading.new(field:, spec:, split_on_subfield: %w[x z]).to_s
2✔
325
      hierarchical_string.split(SEPARATOR)
2✔
326
    end
327
  end.flatten.compact
328
  lcsh_subjects + other_thesaurus_subjects
41✔
329
end
330

331
def oclc_number? oclc
1✔
332
  # Strip spaces and dashes
333
  clean_oclc = oclc.gsub(/[\-\s]/, '')
33✔
334
  # Ensure it follows the OCLC standard
335
  # (see https://help.oclc.org/Metadata_Services/WorldShare_Collection_Manager/Data_sync_collections/Prepare_your_data/30035_field_and_OCLC_control_numbers)
336
  clean_oclc.match(/\(OCoLC\)(ocn|ocm|on)*\d+/) != nil
33✔
337
end
338

339
def oclc_normalize oclc, opts = { prefix: false }
1✔
340
  oclc_num = BibdataRs::Marc.strip_non_numeric(oclc)
26✔
341
  if opts[:prefix] == true
26✔
342
    case oclc_num.length
17✔
343
    when 1..8
344
      'ocm' + ('%08d' % oclc_num)
5✔
345
    when 9
346
      'ocn' + oclc_num
2✔
347
    else
348
      'on' + oclc_num
10✔
349
    end
350
  else
351
    oclc_num
9✔
352
  end
353
end
354

355
# Construct (or retrieve) the cache manager service
356
# @return [CacheManager] the cache manager service
357
def build_cache_manager(figgy_dir_path:)
1✔
358
  return @cache_manager unless @cache_manager.nil?
6✔
359

360
  figgy_lightly = Lightly.new(dir: figgy_dir_path, life: 0, hash: false)
2✔
361
  figgy_cache_adapter = CacheAdapter.new(service: figgy_lightly)
2✔
362

363
  CacheManager.initialize(figgy_cache: figgy_cache_adapter, logger:)
2✔
364

365
  @cache_manager = CacheManager.current
2✔
366
end
367

368
# returns hash of links ($u) (key),
369
# anchor text ($y, $3, hostname), and additional labels ($z) (array value)
370
# @param [MARC::Record] the MARC record being parsed
371
# @return [Hash] the values used to construct the links
372
def electronic_access_links(record, figgy_dir_path)
1✔
373
  solr_field_values = {}
58✔
374
  holding_856s = {}
58✔
375
  iiif_manifest_paths = {}
58✔
376

377
  output = []
58✔
378
  iiif_manifest_links = []
58✔
379
  fragment_index = 0
58✔
380

381
  Traject::MarcExtractor.cached('856').collect_matching_lines(record) do |field, _spec, _extractor|
58✔
382
    anchor_text = false
21✔
383
    z_label = false
21✔
384
    url_key = false
21✔
385
    holding_id = nil
21✔
386
    bib_id = record['001']
21✔
387

388
    electronic_access_link = ElectronicAccessLinkFactory.build bib_id: bib_id, marc_field: field
21✔
389

390
    # If the electronic access link is an ARK...
391
    if electronic_access_link.ark
21✔
392
      # ...and attempt to build an Orangelight URL from the (cached) mappings exposed by the repositories
393
      cache_manager = build_cache_manager(figgy_dir_path:)
6✔
394

395
      # Orangelight links
396
      catalog_url_builder = OrangelightUrlBuilder.new(ark_cache: cache_manager.ark_cache, fragment: fragment_value(fragment_index))
6✔
397
      orangelight_url = catalog_url_builder.build(url: electronic_access_link.ark)
6✔
398

399
      if orangelight_url
6✔
400
        # Index this by the domain for Orangelight
401
        anchor_text = electronic_access_link.anchor_text
6✔
402
        anchor_text = 'Digital content' if electronic_access_link.url&.host == electronic_access_link.anchor_text
6✔
403
        orangelight_link = electronic_access_link.clone url_key: orangelight_url.to_s, anchor_text: anchor_text
6✔
404
        # Only add the link to the current page if it resolves to a resource with a IIIF Manifest
405
        output << orangelight_link
6✔
406
      else
407
        # Otherwise, always add the link to the resource
UNCOV
408
        output << electronic_access_link
×
409
      end
410

411
      # Figgy URL's
412
      figgy_url_builder = IIIFManifestUrlBuilder.new(ark_cache: cache_manager.figgy_ark_cache, service_host: 'figgy.princeton.edu')
6✔
413
      figgy_iiif_manifest = figgy_url_builder.build(url: electronic_access_link.ark)
6✔
414
      if figgy_iiif_manifest
6✔
415
        figgy_iiif_manifest_link = electronic_access_link.clone url_key: figgy_iiif_manifest.to_s
6✔
416
        iiif_manifest_paths[electronic_access_link.url_key] = figgy_iiif_manifest_link.url.to_s
6✔
417
      end
418

419
    else
420
      # Always add links to the resource if it isn't an ARK
421
      output << electronic_access_link
15✔
422
    end
423

424
    output.each do |link|
21✔
425
      if link.holding_id
27✔
426
        holding_856s[link.holding_id] = { link.url_key => link.url_labels }
1✔
427
      elsif link.url_key && link.url_labels
26✔
428
        solr_field_values[link.url_key] = link.url_labels
22✔
429
      end
430
    end
431
    fragment_index += 1
21✔
432
  end
433

434
  solr_field_values['holding_record_856s'] = holding_856s unless holding_856s == {}
58✔
435
  solr_field_values['iiif_manifest_paths'] = iiif_manifest_paths unless iiif_manifest_paths.empty?
58✔
436
  solr_field_values
58✔
437
end
438

439
def fragment_value(fragment_index)
1✔
440
  if fragment_index == 0
6✔
441
    'view'
2✔
442
  else
443
    "view_#{fragment_index}"
4✔
444
  end
445
end
446

447
def remove_parens_035 standard_no
1✔
448
  standard_no.gsub(/^\(.*?\)/, '')
8✔
449
end
450

451
def everything_after_t record, fields
1✔
452
  values = []
166✔
453
  Traject::MarcExtractor.cached(fields).collect_matching_lines(record) do |field, _spec, _extractor|
166✔
454
    after_t = false
13✔
455
    title = []
13✔
456
    field.subfields.each do |s_field|
13✔
457
      title << s_field.value if after_t
52✔
458
      if s_field.code == 't'
52✔
459
        title << s_field.value
4✔
460
        after_t = true
4✔
461
      end
462
    end
463
    values << Traject::Macros::Marc21.trim_punctuation(title.join(' ')) unless title.empty?
13✔
464
  end
465
  values
166✔
466
end
467

468
def everything_after_t_alt_script record, fields
1✔
469
  values = []
83✔
470
  Traject::MarcExtractor.cached(fields).collect_matching_lines(record) do |field, _spec, _extractor|
83✔
471
    next unless field.tag == '880'
12✔
472

473
    after_t = false
3✔
474
    title = []
3✔
475
    field.subfields.each do |s_field|
3✔
476
      title << s_field.value if after_t
12✔
477
      if s_field.code == 't'
12✔
478
        title << s_field.value
1✔
479
        after_t = true
1✔
480
      end
481
    end
482
    values << Traject::Macros::Marc21.trim_punctuation(title.join(' ')) unless title.empty?
3✔
483
  end
484
  values
83✔
485
end
486

487
def everything_through_t record, fields
1✔
488
  values = []
42✔
489
  Traject::MarcExtractor.cached(fields).collect_matching_lines(record) do |field, _spec, _extractor|
42✔
490
    non_t = true
3✔
491
    title = []
3✔
492
    field.subfields.each do |s_field|
3✔
493
      title << s_field.value
6✔
494
      if s_field.code == 't'
6✔
495
        non_t = false
2✔
496
        break
2✔
497
      end
498
    end
499
    values << Traject::Macros::Marc21.trim_punctuation(title.join(' ')) unless (title.empty? || non_t)
3✔
500
  end
501
  values
42✔
502
end
503

504
##
505
# @param record [MARC::Record]
506
# @param fields [String] MARC fields of interest
507
# @return [Array] of name-titles each in an [Array], each element [String] split by hierarchy,
508
# both name ($a) and title ($t) are required
509
def prep_name_title record, fields
1✔
510
  values = []
121✔
511
  Traject::MarcExtractor.cached(fields).collect_matching_lines(record) do |field, spec, _extractor|
121✔
512
    name_title = []
10✔
513
    author = []
10✔
514
    non_a = true
10✔
515
    non_t = true
10✔
516
    field.subfields.each do |s_field|
10✔
517
      next if (!spec.subfields.nil? && !spec.subfields.include?(s_field.code))
39✔
518

519
      non_a = false if s_field.code == 'a'
28✔
520
      non_t = false if s_field.code == 't'
28✔
521
      if non_t
28✔
522
        author << s_field.value
16✔
523
      else
524
        name_title << s_field.value
12✔
525
      end
526
    end
527
    unless (non_a || non_t)
10✔
528
      name_title.unshift(author.join(' '))
4✔
529
      values << name_title unless name_title.empty?
4✔
530
    end
531
  end
532
  values
121✔
533
end
534

535
# @param fields [Array] with portions of hierarchy
536
# @return [Array] portions of hierarchy including previous elements
537
def expand_sublists_for_hierarchy(fields)
1✔
538
  fields.collect do |field|
41✔
539
    field.collect.with_index do |_v, index|
5✔
540
      Traject::Macros::Marc21.trim_punctuation(field[0..index].join(' '))
16✔
541
    end
542
  end
543
end
544

545
# @param fields [Array] with portions of hierarchy from name-titles or title-only fields
546
# @return [Array] portions of hierarchy including previous elements
547
def join_hierarchy(fields, include_first_element: false)
1✔
548
  if include_first_element == false
41✔
549
    # Exclude the name-only portion of hierarchy
550
    expand_sublists_for_hierarchy(fields).map { |a| a[1..-1] }
6✔
551
  else
552
    # Include full hierarchy
553
    expand_sublists_for_hierarchy(fields)
39✔
554
  end
555
end
556

557
# Removes empty call_number fields from holdings_1display
558
def remove_empty_call_number_fields(holding)
1✔
UNCOV
559
  holding.tap { |h| %w[call_number call_number_browse].map { |k| h.delete(k) if h.fetch(k, []).empty? } }
×
560
end
561

562
# Collects only non empty khi
563
def call_number_khi(field)
1✔
UNCOV
564
  field.subfields.reject { |s| s.value.empty? }.collect { |s| s if %w[k h i].include?(s.code) }.compact
×
565
end
566

567
# Alma Princeton item
568
def alma_code_start_22?(code)
1✔
569
  code.to_s.start_with?('22') && code.to_s.end_with?('06421')
6✔
570
end
571

572
def alma_code_start_53?(code)
1✔
573
  code.to_s.start_with?('53') && code.to_s.end_with?('06421')
2✔
574
end
575

576
def scsb_code_start?(code)
1✔
UNCOV
577
  code.to_s.start_with?('scsb')
×
578
end
579

580
def alma_852(record)
1✔
581
  record.fields('852').select { |f| alma_code_start_22?(f['8']) }
114✔
582
end
583

584
def scsb_852(record)
1✔
UNCOV
585
  record.fields('852').select { |f| scsb_code_start?(f['b']) }
×
586
end
587

588
def browse_fields(record, marc_breaker:, khi_key_order: %w[k h i])
1✔
589
  result = []
76✔
590
  fields = if BibdataRs::Marc.is_scsb?(marc_breaker)
76✔
UNCOV
591
             scsb_852(record)
×
592
           else
593
             alma_852(record)
76✔
594
           end
595
  fields.each do |field|
76✔
UNCOV
596
    subfields = call_number_khi(field)
×
UNCOV
597
    next if subfields.empty?
×
598

UNCOV
599
    values = [field[khi_key_order[0]], field[khi_key_order[1]], field[khi_key_order[2]]].compact.reject(&:empty?)
×
UNCOV
600
    result << values.join(' ') if values.present?
×
601
  end
602
  result
76✔
603
end
604

605
def alma_876(record)
1✔
606
  record.fields('876').select { |f| alma_code_start_22?(f['0']) }
90✔
607
end
608

609
def alma_951_active(record)
1✔
610
  alma_951 = record.fields('951').select { |f| alma_code_start_53?(f['8']) }
118✔
611
  alma_951&.select { |f| f['a'] == 'Available' }
118✔
612
end
613

614
def alma_953(record)
1✔
615
  record.fields('953').select { |f| alma_code_start_53?(f['a']) }
38✔
616
end
617

618
def alma_954(record)
1✔
619
  record.fields('954').select { |f| alma_code_start_53?(f['a']) }
38✔
620
end
621

622
def alma_950(record)
1✔
623
  field_950_a = record.fields('950').select { |f| %w[true false].include?(f['a']) }
40✔
624
  field_950_a.map { |f| f['b'] }.first if field_950_a.present?
40✔
625
end
626

627
def process_holdings(record, marc_breaker)
1✔
628
  all_holdings = {}
40✔
629
  holdings_helpers = ProcessHoldingsHelpers.new(record:, marc_breaker:)
40✔
630
  holdings_helpers.fields_852_alma_or_scsb.each do |field_852|
40✔
631
    next if holdings_helpers.includes_only_private_scsb_items?(field_852)
2✔
632

633
    holding_id = holdings_helpers.holding_id(field_852)
2✔
634
    # Calculate the permanent holding
635
    holding = holdings_helpers.build_holding(field_852, permanent: true)
2✔
636
    items_by_holding = holdings_helpers.items_by_852(field_852)
2✔
637
    group_866_867_868_fields = holdings_helpers.group_866_867_868_on_holding_perm_id(holding_id, field_852)
2✔
638
    # if there are items (876 fields)
639
    if items_by_holding.present?
2✔
640
      add_permanent_items_to_holdings(items_by_holding, field_852, holdings_helpers, all_holdings, holding)
2✔
UNCOV
641
      add_temporary_items_to_holdings(items_by_holding, field_852, holdings_helpers, all_holdings)
×
642
    else
643
      # if there are no items (876 fields), create the holding by using the 852 field
UNCOV
644
      unless holding_id.nil? || invalid_location?(holding['location_code'])
×
UNCOV
645
        all_holdings[holding_id] = remove_empty_call_number_fields(holding)
×
646
      end
647
    end
UNCOV
648
    if all_holdings.present? && all_holdings[holding_id]
×
UNCOV
649
      all_holdings = holdings_helpers.process_866_867_868_fields(fields: group_866_867_868_fields, all_holdings:, holding_id:)
×
650
    end
651
  end
652
  all_holdings
38✔
653
end
654

655
def add_permanent_items_to_holdings(items_by_holding, field_852, holdings_helpers, all_holdings, holding)
1✔
656
  locations = holdings_helpers.select_permanent_location_876(items_by_holding, field_852)
2✔
657

UNCOV
658
  locations.each do |field_876|
×
UNCOV
659
    holding_key = holdings_helpers.holding_id(field_852)
×
UNCOV
660
    add_item_to_holding(field_852, field_876, holding_key, holdings_helpers, all_holdings, holding)
×
661
  end
662
end
663

664
def add_temporary_items_to_holdings(items_by_holding, field_852, holdings_helpers, all_holdings)
1✔
UNCOV
665
  locations = holdings_helpers.select_temporary_location_876(items_by_holding, field_852)
×
666

UNCOV
667
  locations.each do |field_876|
×
UNCOV
668
    next if holdings_helpers.includes_only_private_scsb_items?(field_852)
×
669

UNCOV
670
    if holdings_helpers.current_location_code(field_876) == 'RES_SHARE$IN_RS_REQ'
×
UNCOV
671
      holding = holdings_helpers.build_holding(field_852, permanent: true)
×
UNCOV
672
      holding_key = holdings_helpers.holding_id(field_852)
×
673
    else
UNCOV
674
      holding = holdings_helpers.build_holding(field_852, field_876, permanent: false)
×
UNCOV
675
      holding_key = holdings_helpers.current_location_code(field_876)
×
676
    end
UNCOV
677
    holding['temp_location_code'] = holdings_helpers.current_location_code(field_876)
×
UNCOV
678
    add_item_to_holding(field_852, field_876, holding_key, holdings_helpers, all_holdings, holding)
×
679
  end
680
end
681

682
def add_item_to_holding(field_852, field_876, holding_key, holdings_helpers, all_holdings, holding)
1✔
UNCOV
683
  item = holdings_helpers.build_item(field_852:, field_876:)
×
UNCOV
684
  if (holding_key.present? || !invalid_location?(holding['location_code'])) && all_holdings[holding_key].nil?
×
UNCOV
685
    all_holdings[holding_key] = remove_empty_call_number_fields(holding)
×
686
  end
UNCOV
687
  all_holdings = holdings_helpers.holding_items(value: holding_key, all_holdings:, item:)
×
688
end
689

690
def invalid_location?(code)
1✔
UNCOV
691
  Traject::TranslationMap.new('locations')[code].nil?
×
692
end
693

694
def local_heading?(field)
1✔
695
  field.any? { |subfield| subfield.code == '2' && subfield.value == 'local' } &&
3✔
UNCOV
696
    field.any? { |subfield| subfield.code == '5' && subfield.value == 'NjP' }
×
697
end
698

699
def siku_heading?(field)
1✔
700
  any_thesaurus_match? field, %w[sk skbb]
2✔
701
end
702

703
def any_thesaurus_match?(field, thesauri)
1✔
704
  field.any? { |subfield| subfield.code == '2' && thesauri.include?(subfield.value) }
869✔
705
end
706

707
def valid_linked_fields(record, field_tag, accumulator)
1✔
708
  accumulator.concat LinkedFieldsExtractor.new(record, field_tag).mms_ids
82✔
709
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc