pennmarc 1.0.25 → 1.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +8 -9
  3. data/lib/pennmarc/helpers/creator.rb +139 -65
  4. data/lib/pennmarc/helpers/edition.rb +5 -3
  5. data/lib/pennmarc/helpers/note.rb +24 -19
  6. data/lib/pennmarc/helpers/production.rb +113 -20
  7. data/lib/pennmarc/test/marc_helpers.rb +83 -0
  8. data/lib/pennmarc/util.rb +98 -69
  9. data/lib/pennmarc/version.rb +1 -1
  10. data/lib/pennmarc.rb +7 -0
  11. data/spec/lib/pennmarc/helpers/access_spec.rb +0 -2
  12. data/spec/lib/pennmarc/helpers/citation_spec.rb +0 -2
  13. data/spec/lib/pennmarc/helpers/classification_spec.rb +0 -2
  14. data/spec/lib/pennmarc/helpers/creator_spec.rb +103 -2
  15. data/spec/lib/pennmarc/helpers/database_spec.rb +0 -2
  16. data/spec/lib/pennmarc/helpers/date_spec.rb +0 -2
  17. data/spec/lib/pennmarc/helpers/edition_spec.rb +4 -2
  18. data/spec/lib/pennmarc/helpers/format_spec.rb +0 -2
  19. data/spec/lib/pennmarc/helpers/genre_spec.rb +0 -2
  20. data/spec/lib/pennmarc/helpers/identifer_spec.rb +0 -2
  21. data/spec/lib/pennmarc/helpers/inventory_spec.rb +0 -2
  22. data/spec/lib/pennmarc/helpers/language_spec.rb +0 -2
  23. data/spec/lib/pennmarc/helpers/link_spec.rb +0 -2
  24. data/spec/lib/pennmarc/helpers/location_spec.rb +0 -2
  25. data/spec/lib/pennmarc/helpers/note_spec.rb +22 -29
  26. data/spec/lib/pennmarc/helpers/production_spec.rb +121 -22
  27. data/spec/lib/pennmarc/helpers/relation_spec.rb +0 -2
  28. data/spec/lib/pennmarc/helpers/series_spec.rb +0 -2
  29. data/spec/lib/pennmarc/helpers/subject_spec.rb +0 -2
  30. data/spec/lib/pennmarc/helpers/title_spec.rb +0 -2
  31. data/spec/lib/pennmarc/marc_util_spec.rb +0 -2
  32. data/spec/lib/pennmarc/parser_spec.rb +1 -1
  33. data/spec/spec_helper.rb +7 -0
  34. data/spec/support/fixture_helpers.rb +10 -0
  35. metadata +4 -3
  36. data/spec/support/marc_spec_helpers.rb +0 -85
@@ -4,31 +4,47 @@ module PennMARC
4
4
  # Extracts data related to a resource's production, distribution, manufacture, and publication.
5
5
  class Production < Helper
6
6
  class << self
7
- # Retrieve production values for display from {https://www.oclc.org/bibformats/en/2xx/264.html 264 field}.
8
- # @param [MARC::Record] record
7
+ # Retrieve production values for display from {https://www.loc.gov/marc/bibliographic/bd264.html 264 field}.
8
+ # @param record [MARC::Record]
9
9
  # @return [Array<String>]
10
10
  def show(record)
11
11
  get_264_or_880_fields(record, '0').uniq
12
12
  end
13
13
 
14
- # Retrieve distribution values for display from {https://www.oclc.org/bibformats/en/2xx/264.html 264 field}.
15
- # @param [MARC::Record] record
14
+ # Retrieve production values for searching. Includes only
15
+ # {https://www.loc.gov/marc/bibliographic/bd260.html 260} and
16
+ # {https://www.loc.gov/marc/bibliographic/bd264.html 264}.
17
+ # @param record [MARC::Record]
18
+ # @return [Array<String>]
19
+ def search(record)
20
+ values = record.fields('260').filter_map do |field|
21
+ join_subfields(field, &subfield_in?(['b']))
22
+ end
23
+ values + record.fields('264').filter_map { |field|
24
+ next unless field.indicator2 == '1'
25
+
26
+ join_subfields(field, &subfield_in?(['b']))
27
+ }.uniq
28
+ end
29
+
30
+ # Retrieve distribution values for display from {https://www.loc.gov/marc/bibliographic/bd264.html 264 field}.
31
+ # @param record [MARC::Record]
16
32
  # @return [Array<String>]
17
33
  def distribution_show(record)
18
34
  get_264_or_880_fields(record, '2').uniq
19
35
  end
20
36
 
21
- # Retrieve manufacture values for display from {https://www.oclc.org/bibformats/en/2xx/264.html 264 field}.
22
- # @param [MARC::Record] record
37
+ # Retrieve manufacture values for display from {https://www.loc.gov/marc/bibliographic/bd264.html 264 field}.
38
+ # @param record [MARC::Record]
23
39
  # @return [Array<String>]
24
40
  def manufacture_show(record)
25
41
  get_264_or_880_fields(record, '3').uniq
26
42
  end
27
43
 
28
44
  # Retrieve publication values. Return publication values from
29
- # {https://www.oclc.org/bibformats/en/2xx/264.html 264 field} only if none found
30
- # {https://www.oclc.org/bibformats/en/2xx/260.html 260}-262 fields.
31
- # @param [MARC::Record] record
45
+ # {https://www.loc.gov/marc/bibliographic/bd264.html 264 field} only if none found
46
+ # {https://www.loc.gov/marc/bibliographic/bd260.html 260}-262 fields.
47
+ # @param record [MARC::Record]
32
48
  # @return [Array<String>]
33
49
  def publication_values(record)
34
50
  # first get inclusive dates
@@ -64,10 +80,10 @@ module PennMARC
64
80
  end
65
81
 
66
82
  # Retrieve publication values for display from fields
67
- # {https://www.oclc.org/bibformats/en/2xx/245.html 245},
68
- # {https://www.oclc.org/bibformats/en/2xx/260.html 260}-262 and their linked alternates,
69
- # and {https://www.oclc.org/bibformats/en/2xx/264.html 264} and its linked alternate.
70
- # @param [MARC::Record] record
83
+ # {https://www.loc.gov/marc/bibliographic/bd245.html 245},
84
+ # {https://www.loc.gov/marc/bibliographic/bd260.html 260}-262 and their linked alternates,
85
+ # and {https://www.loc.gov/marc/bibliographic/bd264.html 264} and its linked alternate.
86
+ # @param record [MARC::Record]
71
87
  # @return [Array<String>]
72
88
  def publication_show(record)
73
89
  values = record.fields('245').first(1).flat_map { |field| subfield_values(field, 'f') }
@@ -89,10 +105,49 @@ module PennMARC
89
105
  values.compact_blank.uniq
90
106
  end
91
107
 
92
- # Retrieve place of publication for display from {https://www.oclc.org/bibformats/en/7xx/752.html 752 field} and
108
+ # Retrieve publication values for citation
109
+ # {https://www.loc.gov/marc/bibliographic/bd245.html 245},
110
+ # {https://www.loc.gov/marc/bibliographic/bd260.html 260}-262 and their linked alternates,
111
+ # and {https://www.loc.gov/marc/bibliographic/bd264.html 264} and its linked alternate.
112
+ # @param [MARC::Record] record
113
+ # @param [Boolean] with_year: return results with publication year if true
114
+ # @return [Array<String>]
115
+ def publication_citation_show(record, with_year: true)
116
+ values = record.fields('245').first(1).flat_map { |field| subfield_values(field, 'f') }
117
+
118
+ subfields = with_year ? %w[6 8] : %w[6 8 c]
119
+ values += record.fields(%w[260 261 262]).first(1).map do |field|
120
+ join_subfields(field, &subfield_not_in?(subfields))
121
+ end
122
+
123
+ subfields = with_year ? %w[a b c] : %w[a b]
124
+ values += record.fields('264').filter_map do |field|
125
+ next unless field.indicator2 == '1'
126
+
127
+ join_subfields(field, &subfield_in?(subfields))
128
+ end
129
+
130
+ values.compact_blank.uniq
131
+ end
132
+
133
+ # Returns the place of publication for RIS
134
+ # @param [MARC::Record] record
135
+ # @return [Array<String>]
136
+ def publication_ris_place_of_pub(record)
137
+ get_publication_ris_values(record, 'a')
138
+ end
139
+
140
+ # Returns the publisher for RIS
141
+ # @param [MARC::Record] record
142
+ # @return [Array<String>]
143
+ def publication_ris_publisher(record)
144
+ get_publication_ris_values(record, 'b')
145
+ end
146
+
147
+ # Retrieve place of publication for display from {https://www.loc.gov/marc/bibliographic/bd752.html 752 field} and
93
148
  # its linked alternate.
94
149
  # @note legacy version returns array of hash objects including data for display link
95
- # @param [MARC::Record] record
150
+ # @param record [MARC::Record]
96
151
  # @return [Array<String>]
97
152
  def place_of_publication_show(record)
98
153
  record.fields(%w[752 880]).filter_map { |field|
@@ -104,13 +159,32 @@ module PennMARC
104
159
  }.uniq
105
160
  end
106
161
 
162
+ # Retrieves place of publication values for searching. Includes
163
+ # {https://www.loc.gov/marc/bibliographic/bd752.html 752} as well as sf a from
164
+ # {https://www.loc.gov/marc/bibliographic/bd260.html 260} and
165
+ # {https://www.loc.gov/marc/bibliographic/bd264.html 264} with an indicator2 of 1.
166
+ # @param record [MARC::Record]
167
+ # @return [Array<String>]
168
+ def place_of_publication_search(record)
169
+ values = record.fields('260').filter_map do |field|
170
+ join_subfields(field, &subfield_in?(['a']))
171
+ end
172
+ values += record.fields('264').filter_map do |field|
173
+ next unless field.indicator2 == '1'
174
+
175
+ join_subfields(field, &subfield_in?(['a']))
176
+ end
177
+ values + record.fields('752').filter_map { |field|
178
+ join_subfields(field, &subfield_in?(%w[a b c d f g h]))
179
+ }.uniq
180
+ end
181
+
107
182
  private
108
183
 
109
- # base method to retrieve production values from {https://www.oclc.org/bibformats/en/2xx/264.html 264 field} based
110
- # on indicator2.
111
- # distribution and manufacture share the same logic except for indicator2
112
- # @param [MARC::Record] record
113
- # @param [String] indicator2
184
+ # base method to retrieve production values from {https://www.loc.gov/marc/bibliographic/bd264.html 264 field}
185
+ # based on indicator2. "Distribution" and "manufacture" share the same logic except for indicator2.
186
+ # @param record [MARC::Record]
187
+ # @param indicator2 [String]
114
188
  # @return [Array<String>]
115
189
  def get_264_or_880_fields(record, indicator2)
116
190
  values = record.fields('264').filter_map do |field|
@@ -126,6 +200,25 @@ module PennMARC
126
200
  join_subfields(field, &subfield_in?(%w[a b c]))
127
201
  end
128
202
  end
203
+
204
+ # Returns the publication value of the given subfield
205
+ # @param [MARC::Record] record
206
+ # @param [String] subfield
207
+ def get_publication_ris_values(record, subfield)
208
+ values = record.fields('245').first(1).flat_map { |field| subfield_values(field, 'f') }
209
+
210
+ values += record.fields(%w[260 261 262]).first(1).map do |field|
211
+ join_subfields(field, &subfield_in?([subfield]))
212
+ end
213
+
214
+ values += record.fields('264').filter_map do |field|
215
+ next unless field.indicator2 == '1'
216
+
217
+ join_subfields(field, &subfield_in?([subfield]))
218
+ end
219
+
220
+ values.compact_blank.uniq
221
+ end
129
222
  end
130
223
  end
131
224
  end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+ require 'marc'
5
+
6
+ module PennMARC
7
+ module Test
8
+ # Helper methods for use in constructing MARC objects for testing
9
+ module MarcHelpers
10
+ # Return a MARC::XMLReader that will parse a given file and return MARC::Record objects
11
+ # @param [String] filename of MARCXML fixture
12
+ # @return [MARC::Record, NilClass]
13
+ def record_from(filename)
14
+ MARC::XMLReader.new(marc_xml_path(filename)).first
15
+ end
16
+
17
+ # Create an isolated MARC::Subfield object for use in specs or as part of a MARC::Field
18
+ # @param [String] code
19
+ # @param [String] value
20
+ # @return [MARC::Subfield]
21
+ def marc_subfield(code, value)
22
+ MARC::Subfield.new code.to_s, value
23
+ end
24
+
25
+ # Return a new ControlField (000-009)
26
+ # @param [String] tag
27
+ # @param [String] value
28
+ # @return [MARC::ControlField]
29
+ def marc_control_field(tag:, value:)
30
+ MARC::ControlField.new tag, value
31
+ end
32
+
33
+ # Create an isolated MARC::DataField object for use in specs
34
+ # Can pass in tag, indicators and subfields (using simple hash structure). E.g.,
35
+ # marc_field(tag: '650', indicator2: '7'),
36
+ # subfields: { a: 'Tax planning',
37
+ # m: ['Multiple', 'Subfields']
38
+ # z: 'United States.',
39
+ # '0': http://id.loc.gov/authorities/subjects/sh2008112546 }
40
+ # )
41
+ # @param [String (frozen)] tag MARC tag, e.g., 001, 665
42
+ # @param [String (frozen)] indicator1 MARC indicator, e.g., 0
43
+ # @param [String (frozen)] indicator2
44
+ # @param [Hash] subfields hash of subfield values as code => value or code => [value, value]
45
+ # @return [MARC::DataField]
46
+ def marc_field(tag: 'TST', indicator1: ' ', indicator2: ' ', subfields: {})
47
+ subfield_objects = subfields.each_with_object([]) do |(code, value), array|
48
+ Array.wrap(value).map { |v| array << marc_subfield(code, v) }
49
+ end
50
+ MARC::DataField.new tag, indicator1, indicator2, *subfield_objects
51
+ end
52
+
53
+ # Return a MARC::Record containing passed in DataFields
54
+ # @param [Array<MARC::DataField>] fields
55
+ # @param [String, nil] leader
56
+ # @return [MARC::Record]
57
+ def marc_record(fields: [], leader: nil)
58
+ record = MARC::Record.new
59
+ fields.each { |field| record << field }
60
+ record.leader = leader if leader
61
+ record
62
+ end
63
+
64
+ # Mock map for location lookup using Location helper
65
+ # The location codes :dent and :stor are the two outermost keys
66
+ # :specific_location, :library, :display are the inner keys that store location values
67
+ # @example
68
+ # location_map[:stor][:library] #=> 'LIBRA'
69
+ # @return [Hash]
70
+ def location_map
71
+ { dent: { specific_location: 'Levy Dental Medicine Library - Stacks',
72
+ library: ['Health Sciences Libraries', 'Levy Dental Medicine Library'],
73
+ display: 'Levy Dental Medicine Library - Stacks' },
74
+ stor: { specific_location: 'LIBRA',
75
+ library: 'LIBRA',
76
+ display: 'LIBRA' },
77
+ vanp: { specific_location: 'Van Pelt - Stacks',
78
+ library: 'Van Pelt-Dietrich Library Center',
79
+ display: 'Van Pelt Library' } }
80
+ end
81
+ end
82
+ end
83
+ end
data/lib/pennmarc/util.rb CHANGED
@@ -13,16 +13,16 @@ module PennMARC
13
13
  period: /\.\s*$/ }.freeze # TODO: revise to exclude "etc."
14
14
 
15
15
  # Check if a given record has a field present by tag (e.g., '041')
16
- # @param [MARC::Record] record
17
- # @param [String] marc_field
16
+ # @param record [MARC::Record]
17
+ # @param marc_field [String]
18
18
  # @return [Boolean]
19
19
  def field_defined?(record, marc_field)
20
20
  record.select { |field| field.tag == marc_field }.any?
21
21
  end
22
22
 
23
23
  # Join subfields from a field selected based on a provided proc
24
- # @param [MARC::DataField, nil] field
25
- # @param [Proc] selector
24
+ # @param field [MARC::DataField, nil]
25
+ # @param selector [Proc]
26
26
  # @return [String]
27
27
  def join_subfields(field, &selector)
28
28
  return '' unless field
@@ -37,77 +37,75 @@ module PennMARC
37
37
 
38
38
  # returns true if field has a value that matches
39
39
  # passed-in regex and passed in subfield
40
- # @todo example usage
41
- # @param [MARC::DataField] field
42
- # @param [String|Integer|Symbol] subfield
43
- # @param [Regexp] regex
44
- # @return [TrueClass, FalseClass]
40
+ # @param field [MARC::DataField]
41
+ # @param subfield [String|Integer|Symbol]
42
+ # @param regex [Regexp]
43
+ # @return [Boolean, nil]
45
44
  def subfield_value?(field, subfield, regex)
46
45
  field&.any? { |sf| sf.code == subfield.to_s && sf.value =~ regex }
47
46
  end
48
47
 
49
48
  # returns true if field has no value that matches
50
49
  # passed-in regex and passed in subfield
51
- # @param [MARC::DataField] field
52
- # @param [String|Integer|Symbol] subfield
53
- # @param [Regexp] regex
54
- # @return [TrueClass, FalseClass, nil]
50
+ # @param field [MARC::DataField]
51
+ # @param subfield [String|Integer|Symbol]
52
+ # @param regex [Regexp]
53
+ # @return [Boolean, nil]
55
54
  def no_subfield_value_matches?(field, subfield, regex)
56
55
  field&.none? { |sf| sf.code == subfield.to_s && sf.value =~ regex }
57
56
  end
58
57
 
59
58
  # returns true if a given field has a given subfield value in a given array
60
- # TODO: example usage
61
- # @param [MARC:DataField] field
62
- # @param [String|Integer|Symbol] subfield
63
- # @param [Array] array
64
- # @return [TrueClass, FalseClass]
59
+ # @param field [MARC:DataField]
60
+ # @param subfield [String|Integer|Symbol]
61
+ # @param array [Array]
62
+ # @return [Boolean]
65
63
  def subfield_value_in?(field, subfield, array)
66
64
  field.any? { |sf| sf.code == subfield.to_s && sf.value.in?(array) }
67
65
  end
68
66
 
69
67
  # returns true if a given field does not have a given subfield value in a given array
70
- # @param [MARC:DataField] field
71
- # @param [String|Integer|Symbol] subfield
72
- # @param [Array] array
73
- # @return [TrueClass, FalseClass
68
+ # @param field [MARC:DataField]
69
+ # @param subfield [String|Integer|Symbol]
70
+ # @param array [Array]
71
+ # @return [Boolean]
74
72
  def subfield_value_not_in?(field, subfield, array)
75
73
  field.none? { |sf| sf.code == subfield.to_s && sf.value.in?(array) }
76
74
  end
77
75
 
78
76
  # returns a lambda checking if passed-in subfield's code is a member of array
79
- # @param [Array] array
77
+ # @param array [Array]
80
78
  # @return [Proc]
81
79
  def subfield_in?(array)
82
80
  ->(subfield) { array.member?(subfield.code) }
83
81
  end
84
82
 
85
83
  # returns a lambda checking if passed-in subfield's code is NOT a member of array
86
- # @param [Array] array
84
+ # @param array [Array]
87
85
  # @return [Proc]
88
86
  def subfield_not_in?(array)
89
87
  ->(subfield) { !array.member?(subfield.code) }
90
88
  end
91
89
 
92
90
  # Check if a field has a given subfield defined
93
- # @param [MARC::DataField] field
94
- # @param [String|Symbol|Integer] subfield
95
- # @return [TrueClass, FalseClass]
91
+ # @param field [MARC::DataField]
92
+ # @param subfield [String|Symbol|Integer]
93
+ # @return [Boolean]
96
94
  def subfield_defined?(field, subfield)
97
95
  field.any? { |sf| sf.code == subfield.to_s }
98
96
  end
99
97
 
100
98
  # Check if a field does not have a given subfield defined
101
- # @param [MARC::DataField] field
102
- # @param [String|Symbol|Integer] subfield
103
- # @return [TrueClass, FalseClass]
99
+ # @param field [MARC::DataField]
100
+ # @param subfield [String|Symbol|Integer]
101
+ # @return [Boolean]
104
102
  def subfield_undefined?(field, subfield)
105
103
  field.none? { |sf| sf.code == subfield.to_s }
106
104
  end
107
105
 
108
106
  # Gets all subfield values for a subfield in a given field
109
- # @param [MARC::DataField] field
110
- # @param [String|Symbol] subfield as a string or symbol
107
+ # @param field [MARC::DataField]
108
+ # @param subfield [String|Symbol] as a string or symbol
111
109
  # @return [Array] subfield values for given subfield code
112
110
  def subfield_values(field, subfield)
113
111
  field.filter_map do |sf|
@@ -120,9 +118,9 @@ module PennMARC
120
118
  end
121
119
 
122
120
  # Get all subfield values for a provided subfield from any occurrence of a provided tag/tags
123
- # @param [String|Array] tag tags to consider
124
- # @param [String|Symbol] subfield to take the values from
125
- # @param [MARC::Record] record source
121
+ # @param tag [String|Array] tags to consider
122
+ # @param subfield [String|Symbol] to take the values from
123
+ # @param record [MARC::Record]
126
124
  # @return [Array] array of subfield values
127
125
  def subfield_values_for(tag:, subfield:, record:)
128
126
  record.fields(tag).flat_map do |field|
@@ -130,24 +128,43 @@ module PennMARC
130
128
  end
131
129
  end
132
130
 
133
- # @param [Symbol|String] trailer to target for removal
134
- # @param [String] string to modify
131
+ # Trim punctuation method extracted from Traject macro, to ensure consistent output
132
+ # @param string [String]
133
+ # @return [String] string with relevant punctuation removed
134
+ def trim_punctuation(string)
135
+ return string unless string
136
+
137
+ string = string.sub(%r{ *[ ,/;:] *\Z}, '')
138
+
139
+ # trailing period if it is preceded by at least three letters (possibly preceded and followed by whitespace)
140
+ string = string.sub(/( *[[:word:]]{3,})\. *\Z/, '\1')
141
+
142
+ # single square bracket characters if they are the start and/or end chars and there are no internal square
143
+ # brackets.
144
+ string = string.sub(/\A\[?([^\[\]]+)\]?\Z/, '\1')
145
+
146
+ # trim any leading or trailing whitespace
147
+ string.strip
148
+ end
149
+
150
+ # @param trailer [Symbol|String] to target for removal
151
+ # @param string [String] to modify
135
152
  # @return [String]
136
153
  def trim_trailing(trailer, string)
137
154
  string.sub TRAILING_PUNCTUATIONS_PATTERNS[trailer.to_sym], ''
138
155
  end
139
156
 
140
157
  # trim trailing punctuation, manipulating string in place
141
- # @param [Symbol|String] trailer to target for removal
142
- # @param [String] string to modify
158
+ # @param trailer [Symbol|String] to target for removal
159
+ # @param string [String] to modify
143
160
  # @return [String, Nil] string to modify
144
161
  def trim_trailing!(trailer, string)
145
162
  string.sub! TRAILING_PUNCTUATIONS_PATTERNS[trailer.to_sym], ''
146
163
  end
147
164
 
148
165
  # Intelligently append given punctuation to the end of a string
149
- # @param [Symbol] trailer
150
- # @param [String] string
166
+ # @param trailer [Symbol]
167
+ # @param string [String]
151
168
  # @return [String]
152
169
  def append_trailing(trailer, string)
153
170
  return string if string.end_with?('.', '-')
@@ -165,9 +182,9 @@ module PennMARC
165
182
  # translations of title values. A common need is to extract subfields as selected by
166
183
  # passed-in block from 880 datafield that has a particular subfield 6 value.
167
184
  # See: https://www.loc.gov/marc/bibliographic/bd880.html
168
- # @param [MARC::Record] record
169
- # @param [String|Array] subfield6_value either a string to look for in sub6 or an array of them
170
- # @param [Proc] selector takes a subfield as argument, returns a boolean
185
+ # @param record [MARC::Record]
186
+ # @param subfield6_value [String|Array] either a string to look for in sub6 or an array of them
187
+ # @param selector [Proc] takes a subfield as argument, returns a boolean
171
188
  # @return [Array] array of linked alternates
172
189
  def linked_alternate(record, subfield6_value, &selector)
173
190
  record.fields('880').filter_map do |field|
@@ -180,8 +197,8 @@ module PennMARC
180
197
  # Common case of wanting to extract all the subfields besides 6 or 8,
181
198
  # from 880 datafield that has a particular subfield 6 value. We exclude 6 because
182
199
  # that value is the linkage ID itself and 8 because... IDK
183
- # @param [MARC::Record] record
184
- # @param [String|Array] subfield6_value either a string to look for in sub6 or an array of them
200
+ # @param record [MARC::Record]
201
+ # @param subfield6_value [String|Array] either a string to look for in sub6 or an array of them
185
202
  # @return [Array] array of linked alternates without 8 or 6 values
186
203
  def linked_alternate_not_6_or_8(record, subfield6_value)
187
204
  excluded_subfields = %w[6 8]
@@ -191,8 +208,8 @@ module PennMARC
191
208
  end
192
209
 
193
210
  # Returns the non-6,8 subfields from a datafield and its 880 link.
194
- # @param [MARC::Record] record
195
- # @param [String] tag
211
+ # @param record [MARC::Record]
212
+ # @param tag [String]
196
213
  # @return [Array<String>] values
197
214
  def datafield_and_linked_alternate(record, tag)
198
215
  record.fields(tag).filter_map { |field|
@@ -201,23 +218,23 @@ module PennMARC
201
218
  end
202
219
 
203
220
  # Get the substring of a string up to a given target character
204
- # @param [Object] string to split
205
- # @param [Object] target character to split upon
221
+ # @param string [Object] to split
222
+ # @param target [Object] character to split upon
206
223
  # @return [String (frozen)]
207
224
  def substring_before(string, target)
208
225
  string.scan(target).present? ? string.split(target, 2).first : ''
209
226
  end
210
227
 
211
228
  # Get the substring of a string after the first occurrence of a target character
212
- # @param [Object] string to split
213
- # @param [Object] target character to split upon
229
+ # @param string [Object] to split
230
+ # @param target [Object] character to split upon
214
231
  # @return [String (frozen)]
215
232
  def substring_after(string, target)
216
233
  string.scan(target).present? ? string.split(target, 2).second : ''
217
234
  end
218
235
 
219
236
  # Join array and normalizing extraneous spaces
220
- # @param [Array] array
237
+ # @param array [Array]
221
238
  # @return [String]
222
239
  def join_and_squish(array)
223
240
  array.join(' ').squish
@@ -225,7 +242,7 @@ module PennMARC
225
242
 
226
243
  # If there's a subfield i, extract its value, and if there's something
227
244
  # in parentheses in that value, extract that.
228
- # @param [MARC::Field] field
245
+ # @param field [MARC::Field]
229
246
  # @return [String] subfield i without parentheses value
230
247
  def remove_paren_value_from_subfield_i(field)
231
248
  val = field.filter_map { |sf|
@@ -243,19 +260,19 @@ module PennMARC
243
260
 
244
261
  # Translate a relator code using mapping
245
262
  # @todo handle case of receiving a URI? E.g., http://loc.gov/relator/aut
246
- # @param [String, NilClass] relator_code
247
- # @param [Hash] mapping
263
+ # @param relator_code [String, NilClass]
264
+ # @param mapping [Hash]
248
265
  # @return [String, NilClass] full relator string
249
266
  def translate_relator(relator_code, mapping)
250
267
  return if relator_code.blank?
251
268
 
252
- mapping[relator_code.to_sym]
269
+ mapping[relator_code&.to_sym]
253
270
  end
254
271
 
255
272
  # Get 650 & 880 for Provenance and Chronology: prefix should be 'PRO' or 'CHR' and may be preceded by a '%'
256
273
  # @note 11/2018: do not display $5 in PRO or CHR subjs
257
- # @param [MARC::Record] record
258
- # @param [String] prefix to select from subject field
274
+ # @param record [MARC::Record]
275
+ # @param prefix [String] to select from subject field
259
276
  # @return [Array] array of values
260
277
  def prefixed_subject_and_alternate(record, prefix)
261
278
  record.fields(%w[650 880]).filter_map { |field|
@@ -273,16 +290,16 @@ module PennMARC
273
290
 
274
291
  # Does the given field specify an allowed source code?
275
292
  #
276
- # @param [MARC::DataField] field
293
+ # @param field [MARC::DataField]
277
294
  # @return [Boolean]
278
295
  def valid_subject_genre_source_code?(field)
279
296
  subfield_value_in?(field, '2', PennMARC::HeadingControl::ALLOWED_SOURCE_CODES)
280
297
  end
281
298
 
282
299
  # Does a field or its linked alternate match any of the specified tags?
283
- # @param [MARC::Field] field
284
- # @param [Array<String>] tags
285
- # @return [TrueClass, FalseClass]
300
+ # @param field [MARC::Field]
301
+ # @param tags [Array<String>]
302
+ # @return [Boolean]
286
303
  def field_or_its_linked_alternate?(field, tags)
287
304
  return true if field.tag.in? tags
288
305
  return true if field.tag == '880' && subfield_value?(field, '6', /^(#{tags.join('|')})/)
@@ -291,7 +308,7 @@ module PennMARC
291
308
  end
292
309
 
293
310
  # Match any open dates ending a given string to determine join separator for relator term in 1xx/7xx fields.
294
- # @param [String] str
311
+ # @param str [String]
295
312
  # @return [String (frozen)]
296
313
  def relator_join_separator(str)
297
314
  /\b\d+-\z/.match?(str) ? ' ' : ', '
@@ -302,7 +319,7 @@ module PennMARC
302
319
  # {https://www.loc.gov/marc/bibliographic/bd111.html 111}, {https://www.loc.gov/marc/bibliographic/bd411.html 411},
303
320
  # {https://www.loc.gov/marc/bibliographic/bd611.html 611}, {https://www.loc.gov/marc/bibliographic/bd711.html 711},
304
321
  # {https://www.loc.gov/marc/bibliographic/bd811.html 811}
305
- # @param [MARC:Field] field
322
+ # @param field [MARC:Field]
306
323
  # @return [String (frozen)]
307
324
  def relator_term_subfield(field)
308
325
  field_or_its_linked_alternate?(field, %w[111 411 611 711 811]) ? 'j' : 'e'
@@ -311,10 +328,10 @@ module PennMARC
311
328
  # Appends a relator value to the given string. It prioritizes relator codes found in subfield $4
312
329
  # and falls back to the specified relator term subfield (defaulting to 'e') if no valid codes are found in $4.
313
330
  # Use with 1xx/7xx fields.
314
- # @param [MARC::Field] field where relator values are stored
315
- # @param [String] joined_subfields the string to which the relator is appended
316
- # @param [String] relator_term_sf MARC subfield that stores relator term
317
- # @param [Hash] relator_map
331
+ # @param field [MARC::Field] where relator values are stored
332
+ # @param joined_subfields [String] the string to which the relator is appended
333
+ # @param relator_term_sf [String] MARC subfield that stores relator term
334
+ # @param relator_map [Hash]
318
335
  # @return [String]
319
336
  def append_relator(field:, joined_subfields:, relator_term_sf:, relator_map: Mappers.relator)
320
337
  joined_subfields = trim_trailing(:comma, joined_subfields)
@@ -329,5 +346,17 @@ module PennMARC
329
346
 
330
347
  [joined_subfields, relator].compact_blank.join(join_separator).squish
331
348
  end
349
+
350
+ # Returns a relator value of the given field. Like append_relator, it prioritizes relator codes found in subfileld
351
+ # $4 and falls back to the specified relator term subfield relator_term_sf if no valid codes are found in $4
352
+ # @param [MARC::Field] field where relator values are stored
353
+ # @param [String] relator_term_sf MARC subfield that stores relator term
354
+ # @param [Hash] relator_map
355
+ # @return [String]
356
+ def relator(field:, relator_term_sf:, relator_map: Mappers.relator)
357
+ relator = subfield_values(field, '4').filter_map { |code| translate_relator(code, relator_map) }
358
+ relator = subfield_values(field, relator_term_sf) if relator.blank?
359
+ relator.join
360
+ end
332
361
  end
333
362
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PennMARC
4
- VERSION = '1.0.25'
4
+ VERSION = '1.0.26'
5
5
  end
data/lib/pennmarc.rb CHANGED
@@ -2,5 +2,12 @@
2
2
 
3
3
  $LOAD_PATH.unshift(__dir__) unless $LOAD_PATH.include?(__dir__)
4
4
 
5
+ module PennMARC
6
+ # Autoload MARC helpers
7
+ module Test
8
+ autoload :MarcHelpers, 'pennmarc/test/marc_helpers'
9
+ end
10
+ end
11
+
5
12
  require_relative 'pennmarc/parser'
6
13
  require 'library_stdnums'
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  describe 'PennMARC::Access' do
4
- include MarcSpecHelpers
5
-
6
4
  let(:helper) { PennMARC::Access }
7
5
 
8
6
  describe '.facet' do
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  describe 'PennMARC::Citation' do
4
- include MarcSpecHelpers
5
-
6
4
  let(:helper) { PennMARC::Citation }
7
5
 
8
6
  describe '.cited_in_show' do