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

pulibrary / bibdata / 2ee2c4fc-5ef0-4806-b86e-01bf70aa67a0

24 Dec 2024 04:55PM UTC coverage: 91.859% (-0.04%) from 91.902%
2ee2c4fc-5ef0-4806-b86e-01bf70aa67a0

Pull #2569

circleci

christinach
Generate new .rubocop_todo.yml
rubocop fix
Pull Request #2569: Rubocop gems

335 of 378 new or added lines in 57 files covered. (88.62%)

2 existing lines in 2 files now uncovered.

3385 of 3685 relevant lines covered (91.86%)

377.92 hits per line

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

93.1
/marc_to_solr/lib/princeton_marc.rb
1
require 'active_support'
1✔
2
require 'library_stdnums'
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
×
NEW
33
      if self['260'] && (self['260']['c'])
×
NEW
34
        field_260c = self['260']['c']
×
NEW
35
        case field_260c
×
36
        when @@THREE_OR_FOUR_DIGITS
NEW
37
          date = "#{$1}#{$2}"
×
38
        when @@FOUR_DIGIT_PATTERN_BRACES
NEW
39
          date = $1
×
40
        when @@FOUR_DIGIT_PATTERN_ONE_BRACE
NEW
41
          date = $1
×
42
        when @@FOUR_DIGIT_PATTERN_OTHER_1
NEW
43
          date = "1#{$1}"
×
44
        when @@FOUR_DIGIT_PATTERN_OTHER_2
NEW
45
          date = "#{$1}#{$2}"
×
46
        when @@FOUR_DIGIT_PATTERN_OTHER_3
NEW
47
          date = "#{$1}#{$2}0"
×
48
        when @@FOUR_DIGIT_PATTERN_OTHER_4
NEW
49
          date = "#{$1}#{$2}"
×
50
        when @@FOUR_DIGIT_PATTERN_OTHER_5
NEW
51
          date = "#{$1}00"
×
52
        when @@BC_DATE_PATTERN
NEW
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']
330✔
61
        d = self['008'].value[7, 4]
242✔
62
        d = d.tr 'u', '0' unless d == 'uuuu'
242✔
63
        d = d.tr ' ', '0' unless d == '    '
242✔
64
        d if /^[0-9]{4}$/.match?(d)
242✔
65
      end
66
    end
67

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

77
    def date_display
1✔
78
      date = nil
×
NEW
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
14✔
88
  when '0' then 'International Standard Recording Code'
×
89
  when '1' then 'Universal Product Code'
2✔
90
  when '2' then 'International Standard Music Number'
×
91
  when '3' then 'International Article Number'
4✔
92
  when '4' then 'Serial Item and Contribution Identifier'
2✔
93
  when '7' then '$2'
3✔
94
  else FALLBACK_STANDARD_NO
3✔
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 = {}
165✔
119
  Traject::MarcExtractor.cached('024').collect_matching_lines(record) do |field, _spec, _extractor|
165✔
120
    standard_label = map_024_indicators_to_labels(field.indicator1)
14✔
121
    standard_number = nil
14✔
122
    field.subfields.each do |s_field|
14✔
123
      standard_number = s_field.value if s_field.code == 'a'
18✔
124
      if (s_field.code == '2') && (standard_label == '$2')
18✔
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'
14✔
129
    unless standard_number.nil?
14✔
130
      standard_no[standard_label] ? standard_no[standard_label] << standard_number : standard_no[standard_label] = [standard_number]
14✔
131
    end
132
  end
133
  standard_no
165✔
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 = []
165✔
143
  Traject::MarcExtractor.cached('020az:022alyz:035a:776wxz:787w').collect_matching_lines(record) do |field, _spec, _extractor|
165✔
144
    field.subfields.each do |s_field|
517✔
145
      if (field.tag == '020') || ((field.tag == '776') && (s_field.code == 'z'))
869✔
146
        linked_nums << StdNum::ISBN.normalize(s_field.value)
230✔
147
      end
148
      if (field.tag == '022') || ((field.tag == '776') && (s_field.code == 'x'))
869✔
149
        linked_nums << StdNum::ISSN.normalize(s_field.value)
25✔
150
      end
151
      linked_nums << oclc_normalize(s_field.value, prefix: true) if (field.tag == '035') && oclc_number?(s_field.value)
869✔
152
      if ((field.tag == '776') && (s_field.code == 'w')) || ((field.tag == '787') && (s_field.code == 'w'))
869✔
153
        linked_nums << oclc_normalize(s_field.value, prefix: true) if oclc_number?(s_field.value)
16✔
154
        linked_nums << ('BIB' + strip_non_numeric(s_field.value)) unless s_field.value.include?('(')
16✔
155
        if s_field.value.include?('(') && !s_field.value.start_with?('(')
16✔
NEW
156
          logger.error "#{record['001']} - linked field formatting: #{s_field.value}"
×
157
        end
158
      end
159
    end
160
  end
161
  linked_nums.compact.uniq
165✔
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|
166✔
167
    name = extractor.collect_subfields(field, spec).first
293✔
168
    unless name.nil?
293✔
169
      remove = ''
289✔
170
      after_t = false
289✔
171
      field.subfields.each do |s_field|
289✔
172
        remove << " #{s_field.value}" if after_t && spec.includes_subfield_code?(s_field.code)
978✔
173
        after_t = true if s_field.code == 't'
978✔
174
      end
175
      name = name.chomp(remove)
289✔
176
      Traject::Macros::Marc21.trim_punctuation(name)
289✔
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 = []
166✔
184
  Traject::MarcExtractor.cached('100aqbcdk:110abcdfgkln:111abcdfgklnpq:700aqbcdk:710abcdfgkln:711abcdfgklnpq').collect_matching_lines(record) do |field, spec, extractor|
166✔
185
    next unless field.tag == '880'
293✔
186

187
    name = extractor.collect_subfields(field, spec).first
67✔
188
    unless name.nil?
67✔
189
      remove = ''
67✔
190
      after_t = false
67✔
191
      field.subfields.each do |s_field|
67✔
192
        remove << " #{s_field.value}" if after_t && spec.includes_subfield_code?(s_field.code)
202✔
193
        after_t = true if s_field.code == 't'
202✔
194
      end
195
      name = name.chomp(remove)
67✔
196
      names << Traject::Macros::Marc21.trim_punctuation(name)
67✔
197
    end
198
  end
199
  names.uniq
166✔
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',
166✔
209
    'EDT' => 'editors',
210
    'COM' => 'compilers',
211
    'TRANSLATOR' => 'translators',
212
    'EDITOR' => 'editors',
213
    'COMPILER' => 'compilers'
214
  }
215

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

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

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

242
        # Set role from relator value.
243
        role = author_roles[relator] || 'secondary_authors'
216✔
244
        names[role] << name
216✔
245
      end
246
    end
247
  end
248
  names
166✔
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|
168✔
257
    a_pub_info = nil
161✔
258
    b_pub_info = nil
161✔
259
    pub_info = ''
161✔
260
    field.subfields.each do |s_field|
161✔
261
      a_pub_info = Traject::Macros::Marc21.trim_punctuation(s_field.value).strip if s_field.code == 'a'
483✔
262
      b_pub_info = Traject::Macros::Marc21.trim_punctuation(s_field.value).strip if s_field.code == 'b'
483✔
263
    end
264

265
    # Build publication info string and add to citation array.
266
    pub_info += a_pub_info unless a_pub_info.nil?
161✔
267
    pub_info += ': ' if !a_pub_info.nil? && !b_pub_info.nil?
161✔
268
    pub_info += b_pub_info unless b_pub_info.nil?
161✔
269
    pub_info if !pub_info.empty?
161✔
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]
2,300✔
282
  Traject::MarcExtractor.cached(fields).collect_matching_lines(record) do |field, spec, extractor|
2,300✔
283
    include_heading = block_given? ? yield(field) : true
2,354✔
284
    next unless include_heading && extractor.collect_subfields(field, spec).first
2,354✔
285

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

290
# for the split subject facet
291
# split with em dash along x,z
292
def process_subject_topic_facet record
1✔
293
  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|
165✔
294
    subject = extractor.collect_subfields(field, spec).first
265✔
295
    unless subject.nil?
265✔
296
      hierarchical_string = HierarchicalHeading.new(field:, spec:, split_on_subfield: %w[x z]).to_s
265✔
297
      hierarchical_string.split(SEPARATOR)
265✔
298
    end
299
  end.compact
300
  other_thesaurus_subjects = Traject::MarcExtractor.cached('650|*7|abcxz').collect_matching_lines(record) do |field, spec, extractor|
165✔
301
    subject = extractor.collect_subfields(field, spec).first
170✔
302
    should_include = siku_heading?(field) || local_heading?(field) || any_thesaurus_match?(field, %w[homoit])
170✔
303
    if should_include && !subject.nil?
170✔
304
      hierarchical_string = HierarchicalHeading.new(field:, spec:, split_on_subfield: %w[x z]).to_s
49✔
305
      hierarchical_string.split(SEPARATOR)
49✔
306
    end
307
  end.flatten.compact
308
  lcsh_subjects + other_thesaurus_subjects
165✔
309
end
310

311
def strip_non_numeric num_str
1✔
312
  num_str.gsub(/\D/, '').to_i.to_s
336✔
313
end
314

315
def oclc_number? oclc
1✔
316
  # Strip spaces and dashes
317
  clean_oclc = oclc.gsub(/[\-\s]/, '')
910✔
318
  # Ensure it follows the OCLC standard
319
  # (see https://help.oclc.org/Metadata_Services/WorldShare_Collection_Manager/Data_sync_collections/Prepare_your_data/30035_field_and_OCLC_control_numbers)
320
  clean_oclc.match(/\(OCoLC\)(ocn|ocm|on)*\d+/) != nil
910✔
321
end
322

323
def oclc_normalize oclc, opts = { prefix: false }
1✔
324
  oclc_num = strip_non_numeric(oclc)
333✔
325
  if opts[:prefix] == true
333✔
326
    case oclc_num.length
200✔
327
    when 1..8
328
      'ocm' + ('%08d' % oclc_num)
69✔
329
    when 9
330
      'ocn' + oclc_num
65✔
331
    else
332
      'on' + oclc_num
66✔
333
    end
334
  else
335
    oclc_num
133✔
336
  end
337
end
338

339
# Construct (or retrieve) the cache manager service
340
# @return [CacheManager] the cache manager service
341
def build_cache_manager(figgy_dir_path:)
1✔
342
  return @cache_manager unless @cache_manager.nil?
17✔
343

344
  figgy_lightly = Lightly.new(dir: figgy_dir_path, life: 0, hash: false)
3✔
345
  figgy_cache_adapter = CacheAdapter.new(service: figgy_lightly)
3✔
346

347
  CacheManager.initialize(figgy_cache: figgy_cache_adapter, logger:)
3✔
348

349
  @cache_manager = CacheManager.current
3✔
350
end
351

352
# returns hash of links ($u) (key),
353
# anchor text ($y, $3, hostname), and additional labels ($z) (array value)
354
# @param [MARC::Record] the MARC record being parsed
355
# @return [Hash] the values used to construct the links
356
def electronic_access_links(record, figgy_dir_path)
1✔
357
  solr_field_values = {}
182✔
358
  holding_856s = {}
182✔
359
  iiif_manifest_paths = {}
182✔
360

361
  output = []
182✔
362
  iiif_manifest_links = []
182✔
363
  fragment_index = 0
182✔
364

365
  Traject::MarcExtractor.cached('856').collect_matching_lines(record) do |field, _spec, _extractor|
182✔
366
    anchor_text = false
51✔
367
    z_label = false
51✔
368
    url_key = false
51✔
369
    holding_id = nil
51✔
370
    bib_id = record['001']
51✔
371

372
    electronic_access_link = ElectronicAccessLinkFactory.build bib_id: bib_id, marc_field: field
51✔
373

374
    # If the electronic access link is an ARK...
375
    if electronic_access_link.ark
51✔
376
      # ...and attempt to build an Orangelight URL from the (cached) mappings exposed by the repositories
377
      cache_manager = build_cache_manager(figgy_dir_path:)
17✔
378

379
      # Orangelight links
380
      catalog_url_builder = OrangelightUrlBuilder.new(ark_cache: cache_manager.ark_cache, fragment: fragment_value(fragment_index))
17✔
381
      orangelight_url = catalog_url_builder.build(url: electronic_access_link.ark)
17✔
382

383
      if orangelight_url
17✔
384
        # Index this by the domain for Orangelight
385
        anchor_text = electronic_access_link.anchor_text
6✔
386
        anchor_text = 'Digital content' if electronic_access_link.url&.host == electronic_access_link.anchor_text
6✔
387
        orangelight_link = electronic_access_link.clone url_key: orangelight_url.to_s, anchor_text: anchor_text
6✔
388
        # Only add the link to the current page if it resolves to a resource with a IIIF Manifest
389
        output << orangelight_link
6✔
390
      else
391
        # Otherwise, always add the link to the resource
392
        output << electronic_access_link
11✔
393
      end
394

395
      # Figgy URL's
396
      figgy_url_builder = IIIFManifestUrlBuilder.new(ark_cache: cache_manager.figgy_ark_cache, service_host: 'figgy.princeton.edu')
17✔
397
      figgy_iiif_manifest = figgy_url_builder.build(url: electronic_access_link.ark)
17✔
398
      if figgy_iiif_manifest
17✔
399
        figgy_iiif_manifest_link = electronic_access_link.clone url_key: figgy_iiif_manifest.to_s
6✔
400
        iiif_manifest_paths[electronic_access_link.url_key] = figgy_iiif_manifest_link.url.to_s
6✔
401
      end
402

403
    else
404
      # Always add links to the resource if it isn't an ARK
405
      output << electronic_access_link
34✔
406
    end
407

408
    output.each do |link|
51✔
409
      if link.holding_id
62✔
410
        holding_856s[link.holding_id] = { link.url_key => link.url_labels }
1✔
411
      elsif link.url_key && link.url_labels
61✔
412
        solr_field_values[link.url_key] = link.url_labels
57✔
413
      end
414
    end
415
    fragment_index += 1
51✔
416
  end
417

418
  solr_field_values['holding_record_856s'] = holding_856s unless holding_856s == {}
182✔
419
  solr_field_values['iiif_manifest_paths'] = iiif_manifest_paths unless iiif_manifest_paths.empty?
182✔
420
  solr_field_values
182✔
421
end
422

423
def fragment_value(fragment_index)
1✔
424
  if fragment_index == 0
17✔
425
    'view'
12✔
426
  else
427
    "view_#{fragment_index}"
5✔
428
  end
429
end
430

431
def remove_parens_035 standard_no
1✔
432
  standard_no.gsub(/^\(.*?\)/, '')
332✔
433
end
434

435
def everything_after_t record, fields
1✔
436
  values = []
662✔
437
  Traject::MarcExtractor.cached(fields).collect_matching_lines(record) do |field, _spec, _extractor|
662✔
438
    after_t = false
297✔
439
    title = []
297✔
440
    field.subfields.each do |s_field|
297✔
441
      title << s_field.value if after_t
991✔
442
      if s_field.code == 't'
991✔
443
        title << s_field.value
19✔
444
        after_t = true
19✔
445
      end
446
    end
447
    values << Traject::Macros::Marc21.trim_punctuation(title.join(' ')) unless title.empty?
297✔
448
  end
449
  values
662✔
450
end
451

452
def everything_after_t_alt_script record, fields
1✔
453
  values = []
331✔
454
  Traject::MarcExtractor.cached(fields).collect_matching_lines(record) do |field, _spec, _extractor|
331✔
455
    next unless field.tag == '880'
298✔
456

457
    after_t = false
67✔
458
    title = []
67✔
459
    field.subfields.each do |s_field|
67✔
460
      title << s_field.value if after_t
202✔
461
      if s_field.code == 't'
202✔
462
        title << s_field.value
3✔
463
        after_t = true
3✔
464
      end
465
    end
466
    values << Traject::Macros::Marc21.trim_punctuation(title.join(' ')) unless title.empty?
67✔
467
  end
468
  values
331✔
469
end
470

471
def everything_through_t record, fields
1✔
472
  values = []
166✔
473
  Traject::MarcExtractor.cached(fields).collect_matching_lines(record) do |field, _spec, _extractor|
166✔
474
    non_t = true
3✔
475
    title = []
3✔
476
    field.subfields.each do |s_field|
3✔
477
      title << s_field.value
6✔
478
      if s_field.code == 't'
6✔
479
        non_t = false
2✔
480
        break
2✔
481
      end
482
    end
483
    values << Traject::Macros::Marc21.trim_punctuation(title.join(' ')) unless (title.empty? || non_t)
3✔
484
  end
485
  values
166✔
486
end
487

488
##
489
# @param record [MARC::Record]
490
# @param fields [String] MARC fields of interest
491
# @return [Array] of name-titles each in an [Array], each element [String] split by hierarchy,
492
# both name ($a) and title ($t) are required
493
def prep_name_title record, fields
1✔
494
  values = []
494✔
495
  Traject::MarcExtractor.cached(fields).collect_matching_lines(record) do |field, spec, _extractor|
494✔
496
    name_title = []
128✔
497
    author = []
128✔
498
    non_a = true
128✔
499
    non_t = true
128✔
500
    field.subfields.each do |s_field|
128✔
501
      next if (!spec.subfields.nil? && !spec.subfields.include?(s_field.code))
438✔
502

503
      non_a = false if s_field.code == 'a'
197✔
504
      non_t = false if s_field.code == 't'
197✔
505
      if non_t
197✔
506
        author << s_field.value
172✔
507
      else
508
        name_title << s_field.value
25✔
509
      end
510
    end
511
    unless (non_a || non_t)
128✔
512
      name_title.unshift(author.join(' '))
15✔
513
      values << name_title unless name_title.empty?
15✔
514
    end
515
  end
516
  values
494✔
517
end
518

519
# @param fields [Array] with portions of hierarchy
520
# @return [Array] portions of hierarchy including previous elements
521
def expand_sublists_for_hierarchy(fields)
1✔
522
  fields.collect do |field|
186✔
523
    field.collect.with_index do |_v, index|
31✔
524
      Traject::Macros::Marc21.trim_punctuation(field[0..index].join(' '))
86✔
525
    end
526
  end
527
end
528

529
# @param fields [Array] with portions of hierarchy from name-titles or title-only fields
530
# @return [Array] portions of hierarchy including previous elements
531
def join_hierarchy(fields, include_first_element: false)
1✔
532
  if include_first_element == false
186✔
533
    # Exclude the name-only portion of hierarchy
534
    expand_sublists_for_hierarchy(fields).map { |a| a[1..-1] }
52✔
535
  else
536
    # Include full hierarchy
537
    expand_sublists_for_hierarchy(fields)
164✔
538
  end
539
end
540

541
# Removes empty call_number fields from holdings_1display
542
def remove_empty_call_number_fields(holding)
1✔
543
  holding.tap { |h| %w[call_number call_number_browse].map { |k| h.delete(k) if h.fetch(k, []).empty? } }
1,316✔
544
end
545

546
# Collects only non empty khi
547
def call_number_khi(field)
1✔
548
  field.subfields.reject { |s| s.value.empty? }.collect { |s| s if %w[k h i].include?(s.code) }.compact
6,144✔
549
end
550

551
# Alma Princeton item
552
def alma_code_start_22?(code)
1✔
553
  code.to_s.start_with?('22') && code.to_s.end_with?('06421')
5,642✔
554
end
555

556
def alma_code_start_53?(code)
1✔
557
  code.to_s.start_with?('53') && code.to_s.end_with?('06421')
199✔
558
end
559

560
def scsb_code_start?(code)
1✔
561
  code.to_s.start_with?('scsb')
430✔
562
end
563

564
def alma_852(record)
1✔
565
  record.fields('852').select { |f| alma_code_start_22?(f['8']) }
1,199✔
566
end
567

568
def scsb_852(record)
1✔
569
  record.fields('852').select { |f| scsb_code_start?(f['b']) }
448✔
570
end
571

572
def browse_fields(record, khi_key_order: %w[k h i])
1✔
573
  result = []
326✔
574
  fields = if scsb_doc?(record['001']&.value)
326✔
575
             scsb_852(record)
18✔
576
           else
577
             alma_852(record)
308✔
578
           end
579
  fields.each do |field|
326✔
580
    subfields = call_number_khi(field)
724✔
581
    next if subfields.empty?
724✔
582

583
    values = [field[khi_key_order[0]], field[khi_key_order[1]], field[khi_key_order[2]]].compact.reject(&:empty?)
704✔
584
    result << values.join(' ') if values.present?
704✔
585
  end
586
  result
326✔
587
end
588

589
def alma_876(record)
1✔
590
  record.fields('876').select { |f| alma_code_start_22?(f['0']) }
2,458✔
591
end
592

593
def alma_951_active(record)
1✔
594
  alma_951 = record.fields('951').select { |f| alma_code_start_53?(f['8']) }
525✔
595
  alma_951&.select { |f| f['a'] == 'Available' }
513✔
596
end
597

598
def alma_953(record)
1✔
599
  record.fields('953').select { |f| alma_code_start_53?(f['a']) }
186✔
600
end
601

602
def alma_954(record)
1✔
603
  record.fields('954').select { |f| alma_code_start_53?(f['a']) }
173✔
604
end
605

606
def alma_950(record)
1✔
607
  field_950_a = record.fields('950').select { |f| %w[true false].include?(f['a']) }
91✔
608
  field_950_a.map { |f| f['b'] }.first if field_950_a.present?
90✔
609
end
610

611
# SCSB item
612
# Keep this check with the alma_code? check
613
# until we make sure that the records in alma are updated
614
def scsb_doc?(record_id)
1✔
615
  /^SCSB-\d+/.match?(record_id)
6,162✔
616
end
617

618
def process_holdings(record)
1✔
619
  all_holdings = {}
164✔
620
  holdings_helpers = ProcessHoldingsHelpers.new(record:)
164✔
621
  holdings_helpers.fields_852_alma_or_scsb.each do |field_852|
164✔
622
    next if holdings_helpers.includes_only_private_scsb_items?(field_852)
363✔
623

624
    holding_id = holdings_helpers.holding_id(field_852)
360✔
625
    # Calculate the permanent holding
626
    holding = holdings_helpers.build_holding(field_852, permanent: true)
360✔
627
    items_by_holding = holdings_helpers.items_by_852(field_852)
360✔
628
    group_866_867_868_fields = holdings_helpers.group_866_867_868_on_holding_perm_id(holding_id, field_852)
360✔
629
    # if there are items (876 fields)
630
    if items_by_holding.present?
360✔
631
      add_permanent_items_to_holdings(items_by_holding, field_852, holdings_helpers, all_holdings, holding)
337✔
632
      add_temporary_items_to_holdings(items_by_holding, field_852, holdings_helpers, all_holdings)
337✔
633
    else
634
      # if there are no items (876 fields), create the holding by using the 852 field
635
      unless holding_id.nil? || invalid_location?(holding['location_code'])
23✔
636
        all_holdings[holding_id] = remove_empty_call_number_fields(holding)
22✔
637
      end
638
    end
639
    if all_holdings.present? && all_holdings[holding_id]
360✔
640
      all_holdings = holdings_helpers.process_866_867_868_fields(fields: group_866_867_868_fields, all_holdings:, holding_id:)
325✔
641
    end
642
  end
643
  all_holdings
164✔
644
end
645

646
def add_permanent_items_to_holdings(items_by_holding, field_852, holdings_helpers, all_holdings, holding)
1✔
647
  locations = holdings_helpers.select_permanent_location_876(items_by_holding, field_852)
337✔
648

649
  locations.each do |field_876|
337✔
650
    holding_key = holdings_helpers.holding_id(field_852)
439✔
651
    add_item_to_holding(field_852, field_876, holding_key, holdings_helpers, all_holdings, holding)
439✔
652
  end
653
end
654

655
def add_temporary_items_to_holdings(items_by_holding, field_852, holdings_helpers, all_holdings)
1✔
656
  locations = holdings_helpers.select_temporary_location_876(items_by_holding, field_852)
337✔
657

658
  locations.each do |field_876|
337✔
659
    next if holdings_helpers.includes_only_private_scsb_items?(field_852)
378✔
660

661
    if holdings_helpers.current_location_code(field_876) == 'RES_SHARE$IN_RS_REQ'
378✔
662
      holding = holdings_helpers.build_holding(field_852, permanent: true)
2✔
663
      holding_key = holdings_helpers.holding_id(field_852)
2✔
664
    else
665
      holding = holdings_helpers.build_holding(field_852, field_876, permanent: false)
376✔
666
      holding_key = holdings_helpers.current_location_code(field_876)
376✔
667
    end
668
    holding['temp_location_code'] = holdings_helpers.current_location_code(field_876)
378✔
669
    add_item_to_holding(field_852, field_876, holding_key, holdings_helpers, all_holdings, holding)
378✔
670
  end
671
end
672

673
def add_item_to_holding(field_852, field_876, holding_key, holdings_helpers, all_holdings, holding)
1✔
674
  item = holdings_helpers.build_item(field_852:, field_876:)
817✔
675
  if (holding_key.present? || !invalid_location?(holding['location_code'])) && all_holdings[holding_key].nil?
817✔
676
    all_holdings[holding_key] = remove_empty_call_number_fields(holding)
307✔
677
  end
678
  all_holdings = holdings_helpers.holding_items(value: holding_key, all_holdings:, item:)
817✔
679
end
680

681
def invalid_location?(code)
1✔
682
  Traject::TranslationMap.new('locations')[code].nil?
387✔
683
end
684

685
def process_recap_notes record
1✔
686
  item_notes = []
327✔
687
  partner_lib = nil
327✔
688
  Traject::MarcExtractor.cached('852').collect_matching_lines(record) do |field, _spec, _extractor|
327✔
689
    is_scsb = scsb_doc?(record['001'].value) && field['0']
773✔
690
    next unless is_scsb
773✔
691

692
    field.subfields.each do |s_field|
431✔
693
      if s_field.code == 'b'
1,338✔
694
        partner_lib = s_field.value # ||= Traject::TranslationMap.new("locations", :default => "__passthrough__")[s_field.value]
431✔
695
      end
696
    end
697
  end
698
  Traject::MarcExtractor.cached('87603ahjptxz').collect_matching_lines(record) do |field, _spec, _extractor|
327✔
699
    is_scsb = scsb_doc?(record['001'].value) && field['0']
1,639✔
700
    next unless is_scsb
1,639✔
701

702
    col_group = ''
471✔
703
    field.subfields.each do |s_field|
471✔
704
      if s_field.code == 'x'
4,273✔
705
        if s_field.value == 'Shared'
471✔
706
          col_group = 'S'
6✔
707
        elsif s_field.value == 'Private'
465✔
708
          col_group = 'P'
5✔
709
        elsif s_field.value == 'Committed'
460✔
710
          col_group = 'C'
2✔
711
        elsif s_field.value == 'Uncommittable'
458✔
712
          col_group = 'U'
2✔
713
        else
714
          col_group = 'O'
456✔
715
        end
716
      end
717
    end
718
    if partner_lib == 'scsbnypl'
471✔
719
      partner_display_string = 'N'
452✔
720
    elsif partner_lib == 'scsbcul'
19✔
721
      partner_display_string = 'C'
2✔
722
    elsif partner_lib == 'scsbhl'
17✔
723
      partner_display_string = 'H'
17✔
724
    end
725
    item_notes << "#{partner_display_string} - #{col_group}"
471✔
726
  end
727
  item_notes
327✔
728
end
729

730
def local_heading?(field)
1✔
731
  field.any? { |subfield| subfield.code == '2' && subfield.value == 'local' } &&
2,439✔
732
    field.any? { |subfield| subfield.code == '5' && subfield.value == 'NjP' }
112✔
733
end
734

735
def siku_heading?(field)
1✔
736
  any_thesaurus_match? field, %w[sk skbb]
506✔
737
end
738

739
def any_thesaurus_match?(field, thesauri)
1✔
740
  field.any? { |subfield| subfield.code == '2' && thesauri.include?(subfield.value) }
5,698✔
741
end
742

743
def valid_linked_fields(record, field_tag, accumulator)
1✔
744
  accumulator.concat LinkedFieldsExtractor.new(record, field_tag).mms_ids
330✔
745
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