Class: Pubid::Nist::Identifiers::Base

Inherits:
Identifier
  • Object
show all
Defined in:
lib/pubid/nist/identifiers/base.rb

Overview

Base NIST/NBS identifier class Each series type inherits from this and overrides series_code

Constant Summary collapse

EQUALITY_IGNORED_ATTRS =

Attributes that are build artifacts or rendering aliases, not part of an identifier’s logical identity. They diverge between equally- valid spellings of the same id (e.g. long “Rev. 1” vs short “r1”):

- edition_component: redundant alias of :edition
- first_number/second_number: decomposed parts of the canonical
  :number, retained from the parse for building
- parsed_format: records the input format for round-trip rendering
%i[
  edition_component first_number second_number parsed_format
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Identifier

#base_identifier, #mr_number, #mr_number_with_part, #mr_part, #mr_publisher, #mr_type, #mr_year, #new_edition_of?, polymorphic_name, #render, #resolve_urn_generator, #root, #to_mr_string, #to_supplement_s, #to_urn, #urn_supplement_type, #urn_type_code, #year

Constructor Details

#initialize(**attributes) ⇒ Base

Returns a new instance of Base.



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/pubid/nist/identifiers/base.rb', line 74

def initialize(**attributes)
  super()

  attrs = self.class.attributes
  attributes.each do |key, value|
    next if value.nil?

    setter = :"#{key}="
    public_send(setter, value) if attrs.key?(key)
  end

  # NOTE: Compound number building is handled by the Builder class
  # Do NOT build compound numbers here - let the builder apply special patterns first
  # See lib/pubid/nist/builder.rb lines 368-472 for compound number logic
end

Class Method Details

.typed_stagesObject

Default: no typed stages. Subclasses override as needed.



10
11
12
# File 'lib/pubid/nist/identifiers/base.rb', line 10

def self.typed_stages
  []
end

Instance Method Details

#==(other) ⇒ Object Also known as: eql?

Logical identity comparison: equal when every attribute except the build artifacts/aliases above matches. (Edition#== already ignores its rendering-only original_prefix.)



104
105
106
107
108
109
110
# File 'lib/pubid/nist/identifiers/base.rb', line 104

def ==(other)
  return false unless other.instance_of?(self.class)

  self.class.attributes.each_key.all? do |name|
    EQUALITY_IGNORED_ATTRS.include?(name) || send(name) == other.send(name)
  end
end

#edition_greater?(edition1, edition2) ⇒ Boolean

Helper to compare edition values numerically

Returns:

  • (Boolean)

    true if edition1 is greater than edition2



272
273
274
275
276
# File 'lib/pubid/nist/identifiers/base.rb', line 272

def edition_greater?(edition1, edition2)
  num1 = extract_edition_number(edition1)
  num2 = extract_edition_number(edition2)
  num1 && num2 && num1 > num2
end

#exclude(*args) ⇒ Object

Return a copy with the named attributes nil’d. Overrides Pubid::Identifier#exclude because NIST’s initialize is keyword-only (initialize(**attributes)) while the inherited exclude rebuilds via the positional self.class.new(attrs) form — passing a positional hash to a keyword-only initializer raises ArgumentError. Rebuild with the keyword splat instead.



151
152
153
154
155
156
157
158
159
# File 'lib/pubid/nist/identifiers/base.rb', line 151

def exclude(*args)
  excluded_args = args.dup
  excluded_args << :date if excluded_args.delete(:year)

  attrs = self.class.attributes.each_with_object({}) do |(name, _), h|
    h[name] = excluded_args.include?(name) ? nil : send(name)
  end
  self.class.new(**attrs)
end

#extract_edition_number(edition) ⇒ Integer?

Extract numeric value from edition (r3 -> 3, r5 -> 5, e2 -> 2)

Returns:

  • (Integer, nil)

    the edition number or nil if not extractable



280
281
282
283
284
285
286
# File 'lib/pubid/nist/identifiers/base.rb', line 280

def extract_edition_number(edition)
  # Handle both String and Edition component
  edition_str = edition.to_s
  # Match patterns like r3, r5, e2, etc.
  match = edition_str.match(/^[er]?(\d+)$/)
  match ? match[1].to_i : nil
end

#hashObject



114
115
116
117
118
119
# File 'lib/pubid/nist/identifiers/base.rb', line 114

def hash
  vals = self.class.attributes.each_key.reject do |name|
    EQUALITY_IGNORED_ATTRS.include?(name)
  end.map { |name| send(name) }
  [self.class, *vals].hash
end

#languageObject

Backward compatibility: language method returns translation_component This allows tests to use parsed.language instead of parsed.translation_component



193
194
195
# File 'lib/pubid/nist/identifiers/base.rb', line 193

def language
  translation_component
end

#matches?(candidate) ⇒ Boolean

Wildcard / partial-identifier match. Treats self as a QUERY pattern and candidate as a concrete document: every ID part SET on the query must equal the candidate’s, while parts left unset (nil/empty) are wildcards that match any value. So a query carrying no edition and no supplement matches that document across ALL editions, years, and supplements — the basis for “select docs by ID parts”.

Asymmetric (unlike ==): “NBS CIRC 25”.matches?(“NBS CIRC 25sup1924”) is true, but not the reverse. The candidate must be the same class or a subclass so series-level identity still holds.

Returns:

  • (Boolean)


131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/pubid/nist/identifiers/base.rb', line 131

def matches?(candidate)
  return false unless candidate.is_a?(self.class)

  self.class.attributes.each_key.all? do |name|
    next true if EQUALITY_IGNORED_ATTRS.include?(name)

    query_val = public_send(name)
    next true if query_val.nil? ||
      (query_val.respond_to?(:empty?) && query_val.empty?)

    query_val == candidate.public_send(name)
  end
end

#merge(document) ⇒ Base

Merge another document into this one Used for combining document data, preferring more specific values

Parameters:

  • document (Base)

    another NIST document to merge

Returns:

  • (Base)

    self with merged attributes



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/pubid/nist/identifiers/base.rb', line 236

def merge(document)
  return self unless document.is_a?(Base)

  attrs = self.class.attributes
  attrs.each_key do |var_name|
    next if var_name == :rendering_style
    next if var_name == :parsed_format

    current_val = public_send(var_name)
    new_val = document.public_send(var_name)
    next unless new_val

    should_merge = case var_name
                   when :publisher, :series, :number
                     true
                   when :edition
                     current_val.nil? || edition_greater?(new_val,
                                                          current_val)
                   when :volume, :part, :version, :revision
                     current_val.nil? || (new_val.to_s.length > current_val.to_s.length)
                   when :supplement, :errata, :index, :insert, :section, :appendix, :translation
                     true
                   when :year, :month, :update, :draft
                     true
                   else
                     false
                   end

    public_send(:"#{var_name}=", new_val) if should_merge
  end

  self
end

#publisherString

Generate URN for this identifier

Returns:

  • (String)

    URN representation



18
# File 'lib/pubid/nist/identifiers/base.rb', line 18

attribute :publisher, Components::Publisher

#revisionString?

Compute revision from edition component for backward compatibility

Returns:

  • (String, nil)

    revision string (e.g., “r5”) or nil



176
177
178
179
180
181
182
183
# File 'lib/pubid/nist/identifiers/base.rb', line 176

def revision
  return @revision if @revision

  # Compute from edition component if available
  if edition&.type && edition.id
    "#{edition.type}#{edition.id}"
  end
end

#series_codeObject

Default series_code — subclasses override to provide a normalized series name.



70
71
72
# File 'lib/pubid/nist/identifiers/base.rb', line 70

def series_code
  nil
end

#supplement_shortObject

Short-form supplement fragment (“sup”, “sup1924”, “supJan1924”, “suprev”, “ supJun1925-Jun1926”), rendered from the structured component. A present-but-empty component is the bare “sup” marker; a number-less date range gets the leading space the number would have supplied. Shared by base and the per-series to_short_style overrides.



166
167
168
169
170
171
172
# File 'lib/pubid/nist/identifiers/base.rb', line 166

def supplement_short
  return "" unless supplement

  prefix = (supplement.range? && !number) ? " " : ""
  rendered = supplement.to_s(:short)
  prefix + (rendered.empty? ? "sup" : rendered)
end

#to_s(format = nil) ⇒ Object

Generate identifier string in specified format

Parameters:

  • format (:full, :long, :abbreviated, :short, :mr) (defaults to: nil)

    output format



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/pubid/nist/identifiers/base.rb', line 199

def to_s(format = nil)
  # Handle both keyword argument (hash) and positional argument (symbol/string)
  format = format[:format] if format.is_a?(Hash)

  # Default to parsed_format if available (preserves input format on round-trip)
  # Falls back to :short format for output (normalization)
  # Explicit format parameter always overrides parsed_format
  effective_format = format || parsed_format&.to_sym || :short
  effective_format = effective_format.to_sym if effective_format.is_a?(String)
  case effective_format
  when :full, :long
    to_full_style
  when :abbreviated, :abbrev
    to_abbreviated_style
  when :short
    to_short_style
  when :mr
    to_mr_style
  else
    to_short_style
  end
end

#translationObject

Backward compatibility: translation method returns translation_component This allows tests to use parsed.translation.language instead of parsed.translation_component.language



187
188
189
# File 'lib/pubid/nist/identifiers/base.rb', line 187

def translation
  translation_component
end

#weightInteger

Returns weight based on amount of defined attributes Used for ranking identifiers by specificity for conflict resolution

Returns:

  • (Integer)

    weight score (higher = more specific)



225
226
227
228
229
230
# File 'lib/pubid/nist/identifiers/base.rb', line 225

def weight
  self.class.attributes.keys.inject(0) do |sum, key|
    val = public_send(key)
    val && !val.to_s.empty? ? sum + 1 : sum
  end
end