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

pulibrary / marc_cleanup / 40336464-761e-46bf-9575-a4d368cc6c53

21 May 2026 07:34PM UTC coverage: 97.548% (-2.5%) from 100.0%
40336464-761e-46bf-9575-a4d368cc6c53

Pull #206

circleci

mzelesky
further tests for 880 errors
Pull Request #206: 880 error tests and fixes

50 of 99 new or added lines in 2 files covered. (50.51%)

49 existing lines in 1 file now uncovered.

1949 of 1998 relevant lines covered (97.55%)

5.68 hits per line

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

87.81
/lib/marc_cleanup/variable_fields.rb
1
# frozen_string_literal: true
2

3
module MarcCleanup
1✔
4
  BLANK_REGEX = /^.*[[:blank:]]{2,}.*$|^.*[[:blank:]]+$|^[[:blank:]]+(.*)$/
1✔
5
  ### Remove non-numerical strings and append a new 020$q with the string
6
  def new_020_q(record)
1✔
7
    record.fields('020').each do |f020|
1✔
8
      f020.subfields.each do |subfield|
1✔
9
        next unless subfield.code == 'a'
2✔
10

11
        isbn_parts = /^\s*([\d-]+)\s*(\(.*?\))\s*$/.match(subfield.value)
1✔
12
        next if isbn_parts.nil?
1✔
13

14
        subfield.value = isbn_parts[1]
1✔
15
        f020.append(MARC::Subfield.new('q', isbn_parts[2]))
1✔
16
      end
17
    end
18
    record
1✔
19
  end
20

21
  ### Convert ISBN-10 to ISBN-13
22
  def isbn10_to_isbn13(isbn)
1✔
23
    stem = isbn[0..8]
2✔
24
    return nil if stem =~ /\D/
2✔
25

26
    existing_check = isbn[9]
2✔
27
    return nil if existing_check && existing_check != checkdigit_isbn10(stem)
2✔
28

29
    main = ISBN13PREFIX + stem
2✔
30
    checkdigit = checkdigit_isbn13(main)
2✔
31
    main + checkdigit
2✔
32
  end
33

34
  ### Calculate check digit for ISBN-10
35
  def checkdigit_isbn10(stem)
1✔
36
    int_sum = 0
1✔
37
    stem.each_char.with_index do |char, index|
1✔
38
      int_sum += char.to_i * (10 - index)
9✔
39
    end
40
    mod = (11 - (int_sum % 11)) % 11
1✔
41
    mod == 10 ? 'X' : mod.to_s
1✔
42
  end
43

44
  ### Calculate check digit for ISBN-13
45
  def checkdigit_isbn13(stem)
1✔
46
    int_sum = 0
4✔
47
    stem.each_char.with_index do |char, index|
4✔
48
      digit = char.to_i
48✔
49
      int_sum += index.even? ? digit : digit * 3
48✔
50
    end
51
    ((10 - (int_sum % 10)) % 10).to_s
4✔
52
  end
53

54
  ### Normalize ISBN-13
55
  def isbn13_normalize(raw_isbn)
1✔
56
    stem = raw_isbn[0..11]
2✔
57
    return nil if stem =~ /\D/
2✔
58

59
    checkdigit = checkdigit_isbn13(stem)
2✔
60
    if raw_isbn[12] && raw_isbn[12] != checkdigit
2✔
61
      nil
62
    else
63
      stem + checkdigit
1✔
64
    end
65
  end
66

67
  def initial_clean_isbn(isbn)
1✔
68
    isbn.delete('-')
4✔
69
        .delete('\\')
70
        .gsub(/\([^)]*\)/, '')
71
        .gsub(/^(.*)\$[cq].*$/, '\1')
72
        .gsub(/^\D+([0-9].*)$/, '\1')
73
  end
74

75
  def clean_isbn_13digit_vs_10digit(isbn)
1✔
76
    if isbn =~ /^978/
4✔
77
      isbn.gsub(/^(978[0-9 ]+).*$/, '\1')
2✔
78
          .delete(' ')
79
    else
80
      isbn.gsub(/([0-9])\s*([0-9]{4})\s*([0-9]{4})\s*([0-9xX]).*$/, '\1\2\3\4')
2✔
81
    end
82
  end
83

84
  def clean_isbn(isbn)
1✔
85
    new_isbn = initial_clean_isbn(isbn)
4✔
86
    new_isbn = clean_isbn_13digit_vs_10digit(new_isbn)
4✔
87
    new_isbn = new_isbn.gsub(/^([0-9]{9,13}[xX]?)[^0-9xX].*$/, '\1')
4✔
88
                       .gsub(/^([0-9]+?)\D.*$/, '\1')
89
    if new_isbn.length.between?(7, 8) && new_isbn =~ /^[0-9]+$/
4✔
90
      new_isbn.ljust(9, '0')
1✔
91
    else
92
      new_isbn
3✔
93
    end
94
  end
95

96
  ### Normalize any given string that is supposed to include an ISBN
97
  def isbn_normalize(isbn)
1✔
98
    return nil unless isbn
4✔
99

100
    clean_isbn = clean_isbn(isbn)
4✔
101
    valid_lengths = [9, 10, 12, 13] # ISBN10 and ISBN13 with/out check digits
4✔
102
    return nil unless valid_lengths.include? clean_isbn.length
4✔
103

104
    if clean_isbn.length < 12
4✔
105
      isbn10_to_isbn13(clean_isbn)
2✔
106
    else
107
      isbn13_normalize(clean_isbn)
2✔
108
    end
109
  end
110

111
  def modify_invalid_isbn_subfield(subfield)
1✔
112
    normalized_isbn = isbn_normalize(subfield.value)
4✔
113
    if normalized_isbn
4✔
114
      MARC::Subfield.new(subfield.code, normalized_isbn)
3✔
115
    else
116
      MARC::Subfield.new('z', subfield.value)
1✔
117
    end
118
  end
119

120
  ### If the ISBN is invalid, change the subfield code to z
121
  ### Otherwise, replace ISBN with normalized ISBN
122
  def move_invalid_isbn(record)
1✔
123
    record.fields('020').each do |field|
4✔
124
      field.subfields.each do |subfield|
4✔
125
        next unless subfield.code == 'a'
4✔
126

127
        new_subfield = modify_invalid_isbn_subfield(subfield)
4✔
128
        subfield.value = new_subfield.value
4✔
129
        subfield.code = new_subfield.code
4✔
130
      end
131
    end
132
    record
4✔
133
  end
134

135
  # check the 041 field for errors
136
  # 041 is a language code
137
  def f041_errors?(record)
1✔
138
    f041 = record.fields('041')
1✔
139
    return false if f041.empty?
1✔
140

141
    f041.each do |field|
1✔
142
      field.subfields.each do |subfield|
1✔
143
        val = subfield.value
1✔
144
        return true if (val.size > 3) && (val.size % 3).zero?
1✔
145
      end
146
    end
147
    false
1✔
148
  end
149

150
  # http://www.loc.gov/standards/valuelist/marcauthen.html
151
  def auth_codes_f042
1✔
152
    %w[
3✔
153
      anuc croatica dc dhca dlr
154
      gamma gils gnd1 gnd2 gnd3 gnd4 gnd5 gnd6 gnd7 gndz isds/c issnuk
155
      lacderived lc lcac lccopycat lccopycat-nm lcd lcderive
156
      lchlas lcllh lcnccp lcnitrate lcnuc lcode
157
      msc natgaz nbr nlc nlmcopyc norbibl nsdp nst ntccf nznb
158
      pcc premarc reveal sanb scipio toknb
159
      ukblcatcopy ukblderived ukblproject ukblsr ukscp
160
      xisds/c xissnuk xlc xnlc xnsdp
161
    ]
162
  end
163

164
  def auth_code_error?(record)
1✔
165
    f042 = record.fields('042')
4✔
166
    return false if f042.empty?
4✔
167
    return true if f042.size > 1
3✔
168

169
    f042.first.subfields.each do |subfield|
2✔
170
      next if subfield.code != 'a'
3✔
171
      return true unless auth_codes_f042.include?(subfield.value)
3✔
172
    end
173
    false
1✔
174
  end
175

176
  def empty_subfields?(record)
1✔
177
    record.fields.each do |field|
2✔
178
      next unless field.instance_of?(MARC::DataField)
2✔
179

180
      field.subfields.each do |subfield|
2✔
181
        return true if subfield.value =~ /^[[:blank:]]*$/
3✔
182
      end
183
    end
184
    false
1✔
185
  end
186

187
  def extra_space_f880_f533(record)
1✔
188
    record.fields('880').select do |field|
13✔
189
      field['6'] =~ /^533/ &&
4✔
190
        field.subfields.any? do |subfield|
191
          subfield.code != '7' && subfield.value =~ BLANK_REGEX
4✔
192
        end
193
    end
194
  end
195

196
  def extra_space_f533(record)
1✔
197
    fields = record.fields('533').select do |field|
13✔
198
      field.subfields.any? do |subfield|
2✔
199
        subfield.code != '7' && subfield.value =~ BLANK_REGEX
4✔
200
      end
201
    end
202
    fields + extra_space_f880_f533(record)
13✔
203
  end
204

205
  def extra_space_f880_f76x_f830(record)
1✔
206
    record.fields('880').select do |field|
12✔
207
      field['6'] &&
3✔
208
        ('760'..'830').include?(field['6'][0..2]) &&
3✔
209
        field.subfields.any? do |subfield|
210
          subfield.code != '7' && subfield.value =~ BLANK_REGEX
2✔
211
        end
212
    end
213
  end
214

215
  def extra_space_f76x_f830(record)
1✔
216
    fields = record.fields('760'..'830').select do |field|
12✔
217
      field.subfields.any? do |subfield|
4✔
218
        !%w[w 7].include?(subfield.code) && subfield.value =~ BLANK_REGEX
4✔
219
      end
220
    end
221
    fields + extra_space_f880_f76x_f830(record)
12✔
222
  end
223

224
  def extra_space_f880_other_fields(record)
1✔
225
    tag_regex = /[1-469]..|0[2-9].|01[1-9]|7[0-5].|5[0-24-9].|53[0-24-9]/
10✔
226
    record.fields('880').select do |field|
10✔
227
      field['6'] &&
2✔
228
        field['6'][0..2] =~ tag_regex &&
229
        field.subfields.any? do |subfield|
230
          subfield.value =~ BLANK_REGEX
2✔
231
        end
232
    end
233
  end
234

235
  def extra_space_other_fields(record)
1✔
236
    tag_regex = /[1-469]..|0[2-9].|01[1-9]|7[0-5].|5[0-24-9].|53[0-24-9]/
10✔
237
    fields = record.fields.select do |field|
10✔
238
      field.tag =~ tag_regex &&
15✔
239
        field.subfields.any? do |subfield|
240
          subfield.value =~ BLANK_REGEX
7✔
241
        end
242
    end
243
    fields + extra_space_f880_other_fields(record)
10✔
244
  end
245

246
  def extra_spaces?(record)
1✔
247
    return true if extra_space_f533(record).size.positive?
7✔
248
    return true if extra_space_f76x_f830(record).size.positive?
6✔
249

250
    extra_space_other_fields(record).size.positive?
4✔
251
  end
252

253
  def extra_space_gsub(string)
1✔
254
    string.gsub!(/([[:blank:]]){2,}/, '\1')
6✔
255
    string.gsub!(/^(.*)[[:blank:]]+$/, '\1')
6✔
256
    string.gsub(/^[[:blank:]]+(.*)$/, '\1')
6✔
257
  end
258

259
  def extra_space_fix_field(field:, skip_subfields: [])
1✔
260
    field.subfields.each do |subfield|
5✔
261
      next if skip_subfields.include?(subfield.code)
6✔
262
      next unless subfield.value
6✔
263

264
      subfield.value = extra_space_gsub(subfield.value.dup)
6✔
265
    end
266
    field
5✔
267
  end
268

269
  ### Remove extra spaces from all fields that are not positionally defined
270
  def extra_space_fix(record)
1✔
271
    extra_space_f533(record).each do |field|
6✔
272
      extra_space_fix_field(field: field, skip_subfields: %w[7])
1✔
273
    end
274
    extra_space_f76x_f830(record).each do |field|
6✔
275
      extra_space_fix_field(field: field, skip_subfields: %w[w 7])
1✔
276
    end
277
    extra_space_other_fields(record).each do |field|
6✔
278
      extra_space_fix_field(field: field)
3✔
279
    end
280
    record
6✔
281
  end
282

283
  def multiple_no_040?(record)
1✔
284
    record.fields('040').size != 1
4✔
285
  end
286

287
  def multiple_no_040b?(record)
1✔
288
    return true if multiple_no_040?(record)
3✔
289

290
    f040b = record['040'].subfields.select { |subfield| subfield.code == 'b' }
7✔
291
    return true if f040b.size != 1
3✔
292

293
    f040b.first.value.match?(/^\s*$/)
2✔
294
  end
295

296
  def f046_errors?(record)
1✔
297
    subf_codes = %w[b c d e]
4✔
298
    subf_a_values = %w[r s p t x q n i k r m t x n]
4✔
299
    f046 = record.fields('046')
4✔
300
    f046.any? do |field|
4✔
301
      codes = field.subfields.map(&:code)
3✔
302
      field_a = field['a']
3✔
303
      (field_a && !subf_a_values.include?(field_a)) ||
3✔
304
        (field_a.to_s.empty? && subf_codes.intersect?(codes))
2✔
305
    end
306
  end
307

308
  def multiple_no_f245?(record)
1✔
309
    record.fields('245').size != 1
27✔
310
  end
311

312
  def missing_040c?(record)
1✔
313
    return true unless record['040'] && record['040']['c']
2✔
314

315
    false
1✔
316
  end
317

318
  ### Scenario 1: $6 value contains spaces in 880 field or variable field:
319
  ###   Remove all space characters
320
  ### Scenario 2: One 264 field exists with $6, and one 880 field with $6 260:
321
  ###   Change $6 value in 880 to 264
322
  ### Scenario 3: One 260 field exists, and one 880 field with $6 264:
323
  ###   Change $6 value in 880 to 260
324
  ### Scenario 4: Variable field has $6, and there is no corresponding 880 with the field tag:
325
  ###   Remove $6 value in variable field
326
  ### Scenario 5: One 880 field has $6 for a tag, and there is one corresponding variable field with no $6:
327
  ###   Add $6 value to variable field
328
  ### Scenario 6: 880 field has a tag in $6 that is not represented in a variable field:
329
  ###   Change sequence number to '00' to make it an unlinked 880
330
  ### Scenario 7: One $6 per tag in non-880 fields, and one $6 per tag in 880 fields:
331
  ###   Reorder sequence of 880s; if there is a $6 in a variable field and no corresponding 880, reserve
332
  ###   that field sequence for the variable field
333
  def fix_f880(record)
1✔
NEW
UNCOV
334
    fix_subf6_spaces(record)
×
NEW
UNCOV
335
    fix_f880_26x(record)
×
NEW
UNCOV
336
    fix_f880_subf6_variable_fields(record)
×
NEW
UNCOV
337
    fix_f880_subf6_paired_fields(record)
×
NEW
UNCOV
338
    fix_f880_unlinked_f880(record)
×
NEW
UNCOV
339
    resequence_f880_pairs(record)
×
340
  end
341

342
  ### Scenario 1: $6 value contains spaces in 880 field or variable field:
343
  ###   Remove all space characters
344
  def fix_subf6_spaces(record)
1✔
NEW
UNCOV
345
    target_fields = record.fields('010'..'899').select { |field| field['6'] =~ /\s/ }
×
NEW
UNCOV
346
    target_fields.each do |field|
×
NEW
UNCOV
347
      record.fields[field_index].subfields.find { |subfield| subfield.code == '6' }.value = field['6'].gsub(/\s/, '')
×
348
    end
349
  end
350

351
  ### Scenario 2: One 264 field exists with $6, and one 880 field with $6 260:
352
  ###   Change $6 value in 880 to 264
353
  ### Scenario 3: One 260 field exists, and one 880 field with $6 264:
354
  ###   Change $6 value in 880 to 260
355
  def fix_f880_26x(record)
1✔
NEW
UNCOV
356
    return record if blocked_f880_26x?(record)
×
357

NEW
UNCOV
358
    f880_26x = record.fields('880').find { |field| field['6'] =~ /^26[04]/ }
×
NEW
UNCOV
359
    field_index = record.fields.index(f880_26x)
×
NEW
UNCOV
360
    record.fields[field_index].subfields.find do |subfield|
×
NEW
UNCOV
361
      subfield.code == '6'
×
362
    end.value = "#{f26x_tag(record)}-#{extra_subf6_info(f880_26x)}"
NEW
UNCOV
363
    record
×
364
  end
365

366
  def f26x_tag(record)
1✔
NEW
UNCOV
367
    record.fields(%w[260 264]).find { |field| field['6'] =~ /^880/ }.tag
×
368
  end
369

370
  def blocked_f880_26x?(record)
1✔
NEW
UNCOV
371
    f26x = record.fields(%w[260 264]).select { |field| field['6'] =~ /^880/ }
×
NEW
UNCOV
372
    f880_26x = record.fields('880').select { |field| field['6'] =~ /^26[04]/ }
×
NEW
UNCOV
373
    f26x.size != 1 || f880_26x.size != 1
×
374
  end
375

376
  ### Scenario 4: Variable field has $6, and there is no corresponding 880 with the field tag:
377
  ###   Remove $6 value in variable field
378
  def fix_f880_subf6_variable_fields(record)
1✔
NEW
UNCOV
379
    linked_field_subf6 = linked_field_pairings(record).map { |tag| tag[0..2] }.uniq
×
NEW
UNCOV
380
    f880_subf6 = f880_pairings(record).map { |tag| tag[0..2] }.uniq
×
NEW
UNCOV
381
    unmatched_variable_subf6 = linked_field_subf6 - f880_subf6
×
NEW
UNCOV
382
    unmatched_variable_subf6.each do |tag|
×
NEW
UNCOV
383
      remove_subf6_per_tag(tag, record)
×
384
    end
NEW
UNCOV
385
    record
×
386
  end
387

388
  def remove_subf6_per_tag(tag, record)
1✔
NEW
UNCOV
389
    record.fields(tag).each do |field|
×
NEW
UNCOV
390
      field.subfields.delete_if { |subfield| subfield.code == '6' }
×
391
    end
392
  end
393

394
  ### Scenario 5: One 880 field has $6 for a tag, and there is one corresponding variable field with no $6:
395
  ###   Add $6 value to variable field
396
  def fix_f880_subf6_paired_fields(record)
1✔
NEW
UNCOV
397
    linked_field_subf6 = linked_field_pairings(record).map { |tag| tag[0..2] }.uniq
×
NEW
UNCOV
398
    f880_subf6 = f880_pairings(record).map { |tag| tag[0..2] }.uniq
×
NEW
UNCOV
399
    (f880_subf6 - linked_field_subf6).each do |tag|
×
NEW
UNCOV
400
      fix_unmatched_paired_tag(record, tag)
×
401
    end
NEW
UNCOV
402
    record
×
403
  end
404

405
  def blocked_unmatched_pair?(record, tag)
1✔
NEW
UNCOV
406
    paired_fields = record.fields('880').select { |field| field['6'] =~ /^#{tag}/ }
×
NEW
UNCOV
407
    variable_fields = record.fields(tag)
×
NEW
UNCOV
408
    paired_fields.size != 1 || variable_fields.size != 1
×
409
  end
410

411
  def fix_unmatched_paired_tag(record, tag)
1✔
NEW
UNCOV
412
    return if blocked_unmatched_pair?(record, tag)
×
413

NEW
UNCOV
414
    record.fields('880').find { |field| field['6'] =~ /^#{tag}/ }
×
NEW
UNCOV
415
    sequence_number = paired_field['6'].gsub(/^#{tag}-([0-9]{2}).*/, '\1')
×
NEW
UNCOV
416
    record.fields.find do |field|
×
NEW
UNCOV
417
      field.tag == tag
×
418
    end.subfields << MARC::Subfield.new('6', "880-#{sequence_number}")
419
  end
420

421
  ### Scenario 6: 880 field has a tag in $6 that is not represented in a variable field:
422
  ###   Change sequence number to '00' to make it an unlinked 880
423
  def fix_f880_unlinked_f880(record)
1✔
NEW
UNCOV
424
    linked_field_tags = record.fields(LINKED_FIELD_TAGS).map(&:tag).uniq
×
NEW
UNCOV
425
    f880_subf6 = f880_pairings(record).map { |tag| tag[0..2] }.uniq
×
NEW
UNCOV
426
    unmatched_paired_subf6 = f880_subf6 - linked_field_tags
×
NEW
UNCOV
427
    unmatched_paired_subf6.each do |tag|
×
NEW
UNCOV
428
      fix_unmatched_f880_subf6(tag, record)
×
429
    end
NEW
UNCOV
430
    record
×
431
  end
432

433
  def fix_unmatched_f880_subf6(tag, record)
1✔
NEW
UNCOV
434
    record.fields('880').select { |field| field['6'] =~ /^#{tag}/ }.each do |field|
×
NEW
UNCOV
435
      field.subfields.find do |subfield|
×
NEW
UNCOV
436
        subfield.code == '6'
×
437
      end.value = field['6'].gsub(/^([0-9]+)-[0-9]+([^0-9]*.*)$/, '\1-00\2')
438
    end
439
  end
440

441
  def extra_subf6_info(field)
1✔
442
    field['6'].gsub(/^[0-9]+-[0-9]+([^0-9]*.*$)/, '\1')
2✔
443
  end
444

445
  ### Scenario 7: One $6 per tag in non-880 fields, and one $6 per tag in 880 fields:
446
  ###   Reorder sequence of 880s; if there is a $6 in a variable field and no corresponding 880, reserve
447
  ###   that field sequence for the variable field
448
  def resequence_f880_pairs(record)
1✔
449
    return record if blocked_f880_resequence?(record)
2✔
450

451
    linked_fields = linked_field_pairings(record).sort.group_by { |tag| tag[0..2] }
3✔
452
    linked_fields.keys.each_with_index do |tag, index|
1✔
453
      resequence_field_tag(sequence: format('%02d', index + 1), tag: tag, record: record)
2✔
454
    end
455
    record
1✔
456
  end
457

458
  def resequence_field_tag(sequence:, tag:, record:)
1✔
459
    f880 = record.fields(tag).find { |field| field['6'] }
4✔
460
    f880.subfields.find { |subfield| subfield.code == '6' }.value = "880-#{sequence}"
4✔
461
    resequence_paired_field(sequence, tag, record)
2✔
462
  end
463

464
  def resequence_paired_field(sequence, tag, record)
1✔
465
    paired_field = record.fields('880').find { |field| field['6'] =~ /#{tag}-(?!00)/ }
5✔
466
    return unless paired_field
2✔
467

468
    paired_field.subfields.find { |subfield| subfield.code == '6' }
4✔
469
                .value = "#{tag}-#{sequence}#{extra_subf6_info(paired_field)}"
470
  end
471

472
  def blocked_f880_resequence?(record)
1✔
473
    f880_pairings(record).group_by { |tag| tag[0..2] }.any? { |_tag, fields| fields.size > 1 } ||
11✔
474
      linked_field_pairings(record).group_by { |tag| tag[0..2] }.any? { |_tag, fields| fields.size > 1 }
4✔
475
  end
476

477
  def orphan_linked_fields(record)
1✔
478
    {
479
      orphan_f880: f880_pairings(record) - linked_field_pairings(record),
10✔
480
      orphan_linked_field: linked_field_pairings(record) - f880_pairings(record)
481
    }
482
  end
483

484
  def duplicate_subf6(record)
1✔
485
    {
486
      duplicate_f880_subf6: f880_pairings(record).tally.select { |_tag, count| count > 1 }.keys,
17✔
487
      duplicate_linked_field_subf6: linked_field_pairings(record).tally.select { |_tag, count| count > 1 }.keys
7✔
488
    }
489
  end
490

491
  def subf6_spaces(record)
1✔
492
    {
493
      f880_subf6_spaces: record.fields('880').select { |field| field['6'] =~ /\s/ },
20✔
494
      linked_field_subf6_spaces: record.fields(LINKED_FIELD_TAGS).select { |field| field['6'] =~ /\s/ }
10✔
495
    }
496
  end
497

498
  def pair_880_errors(record)
1✔
499
    hash = { f880_no_subf6: record.fields('880').select { |field| field['6'].nil? } }
20✔
500
    hash.merge!(orphan_linked_fields(record))
10✔
501
    hash.merge!(duplicate_subf6(record))
10✔
502
    hash.merge(subf6_spaces(record))
10✔
503
  end
504

505
  def f880_pairings(record)
1✔
506
    target_fields = record.fields('880').select { |field| field['6'] }
67✔
507
    target_fields.reject! { |field| field['6'] =~ /-00/ }
64✔
508
    target_fields.map do |field|
32✔
509
      field['6'].gsub(/^[^0-9]*([0-9]{3}-[0-9]{2}).*$/, '\1')
29✔
510
    end
511
  end
512

513
  def linked_field_pairings(record)
1✔
514
    target_fields = record.fields('010'..'899').select do |field|
32✔
515
      field.tag != '880' && field['6']
68✔
516
    end
517
    target_fields.map do |field|
32✔
518
      "#{field.tag}-#{field['6'].gsub(/^880-([0-9]{2}).*$/, '\1')}"
28✔
519
    end
520
  end
521

522
  def f130_f240?(record)
1✔
523
    (%w[130 240] - record.tags).empty?
27✔
524
  end
525

526
  def multiple_1xx?(record)
1✔
527
    record.fields('100'..'199').size > 1
27✔
528
  end
529

530
  def relator_chars?(record)
1✔
531
    record.fields(%w[100 110 111 700 710 711]).any? do |field|
4✔
532
      relator_chars_target_subfields(field).select do |subfield|
4✔
533
        subfield.value =~ /[^a-z\-, .]/
12✔
534
      end.size.positive?
535
    end
536
  end
537

538
  def relator_chars_target_subfields(field)
1✔
539
    case field.tag
4✔
540
    when '111', '711'
541
      field.subfields.select { |subf| subf.code == 'j' }
10✔
542
    else
543
      field.subfields.select { |subf| subf.code == 'e' }
10✔
544
    end
545
  end
546

547
  def x00_subfq?(record)
1✔
548
    record.fields(%w[100 600 700 800]).any? do |field|
2✔
549
      field.subfields.select do |subfield|
2✔
550
        subfield.code == 'q' && subfield.value =~ /^[^(].*[^)]$/
4✔
551
      end.size.positive?
552
    end
553
  end
554

555
  def x00_subfd_no_comma?(record)
1✔
556
    record.fields(%w[100 600 700 800]).any? do |field|
2✔
557
      subf_d_index = field.subfields.index { |subfield| subfield.code == 'd' }
7✔
558
      next unless subf_d_index
2✔
559

560
      field.subfields[subf_d_index - 1].value =~ /[^,]$/
2✔
561
    end
562
  end
563

564
  def relator_comma?(record)
1✔
565
    record.fields(%w[100 110 111 700 710 711]).any? do |field|
4✔
566
      relator_index = relator_subfield_index(field)
4✔
567
      next unless relator_index
4✔
568

569
      field.subfields[relator_index - 1].value =~ /[^,]$/
4✔
570
    end
571
  end
572

573
  def relator_subfield_index(field)
1✔
574
    case field.tag
4✔
575
    when '111', '711'
576
      field.subfields.index { |subfield| subfield.code == 'j' }
6✔
577
    else
578
      field.subfields.index { |subfield| subfield.code == 'e' }
6✔
579
    end
580
  end
581

582
  def heading_end_punct?(record)
1✔
583
    punct_regex = /[^").!?-]$/
3✔
584
    record.fields(punctuated_heading_fields).any? do |field|
3✔
585
      next unless field.tag =~ /^[1678][0-5].$/
3✔
586

587
      last_heading_subfield = last_heading_subfield(field)
3✔
588
      next unless last_heading_subfield
3✔
589

590
      last_heading_subfield.value =~ punct_regex
2✔
591
    end
592
  end
593

594
  def punctuated_heading_fields
1✔
595
    %w[
3✔
596
      100 110 111 130
597
      600 610 611 630 650 651 654 655 656 657 658 662
598
      700 710 711 730 740 752 754
599
      800 810 811 830
600
    ]
601
  end
602

603
  def last_heading_subfield(field)
1✔
604
    regex = /[^02345]/
3✔
605
    heading_subfields = field.subfields.select do |subfield|
3✔
606
      subfield.code =~ regex
7✔
607
    end
608
    if heading_subfields.empty?
3✔
609
      nil
610
    else
611
      heading_subfields[-1]
2✔
612
    end
613
  end
614

615
  def subf_0_uri?(record)
1✔
616
    record.fields.any? do |field|
3✔
617
      field.instance_of?(MARC::DataField) &&
3✔
618
        field.tag =~ /^[^9]/ &&
619
        field.subfields.any? do |subfield|
620
          subfield.code == '0' && subfield.value =~ /^\(uri\)/
4✔
621
        end
622
    end
623
  end
624

625
  ### Replace empty indicators with a space;
626
  ###   scrub indicators with bad UTF-8;
627
  ###   The ruby-marc gem converts nil subfields to spaces
628
  def empty_indicator_fix(record)
1✔
629
    record.fields.each do |field|
2✔
630
      next unless field.instance_of?(MARC::DataField)
2✔
631

632
      ind1_value = field.indicator1.dup
2✔
633
      ind1_value.scrub!('')
2✔
634
      field.indicator1 = ' ' if ind1_value.empty?
2✔
635
      ind2_value = field.indicator2.dup
2✔
636
      ind2_value.scrub!('')
2✔
637
      field.indicator2 = ' ' if ind2_value.empty?
2✔
638
    end
639
    record
2✔
640
  end
641

642
  ### Remove empty subfields from DataFields
643
  def empty_subfield_fix(record)
1✔
644
    record.fields.each do |field|
2✔
645
      next unless field.instance_of?(MARC::DataField)
8✔
646

647
      field.subfields.delete_if { |subfield| subfield.value.to_s.empty? }
21✔
648
    end
649
    record.fields.delete_if { |field| field.instance_of?(MARC::DataField) && field.subfields.empty? }
10✔
650
    record
2✔
651
  end
652

653
  ### Remove the (uri) prefix from subfield 0s
654
  def subf_0_uri_fix(record)
1✔
655
    record.fields.each do |field|
3✔
656
      next unless field.instance_of?(MARC::DataField) && field.tag[0] != '9'
3✔
657

658
      field.subfields.each do |subfield|
2✔
659
        next unless subfield.code == '0'
4✔
660

661
        subfield.value = subfield.value.dup.delete_prefix('(uri)')
2✔
662
      end
663
    end
664
    record
3✔
665
  end
666

667
  ### Make the 040 $b 'eng' if it doesn't have a value
668
  def fix_040b(record)
1✔
669
    return record unless record.fields('040').size == 1
2✔
670

671
    f040 = record['040']
2✔
672
    return record if f040['b']
2✔
673

674
    field_index = record.fields.index(f040)
2✔
675
    subf_index = f040.subfields.index('a').to_i + 1
2✔
676
    subf_b = MARC::Subfield.new('b', 'eng')
2✔
677
    record.fields[field_index].subfields.insert(subf_index, subf_b)
2✔
678
    record
2✔
679
  end
680

681
  def split_f041_subfield(subfield)
1✔
682
    subfields = []
2✔
683
    if (subfield.value.size % 3).zero?
2✔
684
      subfield.value.scan(/.../).each do |language|
1✔
685
        subfields.append(MARC::Subfield.new(subfield.code, language))
3✔
686
      end
687
    else
688
      subfields.append(MARC::Subfield.new(subfield.code, subfield.value))
1✔
689
    end
690
    subfields
2✔
691
  end
692

693
  def split_f041_field(field)
1✔
694
    new_field = MARC::DataField.new('041', field.indicator1, field.indicator2)
2✔
695
    field.subfields.each do |subfield|
2✔
696
      new_subfields = split_f041_subfield(subfield)
2✔
697
      new_subfields.each { |new_subfield| new_field.append(new_subfield) }
6✔
698
    end
699
    new_field
2✔
700
  end
701

702
  ### Split up subfields that contain multiple 3-letter language codes
703
  def fix_f041(record)
1✔
704
    f041 = record.fields('041')
2✔
705
    f041.each do |field|
2✔
706
      f_index = record.fields.index(field)
2✔
707
      new_field = split_f041_field(field)
2✔
708
      record.fields[f_index] = new_field
2✔
709
    end
710
    record
2✔
711
  end
712

713
  ### Removes text from the beginning of a subfield
714
  ### An array of hashes of the format { field:, subfields: } will be passed
715
  ###   in the targets: symbol
716
  ###   subfield: is an array of subfield codes
717
  def remove_prefix_from_subfield(record:, targets:, string:)
1✔
718
    targets.each do |target|
1✔
719
      record.fields(target[:field]).each do |field|
1✔
720
        field.subfields.each do |subfield|
1✔
721
          next unless target[:subfields].include?(subfield.code)
2✔
722

723
          subfield.value = subfield.value.dup.delete_prefix(string)
1✔
724
        end
725
      end
726
    end
727
    record
1✔
728
  end
729

730
  ### Adds text to the beginning of a subfield
731
  ### An array of hashes of the format { field:, subfields: } will be passed
732
  ###   in the targets: symbol
733
  ###   subfield: is an array of subfield codes
734
  def add_prefix_to_subfield(record:, targets:, string:)
1✔
735
    targets.each do |target|
1✔
736
      record.fields(target[:field]).each do |field|
1✔
737
        field.subfields.each do |subfield|
1✔
738
          next unless target[:subfields].include?(subfield.code)
2✔
739

740
          subfield.value = subfield.value.dup.prepend(string)
1✔
741
        end
742
      end
743
    end
744
    record
1✔
745
  end
746

747
  ### Sort subfields for target fields with an arbitrary order
748
  ### Example order_array: ['a', 'b', 'c']
749
  def subfield_sort(record:, target_tags:, order_array: nil)
1✔
750
    record.fields(target_tags).each do |field|
3✔
751
      next if field.instance_of?(MARC::ControlField)
3✔
752

753
      order_array ||= field.subfields.map(&:code).uniq.sort
2✔
754
      new_subfields = sort_listed_subfields(field: field, order_array: order_array)
2✔
755
      new_subfields += find_unlisted_subfields(field: field, order_array: order_array)
2✔
756
      field.subfields = new_subfields
2✔
757
    end
758
    record
3✔
759
  end
760

761
  def find_unlisted_subfields(field:, order_array:)
1✔
762
    field.subfields.reject do |subfield|
2✔
763
      order_array.include?(subfield.code)
7✔
764
    end
765
  end
766

767
  def sort_listed_subfields(field:, order_array:)
1✔
768
    listed_subfields = field.subfields.select do |subfield|
2✔
769
      order_array.include?(subfield.code)
7✔
770
    end
771
    listed_subfields.sort_by! do |subfield|
2✔
772
      order_array.index { |code| code == subfield.code }
20✔
773
    end
774
  end
775
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

© 2026 Coveralls, Inc