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

pulibrary / bibdata / 1dcebae2-3318-4e77-bc53-82276e293354

02 May 2025 04:45PM UTC coverage: 28.256% (-63.9%) from 92.189%
1dcebae2-3318-4e77-bc53-82276e293354

push

circleci

sandbergja
Add basic infrastructure for compiling rust code

* Add a rake compile task to compile
* Run the rake task in CI
* Run the rake task before rspec tests with the rust tag, to provide quick feedback on rust changes in TDD cycles

2 of 7 new or added lines in 2 files covered. (28.57%)

2467 existing lines in 97 files now uncovered.

1089 of 3854 relevant lines covered (28.26%)

0.29 hits per line

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

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

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

59
    def date_from_008
1✔
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 << LibraryStandardNumbers::ISBN.normalize(s_field.value)
×
147
      end
UNCOV
148
      if (field.tag == '022') || ((field.tag == '776') && (s_field.code == 'x'))
×
UNCOV
149
        linked_nums << LibraryStandardNumbers::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_hierarchy_per_field(record, fields)
1✔
UNCOV
291
  split_on_subfield = %w[t v x y z]
×
UNCOV
292
  Traject::MarcExtractor.cached(fields).collect_matching_lines(record) do |field, spec, extractor|
×
UNCOV
293
    include_heading = block_given? ? yield(field) : true
×
UNCOV
294
    next unless include_heading && extractor.collect_subfields(field, spec).first
×
295

UNCOV
296
    hierarchical_heading = HierarchicalHeading.new(field:, spec:, split_on_subfield:).to_s
×
297

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

303
def accumulate_subheading(heading_split_on_separator)
1✔
UNCOV
304
  heading_split_on_separator.reduce([]) do |accumulator, subheading|
×
305
    # accumulator.last ? "#{accumulator.last}#{SEPARATOR}#{subsubject}" : subsubject
UNCOV
306
    accumulator.append([accumulator.last, subheading].compact.join(SEPARATOR))
×
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✔
UNCOV
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|
×
UNCOV
314
    subject = extractor.collect_subfields(field, spec).first
×
UNCOV
315
    unless subject.nil?
×
UNCOV
316
      hierarchical_string = HierarchicalHeading.new(field:, spec:, split_on_subfield: %w[x z]).to_s
×
UNCOV
317
      hierarchical_string.split(SEPARATOR)
×
318
    end
319
  end.compact
UNCOV
320
  other_thesaurus_subjects = Traject::MarcExtractor.cached('650|*7|abcxz').collect_matching_lines(record) do |field, spec, extractor|
×
UNCOV
321
    subject = extractor.collect_subfields(field, spec).first
×
UNCOV
322
    should_include = siku_heading?(field) || local_heading?(field) || any_thesaurus_match?(field, %w[homoit])
×
UNCOV
323
    if should_include && !subject.nil?
×
UNCOV
324
      hierarchical_string = HierarchicalHeading.new(field:, spec:, split_on_subfield: %w[x z]).to_s
×
UNCOV
325
      hierarchical_string.split(SEPARATOR)
×
326
    end
327
  end.flatten.compact
UNCOV
328
  lcsh_subjects + other_thesaurus_subjects
×
329
end
330

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

335
def oclc_number? oclc
1✔
336
  # Strip spaces and dashes
UNCOV
337
  clean_oclc = oclc.gsub(/[\-\s]/, '')
×
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)
UNCOV
340
  clean_oclc.match(/\(OCoLC\)(ocn|ocm|on)*\d+/) != nil
×
341
end
342

343
def oclc_normalize oclc, opts = { prefix: false }
1✔
UNCOV
344
  oclc_num = strip_non_numeric(oclc)
×
UNCOV
345
  if opts[:prefix] == true
×
UNCOV
346
    case oclc_num.length
×
347
    when 1..8
UNCOV
348
      'ocm' + ('%08d' % oclc_num)
×
349
    when 9
UNCOV
350
      'ocn' + oclc_num
×
351
    else
UNCOV
352
      'on' + oclc_num
×
353
    end
354
  else
UNCOV
355
    oclc_num
×
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✔
UNCOV
362
  return @cache_manager unless @cache_manager.nil?
×
363

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

UNCOV
367
  CacheManager.initialize(figgy_cache: figgy_cache_adapter, logger:)
×
368

UNCOV
369
  @cache_manager = CacheManager.current
×
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✔
UNCOV
377
  solr_field_values = {}
×
UNCOV
378
  holding_856s = {}
×
UNCOV
379
  iiif_manifest_paths = {}
×
380

UNCOV
381
  output = []
×
UNCOV
382
  iiif_manifest_links = []
×
UNCOV
383
  fragment_index = 0
×
384

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

UNCOV
392
    electronic_access_link = ElectronicAccessLinkFactory.build bib_id: bib_id, marc_field: field
×
393

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

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

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

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

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

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

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

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

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

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

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

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

491
def everything_through_t record, fields
1✔
UNCOV
492
  values = []
×
UNCOV
493
  Traject::MarcExtractor.cached(fields).collect_matching_lines(record) do |field, _spec, _extractor|
×
UNCOV
494
    non_t = true
×
UNCOV
495
    title = []
×
UNCOV
496
    field.subfields.each do |s_field|
×
UNCOV
497
      title << s_field.value
×
UNCOV
498
      if s_field.code == 't'
×
UNCOV
499
        non_t = false
×
UNCOV
500
        break
×
501
      end
502
    end
UNCOV
503
    values << Traject::Macros::Marc21.trim_punctuation(title.join(' ')) unless (title.empty? || non_t)
×
504
  end
UNCOV
505
  values
×
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✔
UNCOV
514
  values = []
×
UNCOV
515
  Traject::MarcExtractor.cached(fields).collect_matching_lines(record) do |field, spec, _extractor|
×
UNCOV
516
    name_title = []
×
UNCOV
517
    author = []
×
UNCOV
518
    non_a = true
×
UNCOV
519
    non_t = true
×
UNCOV
520
    field.subfields.each do |s_field|
×
UNCOV
521
      next if (!spec.subfields.nil? && !spec.subfields.include?(s_field.code))
×
522

UNCOV
523
      non_a = false if s_field.code == 'a'
×
UNCOV
524
      non_t = false if s_field.code == 't'
×
UNCOV
525
      if non_t
×
UNCOV
526
        author << s_field.value
×
527
      else
UNCOV
528
        name_title << s_field.value
×
529
      end
530
    end
UNCOV
531
    unless (non_a || non_t)
×
UNCOV
532
      name_title.unshift(author.join(' '))
×
UNCOV
533
      values << name_title unless name_title.empty?
×
534
    end
535
  end
UNCOV
536
  values
×
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✔
UNCOV
542
  fields.collect do |field|
×
UNCOV
543
    field.collect.with_index do |_v, index|
×
UNCOV
544
      Traject::Macros::Marc21.trim_punctuation(field[0..index].join(' '))
×
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✔
UNCOV
552
  if include_first_element == false
×
553
    # Exclude the name-only portion of hierarchy
UNCOV
554
    expand_sublists_for_hierarchy(fields).map { |a| a[1..-1] }
×
555
  else
556
    # Include full hierarchy
UNCOV
557
    expand_sublists_for_hierarchy(fields)
×
558
  end
559
end
560

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

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

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

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

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

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

588
def scsb_852(record)
1✔
UNCOV
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✔
UNCOV
593
  result = []
×
UNCOV
594
  fields = if scsb_doc?(record['001']&.value)
×
UNCOV
595
             scsb_852(record)
×
596
           else
UNCOV
597
             alma_852(record)
×
598
           end
UNCOV
599
  fields.each do |field|
×
UNCOV
600
    subfields = call_number_khi(field)
×
UNCOV
601
    next if subfields.empty?
×
602

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

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

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

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

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

626
def alma_950(record)
1✔
UNCOV
627
  field_950_a = record.fields('950').select { |f| %w[true false].include?(f['a']) }
×
UNCOV
628
  field_950_a.map { |f| f['b'] }.first if field_950_a.present?
×
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✔
UNCOV
635
  /^SCSB-\d+/.match?(record_id)
×
636
end
637

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

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

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

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

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

UNCOV
678
  locations.each do |field_876|
×
UNCOV
679
    next if holdings_helpers.includes_only_private_scsb_items?(field_852)
×
680

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

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

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

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

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

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

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

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

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

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