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

pulibrary / bibdata / f04bc944-f9b4-4a42-8b26-dcacd0e3e688

11 Mar 2025 10:27PM UTC coverage: 34.017% (-58.1%) from 92.162%
f04bc944-f9b4-4a42-8b26-dcacd0e3e688

Pull #2653

circleci

christinach
Add new lc_subject_facet field.
Helps with the vocabulary work https://github.com/pulibrary/orangelight/pull/3386
In this new field we index only the lc subject heading and the subdivisions
So that when the user searches using the Details section, they can query solr for
all the subject headings and their divisions.

This is needed for the Subject browse Vocabulary work.
example: "lc_subject_facet": [
             "Booksellers and bookselling—Italy—Directories",
             "Booksellers and bookselling-Italy",
             "Booksellers and bookselling"
              ]
Pull Request #2653: Add new lc_subject_facet field.

1 of 3 new or added lines in 1 file covered. (33.33%)

2215 existing lines in 93 files now uncovered.

1294 of 3804 relevant lines covered (34.02%)

0.99 hits per line

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

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

68
    def end_date_from_008
1✔
UNCOV
69
      if self['008']
×
UNCOV
70
        d = self['008'].value[11, 4]
×
UNCOV
71
        d = d.tr 'u', '9' unless d == 'uuuu'
×
UNCOV
72
        d = d.tr ' ', '9' unless d == '    '
×
UNCOV
73
        d if /^[0-9]{4}$/.match?(d)
×
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✔
UNCOV
87
  case i
×
88
  when '0' then 'International Standard Recording Code'
×
UNCOV
89
  when '1' then 'Universal Product Code'
×
90
  when '2' then 'International Standard Music Number'
×
UNCOV
91
  when '3' then 'International Article Number'
×
UNCOV
92
  when '4' then 'Serial Item and Contribution Identifier'
×
UNCOV
93
  when '7' then '$2'
×
UNCOV
94
  else FALLBACK_STANDARD_NO
×
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✔
UNCOV
113
  key = subfield_value.capitalize.gsub(/[[:punct:]]?$/, '')
×
UNCOV
114
  key.empty? ? fallback : key
×
115
end
116

117
def standard_no_hash record
1✔
UNCOV
118
  standard_no = {}
×
UNCOV
119
  Traject::MarcExtractor.cached('024').collect_matching_lines(record) do |field, _spec, _extractor|
×
UNCOV
120
    standard_label = map_024_indicators_to_labels(field.indicator1)
×
UNCOV
121
    standard_number = nil
×
UNCOV
122
    field.subfields.each do |s_field|
×
UNCOV
123
      standard_number = s_field.value if s_field.code == 'a'
×
UNCOV
124
      if (s_field.code == '2') && (standard_label == '$2')
×
UNCOV
125
        standard_label = subfield_specified_hash_key(s_field.value, FALLBACK_STANDARD_NO)
×
126
      end
127
    end
UNCOV
128
    standard_label = FALLBACK_STANDARD_NO if standard_label == '$2'
×
UNCOV
129
    unless standard_number.nil?
×
UNCOV
130
      standard_no[standard_label] ? standard_no[standard_label] << standard_number : standard_no[standard_label] = [standard_number]
×
131
    end
132
  end
UNCOV
133
  standard_no
×
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✔
UNCOV
142
  linked_nums = []
×
UNCOV
143
  Traject::MarcExtractor.cached('020az:022alyz:035a:776wxz:787w').collect_matching_lines(record) do |field, _spec, _extractor|
×
UNCOV
144
    field.subfields.each do |s_field|
×
UNCOV
145
      if (field.tag == '020') || ((field.tag == '776') && (s_field.code == 'z'))
×
UNCOV
146
        linked_nums << StdNum::ISBN.normalize(s_field.value)
×
147
      end
UNCOV
148
      if (field.tag == '022') || ((field.tag == '776') && (s_field.code == 'x'))
×
UNCOV
149
        linked_nums << StdNum::ISSN.normalize(s_field.value)
×
150
      end
UNCOV
151
      linked_nums << oclc_normalize(s_field.value, prefix: true) if (field.tag == '035') && oclc_number?(s_field.value)
×
UNCOV
152
      if ((field.tag == '776') && (s_field.code == 'w')) || ((field.tag == '787') && (s_field.code == 'w'))
×
UNCOV
153
        linked_nums << oclc_normalize(s_field.value, prefix: true) if oclc_number?(s_field.value)
×
UNCOV
154
        linked_nums << ('BIB' + strip_non_numeric(s_field.value)) unless s_field.value.include?('(')
×
UNCOV
155
        if s_field.value.include?('(') && !s_field.value.start_with?('(')
×
156
          logger.error "#{record['001']} - linked field formatting: #{s_field.value}"
×
157
        end
158
      end
159
    end
160
  end
UNCOV
161
  linked_nums.compact.uniq
×
162
end
163

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

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

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

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

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

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

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

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

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

290
def accumulate_subheading(heading_split_on_separator)
1✔
NEW
291
  heading_split_on_separator.reduce([]) do |accumulator, subheading|
×
292
    # accumulator.last ? "#{accumulator.last}#{SEPARATOR}#{subsubject}" : subsubject
NEW
293
    accumulator.append([accumulator.last, subheading].compact.join(SEPARATOR))
×
294
  end
295
end
296

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

318
def strip_non_numeric num_str
1✔
UNCOV
319
  num_str.gsub(/\D/, '').to_i.to_s
×
320
end
321

322
def oclc_number? oclc
1✔
323
  # Strip spaces and dashes
UNCOV
324
  clean_oclc = oclc.gsub(/[\-\s]/, '')
×
325
  # Ensure it follows the OCLC standard
326
  # (see https://help.oclc.org/Metadata_Services/WorldShare_Collection_Manager/Data_sync_collections/Prepare_your_data/30035_field_and_OCLC_control_numbers)
UNCOV
327
  clean_oclc.match(/\(OCoLC\)(ocn|ocm|on)*\d+/) != nil
×
328
end
329

330
def oclc_normalize oclc, opts = { prefix: false }
1✔
UNCOV
331
  oclc_num = strip_non_numeric(oclc)
×
UNCOV
332
  if opts[:prefix] == true
×
UNCOV
333
    case oclc_num.length
×
334
    when 1..8
UNCOV
335
      'ocm' + ('%08d' % oclc_num)
×
336
    when 9
UNCOV
337
      'ocn' + oclc_num
×
338
    else
UNCOV
339
      'on' + oclc_num
×
340
    end
341
  else
UNCOV
342
    oclc_num
×
343
  end
344
end
345

346
# Construct (or retrieve) the cache manager service
347
# @return [CacheManager] the cache manager service
348
def build_cache_manager(figgy_dir_path:)
1✔
UNCOV
349
  return @cache_manager unless @cache_manager.nil?
×
350

UNCOV
351
  figgy_lightly = Lightly.new(dir: figgy_dir_path, life: 0, hash: false)
×
UNCOV
352
  figgy_cache_adapter = CacheAdapter.new(service: figgy_lightly)
×
353

UNCOV
354
  CacheManager.initialize(figgy_cache: figgy_cache_adapter, logger:)
×
355

UNCOV
356
  @cache_manager = CacheManager.current
×
357
end
358

359
# returns hash of links ($u) (key),
360
# anchor text ($y, $3, hostname), and additional labels ($z) (array value)
361
# @param [MARC::Record] the MARC record being parsed
362
# @return [Hash] the values used to construct the links
363
def electronic_access_links(record, figgy_dir_path)
1✔
UNCOV
364
  solr_field_values = {}
×
UNCOV
365
  holding_856s = {}
×
UNCOV
366
  iiif_manifest_paths = {}
×
367

UNCOV
368
  output = []
×
UNCOV
369
  iiif_manifest_links = []
×
UNCOV
370
  fragment_index = 0
×
371

UNCOV
372
  Traject::MarcExtractor.cached('856').collect_matching_lines(record) do |field, _spec, _extractor|
×
UNCOV
373
    anchor_text = false
×
UNCOV
374
    z_label = false
×
UNCOV
375
    url_key = false
×
UNCOV
376
    holding_id = nil
×
UNCOV
377
    bib_id = record['001']
×
378

UNCOV
379
    electronic_access_link = ElectronicAccessLinkFactory.build bib_id: bib_id, marc_field: field
×
380

381
    # If the electronic access link is an ARK...
UNCOV
382
    if electronic_access_link.ark
×
383
      # ...and attempt to build an Orangelight URL from the (cached) mappings exposed by the repositories
UNCOV
384
      cache_manager = build_cache_manager(figgy_dir_path:)
×
385

386
      # Orangelight links
UNCOV
387
      catalog_url_builder = OrangelightUrlBuilder.new(ark_cache: cache_manager.ark_cache, fragment: fragment_value(fragment_index))
×
UNCOV
388
      orangelight_url = catalog_url_builder.build(url: electronic_access_link.ark)
×
389

UNCOV
390
      if orangelight_url
×
391
        # Index this by the domain for Orangelight
UNCOV
392
        anchor_text = electronic_access_link.anchor_text
×
UNCOV
393
        anchor_text = 'Digital content' if electronic_access_link.url&.host == electronic_access_link.anchor_text
×
UNCOV
394
        orangelight_link = electronic_access_link.clone url_key: orangelight_url.to_s, anchor_text: anchor_text
×
395
        # Only add the link to the current page if it resolves to a resource with a IIIF Manifest
UNCOV
396
        output << orangelight_link
×
397
      else
398
        # Otherwise, always add the link to the resource
UNCOV
399
        output << electronic_access_link
×
400
      end
401

402
      # Figgy URL's
UNCOV
403
      figgy_url_builder = IIIFManifestUrlBuilder.new(ark_cache: cache_manager.figgy_ark_cache, service_host: 'figgy.princeton.edu')
×
UNCOV
404
      figgy_iiif_manifest = figgy_url_builder.build(url: electronic_access_link.ark)
×
UNCOV
405
      if figgy_iiif_manifest
×
UNCOV
406
        figgy_iiif_manifest_link = electronic_access_link.clone url_key: figgy_iiif_manifest.to_s
×
UNCOV
407
        iiif_manifest_paths[electronic_access_link.url_key] = figgy_iiif_manifest_link.url.to_s
×
408
      end
409

410
    else
411
      # Always add links to the resource if it isn't an ARK
UNCOV
412
      output << electronic_access_link
×
413
    end
414

UNCOV
415
    output.each do |link|
×
UNCOV
416
      if link.holding_id
×
UNCOV
417
        holding_856s[link.holding_id] = { link.url_key => link.url_labels }
×
UNCOV
418
      elsif link.url_key && link.url_labels
×
UNCOV
419
        solr_field_values[link.url_key] = link.url_labels
×
420
      end
421
    end
UNCOV
422
    fragment_index += 1
×
423
  end
424

UNCOV
425
  solr_field_values['holding_record_856s'] = holding_856s unless holding_856s == {}
×
UNCOV
426
  solr_field_values['iiif_manifest_paths'] = iiif_manifest_paths unless iiif_manifest_paths.empty?
×
UNCOV
427
  solr_field_values
×
428
end
429

430
def fragment_value(fragment_index)
1✔
UNCOV
431
  if fragment_index == 0
×
UNCOV
432
    'view'
×
433
  else
UNCOV
434
    "view_#{fragment_index}"
×
435
  end
436
end
437

438
def remove_parens_035 standard_no
1✔
UNCOV
439
  standard_no.gsub(/^\(.*?\)/, '')
×
440
end
441

442
def everything_after_t record, fields
1✔
UNCOV
443
  values = []
×
UNCOV
444
  Traject::MarcExtractor.cached(fields).collect_matching_lines(record) do |field, _spec, _extractor|
×
UNCOV
445
    after_t = false
×
UNCOV
446
    title = []
×
UNCOV
447
    field.subfields.each do |s_field|
×
UNCOV
448
      title << s_field.value if after_t
×
UNCOV
449
      if s_field.code == 't'
×
UNCOV
450
        title << s_field.value
×
UNCOV
451
        after_t = true
×
452
      end
453
    end
UNCOV
454
    values << Traject::Macros::Marc21.trim_punctuation(title.join(' ')) unless title.empty?
×
455
  end
UNCOV
456
  values
×
457
end
458

459
def everything_after_t_alt_script record, fields
1✔
UNCOV
460
  values = []
×
UNCOV
461
  Traject::MarcExtractor.cached(fields).collect_matching_lines(record) do |field, _spec, _extractor|
×
UNCOV
462
    next unless field.tag == '880'
×
463

UNCOV
464
    after_t = false
×
UNCOV
465
    title = []
×
UNCOV
466
    field.subfields.each do |s_field|
×
UNCOV
467
      title << s_field.value if after_t
×
UNCOV
468
      if s_field.code == 't'
×
UNCOV
469
        title << s_field.value
×
UNCOV
470
        after_t = true
×
471
      end
472
    end
UNCOV
473
    values << Traject::Macros::Marc21.trim_punctuation(title.join(' ')) unless title.empty?
×
474
  end
UNCOV
475
  values
×
476
end
477

478
def everything_through_t record, fields
1✔
UNCOV
479
  values = []
×
UNCOV
480
  Traject::MarcExtractor.cached(fields).collect_matching_lines(record) do |field, _spec, _extractor|
×
UNCOV
481
    non_t = true
×
UNCOV
482
    title = []
×
UNCOV
483
    field.subfields.each do |s_field|
×
UNCOV
484
      title << s_field.value
×
UNCOV
485
      if s_field.code == 't'
×
UNCOV
486
        non_t = false
×
UNCOV
487
        break
×
488
      end
489
    end
UNCOV
490
    values << Traject::Macros::Marc21.trim_punctuation(title.join(' ')) unless (title.empty? || non_t)
×
491
  end
UNCOV
492
  values
×
493
end
494

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

UNCOV
510
      non_a = false if s_field.code == 'a'
×
UNCOV
511
      non_t = false if s_field.code == 't'
×
UNCOV
512
      if non_t
×
UNCOV
513
        author << s_field.value
×
514
      else
UNCOV
515
        name_title << s_field.value
×
516
      end
517
    end
UNCOV
518
    unless (non_a || non_t)
×
UNCOV
519
      name_title.unshift(author.join(' '))
×
UNCOV
520
      values << name_title unless name_title.empty?
×
521
    end
522
  end
UNCOV
523
  values
×
524
end
525

526
# @param fields [Array] with portions of hierarchy
527
# @return [Array] portions of hierarchy including previous elements
528
def expand_sublists_for_hierarchy(fields)
1✔
UNCOV
529
  fields.collect do |field|
×
UNCOV
530
    field.collect.with_index do |_v, index|
×
UNCOV
531
      Traject::Macros::Marc21.trim_punctuation(field[0..index].join(' '))
×
532
    end
533
  end
534
end
535

536
# @param fields [Array] with portions of hierarchy from name-titles or title-only fields
537
# @return [Array] portions of hierarchy including previous elements
538
def join_hierarchy(fields, include_first_element: false)
1✔
UNCOV
539
  if include_first_element == false
×
540
    # Exclude the name-only portion of hierarchy
UNCOV
541
    expand_sublists_for_hierarchy(fields).map { |a| a[1..-1] }
×
542
  else
543
    # Include full hierarchy
UNCOV
544
    expand_sublists_for_hierarchy(fields)
×
545
  end
546
end
547

548
# Removes empty call_number fields from holdings_1display
549
def remove_empty_call_number_fields(holding)
1✔
UNCOV
550
  holding.tap { |h| %w[call_number call_number_browse].map { |k| h.delete(k) if h.fetch(k, []).empty? } }
×
551
end
552

553
# Collects only non empty khi
554
def call_number_khi(field)
1✔
UNCOV
555
  field.subfields.reject { |s| s.value.empty? }.collect { |s| s if %w[k h i].include?(s.code) }.compact
×
556
end
557

558
# Alma Princeton item
559
def alma_code_start_22?(code)
1✔
UNCOV
560
  code.to_s.start_with?('22') && code.to_s.end_with?('06421')
×
561
end
562

563
def alma_code_start_53?(code)
1✔
UNCOV
564
  code.to_s.start_with?('53') && code.to_s.end_with?('06421')
×
565
end
566

567
def scsb_code_start?(code)
1✔
UNCOV
568
  code.to_s.start_with?('scsb')
×
569
end
570

571
def alma_852(record)
1✔
UNCOV
572
  record.fields('852').select { |f| alma_code_start_22?(f['8']) }
×
573
end
574

575
def scsb_852(record)
1✔
UNCOV
576
  record.fields('852').select { |f| scsb_code_start?(f['b']) }
×
577
end
578

579
def browse_fields(record, khi_key_order: %w[k h i])
1✔
UNCOV
580
  result = []
×
UNCOV
581
  fields = if scsb_doc?(record['001']&.value)
×
UNCOV
582
             scsb_852(record)
×
583
           else
UNCOV
584
             alma_852(record)
×
585
           end
UNCOV
586
  fields.each do |field|
×
UNCOV
587
    subfields = call_number_khi(field)
×
UNCOV
588
    next if subfields.empty?
×
589

UNCOV
590
    values = [field[khi_key_order[0]], field[khi_key_order[1]], field[khi_key_order[2]]].compact.reject(&:empty?)
×
UNCOV
591
    result << values.join(' ') if values.present?
×
592
  end
UNCOV
593
  result
×
594
end
595

596
def alma_876(record)
1✔
UNCOV
597
  record.fields('876').select { |f| alma_code_start_22?(f['0']) }
×
598
end
599

600
def alma_951_active(record)
1✔
UNCOV
601
  alma_951 = record.fields('951').select { |f| alma_code_start_53?(f['8']) }
×
UNCOV
602
  alma_951&.select { |f| f['a'] == 'Available' }
×
603
end
604

605
def alma_953(record)
1✔
UNCOV
606
  record.fields('953').select { |f| alma_code_start_53?(f['a']) }
×
607
end
608

609
def alma_954(record)
1✔
UNCOV
610
  record.fields('954').select { |f| alma_code_start_53?(f['a']) }
×
611
end
612

613
def alma_950(record)
1✔
UNCOV
614
  field_950_a = record.fields('950').select { |f| %w[true false].include?(f['a']) }
×
UNCOV
615
  field_950_a.map { |f| f['b'] }.first if field_950_a.present?
×
616
end
617

618
# SCSB item
619
# Keep this check with the alma_code? check
620
# until we make sure that the records in alma are updated
621
def scsb_doc?(record_id)
1✔
UNCOV
622
  /^SCSB-\d+/.match?(record_id)
×
623
end
624

625
def process_holdings(record)
1✔
UNCOV
626
  all_holdings = {}
×
UNCOV
627
  holdings_helpers = ProcessHoldingsHelpers.new(record:)
×
UNCOV
628
  holdings_helpers.fields_852_alma_or_scsb.each do |field_852|
×
UNCOV
629
    next if holdings_helpers.includes_only_private_scsb_items?(field_852)
×
630

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

653
def add_permanent_items_to_holdings(items_by_holding, field_852, holdings_helpers, all_holdings, holding)
1✔
UNCOV
654
  locations = holdings_helpers.select_permanent_location_876(items_by_holding, field_852)
×
655

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

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

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

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

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

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

692
def process_recap_notes record
1✔
UNCOV
693
  item_notes = []
×
UNCOV
694
  partner_lib = nil
×
UNCOV
695
  Traject::MarcExtractor.cached('852').collect_matching_lines(record) do |field, _spec, _extractor|
×
UNCOV
696
    is_scsb = scsb_doc?(record['001'].value) && field['0']
×
UNCOV
697
    next unless is_scsb
×
698

UNCOV
699
    field.subfields.each do |s_field|
×
UNCOV
700
      if s_field.code == 'b'
×
UNCOV
701
        partner_lib = s_field.value # ||= Traject::TranslationMap.new("locations", :default => "__passthrough__")[s_field.value]
×
702
      end
703
    end
704
  end
UNCOV
705
  Traject::MarcExtractor.cached('87603ahjptxz').collect_matching_lines(record) do |field, _spec, _extractor|
×
UNCOV
706
    is_scsb = scsb_doc?(record['001'].value) && field['0']
×
UNCOV
707
    next unless is_scsb
×
708

UNCOV
709
    col_group = ''
×
UNCOV
710
    field.subfields.each do |s_field|
×
UNCOV
711
      if s_field.code == 'x'
×
UNCOV
712
        if s_field.value == 'Shared'
×
UNCOV
713
          col_group = 'S'
×
UNCOV
714
        elsif s_field.value == 'Private'
×
UNCOV
715
          col_group = 'P'
×
UNCOV
716
        elsif s_field.value == 'Committed'
×
UNCOV
717
          col_group = 'C'
×
UNCOV
718
        elsif s_field.value == 'Uncommittable'
×
UNCOV
719
          col_group = 'U'
×
720
        else
UNCOV
721
          col_group = 'O'
×
722
        end
723
      end
724
    end
UNCOV
725
    if partner_lib == 'scsbnypl'
×
UNCOV
726
      partner_display_string = 'N'
×
UNCOV
727
    elsif partner_lib == 'scsbcul'
×
UNCOV
728
      partner_display_string = 'C'
×
UNCOV
729
    elsif partner_lib == 'scsbhl'
×
UNCOV
730
      partner_display_string = 'H'
×
731
    end
UNCOV
732
    item_notes << "#{partner_display_string} - #{col_group}"
×
733
  end
UNCOV
734
  item_notes
×
735
end
736

737
def local_heading?(field)
1✔
UNCOV
738
  field.any? { |subfield| subfield.code == '2' && subfield.value == 'local' } &&
×
UNCOV
739
    field.any? { |subfield| subfield.code == '5' && subfield.value == 'NjP' }
×
740
end
741

742
def siku_heading?(field)
1✔
UNCOV
743
  any_thesaurus_match? field, %w[sk skbb]
×
744
end
745

746
def any_thesaurus_match?(field, thesauri)
1✔
UNCOV
747
  field.any? { |subfield| subfield.code == '2' && thesauri.include?(subfield.value) }
×
748
end
749

750
def valid_linked_fields(record, field_tag, accumulator)
1✔
UNCOV
751
  accumulator.concat LinkedFieldsExtractor.new(record, field_tag).mms_ids
×
752
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