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

pulibrary / bibdata / 929a1b49-3757-46f6-b188-4af515cd83e4

12 Mar 2025 06:28PM UTC coverage: 89.757% (-2.4%) from 92.185%
929a1b49-3757-46f6-b188-4af515cd83e4

push

circleci

christinach
Add new facet fields that will be used for the subject browse table and the facet search
Helps with vocabulary work https://github.com/pulibrary/orangelight/pull/3386

rbgenr_genre_facet
aat_genre_facet
lcgft_genre_facet
homoit_genre_facet
homoit_subject_facet
local_subject_facet

3365 of 3749 relevant lines covered (89.76%)

265.68 hits per line

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

86.77
/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
×
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']
66✔
61
        d = self['008'].value[7, 4]
56✔
62
        d = d.tr 'u', '0' unless d == 'uuuu'
56✔
63
        d = d.tr ' ', '0' unless d == '    '
56✔
64
        d if /^[0-9]{4}$/.match?(d)
56✔
65
      end
66
    end
67

68
    def end_date_from_008
1✔
69
      if self['008']
36✔
70
        d = self['008'].value[11, 4]
31✔
71
        d = d.tr 'u', '9' unless d == 'uuuu'
31✔
72
        d = d.tr ' ', '9' unless d == '    '
31✔
73
        d if /^[0-9]{4}$/.match?(d)
31✔
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
9✔
88
  when '0' then 'International Standard Recording Code'
×
89
  when '1' then 'Universal Product Code'
1✔
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 = {}
31✔
119
  Traject::MarcExtractor.cached('024').collect_matching_lines(record) do |field, _spec, _extractor|
31✔
120
    standard_label = map_024_indicators_to_labels(field.indicator1)
9✔
121
    standard_number = nil
9✔
122
    field.subfields.each do |s_field|
9✔
123
      standard_number = s_field.value if s_field.code == 'a'
11✔
124
      if (s_field.code == '2') && (standard_label == '$2')
11✔
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'
9✔
129
    unless standard_number.nil?
9✔
130
      standard_no[standard_label] ? standard_no[standard_label] << standard_number : standard_no[standard_label] = [standard_number]
9✔
131
    end
132
  end
133
  standard_no
31✔
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 = []
31✔
143
  Traject::MarcExtractor.cached('020az:022alyz:035a:776wxz:787w').collect_matching_lines(record) do |field, _spec, _extractor|
31✔
144
    field.subfields.each do |s_field|
115✔
145
      if (field.tag == '020') || ((field.tag == '776') && (s_field.code == 'z'))
182✔
146
        linked_nums << StdNum::ISBN.normalize(s_field.value)
54✔
147
      end
148
      if (field.tag == '022') || ((field.tag == '776') && (s_field.code == 'x'))
182✔
149
        linked_nums << StdNum::ISSN.normalize(s_field.value)
7✔
150
      end
151
      linked_nums << oclc_normalize(s_field.value, prefix: true) if (field.tag == '035') && oclc_number?(s_field.value)
182✔
152
      if ((field.tag == '776') && (s_field.code == 'w')) || ((field.tag == '787') && (s_field.code == 'w'))
182✔
153
        linked_nums << oclc_normalize(s_field.value, prefix: true) if oclc_number?(s_field.value)
7✔
154
        linked_nums << ('BIB' + strip_non_numeric(s_field.value)) unless s_field.value.include?('(')
7✔
155
        if s_field.value.include?('(') && !s_field.value.start_with?('(')
7✔
156
          logger.error "#{record['001']} - linked field formatting: #{s_field.value}"
×
157
        end
158
      end
159
    end
160
  end
161
  linked_nums.compact.uniq
31✔
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|
34✔
167
    name = extractor.collect_subfields(field, spec).first
59✔
168
    unless name.nil?
59✔
169
      remove = ''
59✔
170
      after_t = false
59✔
171
      field.subfields.each do |s_field|
59✔
172
        remove << " #{s_field.value}" if after_t && spec.includes_subfield_code?(s_field.code)
227✔
173
        after_t = true if s_field.code == 't'
227✔
174
      end
175
      name = name.chomp(remove)
59✔
176
      Traject::Macros::Marc21.trim_punctuation(name)
59✔
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 = []
34✔
184
  Traject::MarcExtractor.cached('100aqbcdk:110abcdfgkln:111abcdfgklnpq:700aqbcdk:710abcdfgkln:711abcdfgklnpq').collect_matching_lines(record) do |field, spec, extractor|
34✔
185
    next unless field.tag == '880'
59✔
186

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

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

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

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

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

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

286
    HierarchicalHeading.new(field:, spec:, split_on_subfield:).to_s
266✔
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]
246✔
292
  Traject::MarcExtractor.cached(fields).collect_matching_lines(record) do |field, spec, extractor|
246✔
293
    include_heading = block_given? ? yield(field) : true
167✔
294
    next unless include_heading && extractor.collect_subfields(field, spec).first
165✔
295

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

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

303
def accumulate_subheading(heading_split_on_separator)
1✔
304
  heading_split_on_separator.reduce([]) do |accumulator, subheading|
66✔
305
    # accumulator.last ? "#{accumulator.last}#{SEPARATOR}#{subsubject}" : subsubject
306
    accumulator.append([accumulator.last, subheading].compact.join(SEPARATOR))
129✔
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|
31✔
314
    subject = extractor.collect_subfields(field, spec).first
51✔
315
    unless subject.nil?
51✔
316
      hierarchical_string = HierarchicalHeading.new(field:, spec:, split_on_subfield: %w[x z]).to_s
51✔
317
      hierarchical_string.split(SEPARATOR)
51✔
318
    end
319
  end.compact
320
  other_thesaurus_subjects = Traject::MarcExtractor.cached('650|*7|abcxz').collect_matching_lines(record) do |field, spec, extractor|
31✔
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
31✔
329
end
330

331
def strip_non_numeric num_str
1✔
332
  num_str.gsub(/\D/, '').to_i.to_s
65✔
333
end
334

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

343
def oclc_normalize oclc, opts = { prefix: false }
1✔
344
  oclc_num = strip_non_numeric(oclc)
62✔
345
  if opts[:prefix] == true
62✔
346
    case oclc_num.length
36✔
347
    when 1..8
348
      'ocm' + ('%08d' % oclc_num)
15✔
349
    when 9
350
      'ocn' + oclc_num
8✔
351
    else
352
      'on' + oclc_num
13✔
353
    end
354
  else
355
    oclc_num
26✔
356
  end
357
end
358

359
# Construct (or retrieve) the cache manager service
360
# @return [CacheManager] the cache manager service
361
def build_cache_manager(figgy_dir_path:)
1✔
362
  return @cache_manager unless @cache_manager.nil?
6✔
363

364
  figgy_lightly = Lightly.new(dir: figgy_dir_path, life: 0, hash: false)
2✔
365
  figgy_cache_adapter = CacheAdapter.new(service: figgy_lightly)
2✔
366

367
  CacheManager.initialize(figgy_cache: figgy_cache_adapter, logger:)
2✔
368

369
  @cache_manager = CacheManager.current
2✔
370
end
371

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

381
  output = []
50✔
382
  iiif_manifest_links = []
50✔
383
  fragment_index = 0
50✔
384

385
  Traject::MarcExtractor.cached('856').collect_matching_lines(record) do |field, _spec, _extractor|
50✔
386
    anchor_text = false
26✔
387
    z_label = false
26✔
388
    url_key = false
26✔
389
    holding_id = nil
26✔
390
    bib_id = record['001']
26✔
391

392
    electronic_access_link = ElectronicAccessLinkFactory.build bib_id: bib_id, marc_field: field
26✔
393

394
    # If the electronic access link is an ARK...
395
    if electronic_access_link.ark
26✔
396
      # ...and attempt to build an Orangelight URL from the (cached) mappings exposed by the repositories
397
      cache_manager = build_cache_manager(figgy_dir_path:)
6✔
398

399
      # Orangelight links
400
      catalog_url_builder = OrangelightUrlBuilder.new(ark_cache: cache_manager.ark_cache, fragment: fragment_value(fragment_index))
6✔
401
      orangelight_url = catalog_url_builder.build(url: electronic_access_link.ark)
6✔
402

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

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

423
    else
424
      # Always add links to the resource if it isn't an ARK
425
      output << electronic_access_link
20✔
426
    end
427

428
    output.each do |link|
26✔
429
      if link.holding_id
33✔
430
        holding_856s[link.holding_id] = { link.url_key => link.url_labels }
1✔
431
      elsif link.url_key && link.url_labels
32✔
432
        solr_field_values[link.url_key] = link.url_labels
28✔
433
      end
434
    end
435
    fragment_index += 1
26✔
436
  end
437

438
  solr_field_values['holding_record_856s'] = holding_856s unless holding_856s == {}
50✔
439
  solr_field_values['iiif_manifest_paths'] = iiif_manifest_paths unless iiif_manifest_paths.empty?
50✔
440
  solr_field_values
50✔
441
end
442

443
def fragment_value(fragment_index)
1✔
444
  if fragment_index == 0
6✔
445
    'view'
2✔
446
  else
447
    "view_#{fragment_index}"
4✔
448
  end
449
end
450

451
def remove_parens_035 standard_no
1✔
452
  standard_no.gsub(/^\(.*?\)/, '')
70✔
453
end
454

455
def everything_after_t record, fields
1✔
456
  values = []
134✔
457
  Traject::MarcExtractor.cached(fields).collect_matching_lines(record) do |field, _spec, _extractor|
134✔
458
    after_t = false
61✔
459
    title = []
61✔
460
    field.subfields.each do |s_field|
61✔
461
      title << s_field.value if after_t
233✔
462
      if s_field.code == 't'
233✔
463
        title << s_field.value
5✔
464
        after_t = true
5✔
465
      end
466
    end
467
    values << Traject::Macros::Marc21.trim_punctuation(title.join(' ')) unless title.empty?
61✔
468
  end
469
  values
134✔
470
end
471

472
def everything_after_t_alt_script record, fields
1✔
473
  values = []
67✔
474
  Traject::MarcExtractor.cached(fields).collect_matching_lines(record) do |field, _spec, _extractor|
67✔
475
    next unless field.tag == '880'
60✔
476

477
    after_t = false
9✔
478
    title = []
9✔
479
    field.subfields.each do |s_field|
9✔
480
      title << s_field.value if after_t
32✔
481
      if s_field.code == 't'
32✔
482
        title << s_field.value
1✔
483
        after_t = true
1✔
484
      end
485
    end
486
    values << Traject::Macros::Marc21.trim_punctuation(title.join(' ')) unless title.empty?
9✔
487
  end
488
  values
67✔
489
end
490

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

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

523
      non_a = false if s_field.code == 'a'
52✔
524
      non_t = false if s_field.code == 't'
52✔
525
      if non_t
52✔
526
        author << s_field.value
40✔
527
      else
528
        name_title << s_field.value
12✔
529
      end
530
    end
531
    unless (non_a || non_t)
26✔
532
      name_title.unshift(author.join(' '))
4✔
533
      values << name_title unless name_title.empty?
4✔
534
    end
535
  end
536
  values
93✔
537
end
538

539
# @param fields [Array] with portions of hierarchy
540
# @return [Array] portions of hierarchy including previous elements
541
def expand_sublists_for_hierarchy(fields)
1✔
542
  fields.collect do |field|
39✔
543
    field.collect.with_index do |_v, index|
11✔
544
      Traject::Macros::Marc21.trim_punctuation(field[0..index].join(' '))
35✔
545
    end
546
  end
547
end
548

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

561
# Removes empty call_number fields from holdings_1display
562
def remove_empty_call_number_fields(holding)
1✔
563
  holding.tap { |h| %w[call_number call_number_browse].map { |k| h.delete(k) if h.fetch(k, []).empty? } }
208✔
564
end
565

566
# Collects only non empty khi
567
def call_number_khi(field)
1✔
568
  field.subfields.reject { |s| s.value.empty? }.collect { |s| s if %w[k h i].include?(s.code) }.compact
1,242✔
569
end
570

571
# Alma Princeton item
572
def alma_code_start_22?(code)
1✔
573
  code.to_s.start_with?('22') && code.to_s.end_with?('06421')
1,186✔
574
end
575

576
def alma_code_start_53?(code)
1✔
577
  code.to_s.start_with?('53') && code.to_s.end_with?('06421')
14✔
578
end
579

580
def scsb_code_start?(code)
1✔
581
  code.to_s.start_with?('scsb')
×
582
end
583

584
def alma_852(record)
1✔
585
  record.fields('852').select { |f| alma_code_start_22?(f['8']) }
279✔
586
end
587

588
def scsb_852(record)
1✔
589
  record.fields('852').select { |f| scsb_code_start?(f['b']) }
×
590
end
591

592
def browse_fields(record, khi_key_order: %w[k h i])
1✔
593
  result = []
60✔
594
  fields = if scsb_doc?(record['001']&.value)
60✔
595
             scsb_852(record)
×
596
           else
597
             alma_852(record)
60✔
598
           end
599
  fields.each do |field|
60✔
600
    subfields = call_number_khi(field)
116✔
601
    next if subfields.empty?
116✔
602

603
    values = [field[khi_key_order[0]], field[khi_key_order[1]], field[khi_key_order[2]]].compact.reject(&:empty?)
114✔
604
    result << values.join(' ') if values.present?
114✔
605
  end
606
  result
60✔
607
end
608

609
def alma_876(record)
1✔
610
  record.fields('876').select { |f| alma_code_start_22?(f['0']) }
440✔
611
end
612

613
def alma_951_active(record)
1✔
614
  alma_951 = record.fields('951').select { |f| alma_code_start_53?(f['8']) }
79✔
615
  alma_951&.select { |f| f['a'] == 'Available' }
79✔
616
end
617

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

622
def alma_954(record)
1✔
623
  record.fields('954').select { |f| alma_code_start_53?(f['a']) }
30✔
624
end
625

626
def alma_950(record)
1✔
627
  field_950_a = record.fields('950').select { |f| %w[true false].include?(f['a']) }
17✔
628
  field_950_a.map { |f| f['b'] }.first if field_950_a.present?
17✔
629
end
630

631
# SCSB item
632
# Keep this check with the alma_code? check
633
# until we make sure that the records in alma are updated
634
def scsb_doc?(record_id)
1✔
635
  /^SCSB-\d+/.match?(record_id)
738✔
636
end
637

638
def process_holdings(record)
1✔
639
  all_holdings = {}
30✔
640
  holdings_helpers = ProcessHoldingsHelpers.new(record:)
30✔
641
  holdings_helpers.fields_852_alma_or_scsb.each do |field_852|
30✔
642
    next if holdings_helpers.includes_only_private_scsb_items?(field_852)
58✔
643

644
    holding_id = holdings_helpers.holding_id(field_852)
58✔
645
    # Calculate the permanent holding
646
    holding = holdings_helpers.build_holding(field_852, permanent: true)
58✔
647
    items_by_holding = holdings_helpers.items_by_852(field_852)
58✔
648
    group_866_867_868_fields = holdings_helpers.group_866_867_868_on_holding_perm_id(holding_id, field_852)
58✔
649
    # if there are items (876 fields)
650
    if items_by_holding.present?
58✔
651
      add_permanent_items_to_holdings(items_by_holding, field_852, holdings_helpers, all_holdings, holding)
52✔
652
      add_temporary_items_to_holdings(items_by_holding, field_852, holdings_helpers, all_holdings)
52✔
653
    else
654
      # if there are no items (876 fields), create the holding by using the 852 field
655
      unless holding_id.nil? || invalid_location?(holding['location_code'])
6✔
656
        all_holdings[holding_id] = remove_empty_call_number_fields(holding)
5✔
657
      end
658
    end
659
    if all_holdings.present? && all_holdings[holding_id]
58✔
660
      all_holdings = holdings_helpers.process_866_867_868_fields(fields: group_866_867_868_fields, all_holdings:, holding_id:)
52✔
661
    end
662
  end
663
  all_holdings
30✔
664
end
665

666
def add_permanent_items_to_holdings(items_by_holding, field_852, holdings_helpers, all_holdings, holding)
1✔
667
  locations = holdings_helpers.select_permanent_location_876(items_by_holding, field_852)
52✔
668

669
  locations.each do |field_876|
52✔
670
    holding_key = holdings_helpers.holding_id(field_852)
104✔
671
    add_item_to_holding(field_852, field_876, holding_key, holdings_helpers, all_holdings, holding)
104✔
672
  end
673
end
674

675
def add_temporary_items_to_holdings(items_by_holding, field_852, holdings_helpers, all_holdings)
1✔
676
  locations = holdings_helpers.select_temporary_location_876(items_by_holding, field_852)
52✔
677

678
  locations.each do |field_876|
52✔
679
    next if holdings_helpers.includes_only_private_scsb_items?(field_852)
14✔
680

681
    if holdings_helpers.current_location_code(field_876) == 'RES_SHARE$IN_RS_REQ'
14✔
682
      holding = holdings_helpers.build_holding(field_852, permanent: true)
×
683
      holding_key = holdings_helpers.holding_id(field_852)
×
684
    else
685
      holding = holdings_helpers.build_holding(field_852, field_876, permanent: false)
14✔
686
      holding_key = holdings_helpers.current_location_code(field_876)
14✔
687
    end
688
    holding['temp_location_code'] = holdings_helpers.current_location_code(field_876)
14✔
689
    add_item_to_holding(field_852, field_876, holding_key, holdings_helpers, all_holdings, holding)
14✔
690
  end
691
end
692

693
def add_item_to_holding(field_852, field_876, holding_key, holdings_helpers, all_holdings, holding)
1✔
694
  item = holdings_helpers.build_item(field_852:, field_876:)
118✔
695
  if (holding_key.present? || !invalid_location?(holding['location_code'])) && all_holdings[holding_key].nil?
118✔
696
    all_holdings[holding_key] = remove_empty_call_number_fields(holding)
47✔
697
  end
698
  all_holdings = holdings_helpers.holding_items(value: holding_key, all_holdings:, item:)
118✔
699
end
700

701
def invalid_location?(code)
1✔
702
  Traject::TranslationMap.new('locations')[code].nil?
20✔
703
end
704

705
def process_recap_notes record
1✔
706
  item_notes = []
60✔
707
  partner_lib = nil
60✔
708
  Traject::MarcExtractor.cached('852').collect_matching_lines(record) do |field, _spec, _extractor|
60✔
709
    is_scsb = scsb_doc?(record['001'].value) && field['0']
126✔
710
    next unless is_scsb
126✔
711

712
    field.subfields.each do |s_field|
×
713
      if s_field.code == 'b'
×
714
        partner_lib = s_field.value # ||= Traject::TranslationMap.new("locations", :default => "__passthrough__")[s_field.value]
×
715
      end
716
    end
717
  end
718
  Traject::MarcExtractor.cached('87603ahjptxz').collect_matching_lines(record) do |field, _spec, _extractor|
60✔
719
    is_scsb = scsb_doc?(record['001'].value) && field['0']
236✔
720
    next unless is_scsb
236✔
721

722
    col_group = ''
×
723
    field.subfields.each do |s_field|
×
724
      if s_field.code == 'x'
×
725
        if s_field.value == 'Shared'
×
726
          col_group = 'S'
×
727
        elsif s_field.value == 'Private'
×
728
          col_group = 'P'
×
729
        elsif s_field.value == 'Committed'
×
730
          col_group = 'C'
×
731
        elsif s_field.value == 'Uncommittable'
×
732
          col_group = 'U'
×
733
        else
734
          col_group = 'O'
×
735
        end
736
      end
737
    end
738
    if partner_lib == 'scsbnypl'
×
739
      partner_display_string = 'N'
×
740
    elsif partner_lib == 'scsbcul'
×
741
      partner_display_string = 'C'
×
742
    elsif partner_lib == 'scsbhl'
×
743
      partner_display_string = 'H'
×
744
    end
745
    item_notes << "#{partner_display_string} - #{col_group}"
×
746
  end
747
  item_notes
60✔
748
end
749

750
def local_heading?(field)
1✔
751
  field.any? { |subfield| subfield.code == '2' && subfield.value == 'local' } &&
162✔
752
    field.any? { |subfield| subfield.code == '5' && subfield.value == 'NjP' }
×
753
end
754

755
def siku_heading?(field)
1✔
756
  any_thesaurus_match? field, %w[sk skbb]
26✔
757
end
758

759
def any_thesaurus_match?(field, thesauri)
1✔
760
  field.any? { |subfield| subfield.code == '2' && thesauri.include?(subfield.value) }
1,044✔
761
end
762

763
def valid_linked_fields(record, field_tag, accumulator)
1✔
764
  accumulator.concat LinkedFieldsExtractor.new(record, field_tag).mms_ids
66✔
765
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