Class: Pubid::Itu::Identifier

Inherits:
Pubid::Identifier show all
Defined in:
lib/pubid/itu/identifiers/base.rb

Overview

Base class for all ITU identifiers. Canonical name Pubid::Itu::Identifier.

Constant Summary collapse

LANGUAGES =

Long-form ↔ ITU single-letter language code map. The parser produces single-letter codes (E/F/S/R/A/C); API callers (e.g. metanorma-itu) pass long-form (en/fr/es/ru/ar/zh). Storage is normalized to the single-letter form. Languages with no canonical letter (e.g. "de") pass through unchanged and produce no trailing suffix.

{
  "fr" => "F", "es" => "S", "ru" => "R",
  "ar" => "A", "zh" => "C", "en" => "E",
  "F" => "F", "S" => "S", "R" => "R",
  "A" => "A", "C" => "C", "E" => "E"
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Pubid::Identifier

apply_mappings, #base, #base_document, concrete_class_for, #dated_version_of?, #draft_of?, #drop_supplements, #edition_of?, #eql?, #exclude, from_hash, #has_supplement?, #hash, #includes?, #matches?, #mr_all_parts, #mr_edition, #mr_languages, #mr_number, #mr_part, #mr_subpart, #mr_supplement_suffix, #mr_year, #new_edition_of?, polymorphic_name, polymorphic_type_map, #related_to?, #render, #resolve_urn_generator, #root, #sibling_of?, #supplement_of?, #to_hash, #to_mr_string, #to_slug, #to_supplement_s, #to_urn, #urn_supplement_type, #urn_type_code, #year

Constructor Details

#initialize(**kwargs) ⇒ Identifier

Returns a new instance of Identifier.



77
78
79
80
81
82
83
84
85
# File 'lib/pubid/itu/identifiers/base.rb', line 77

def initialize(**kwargs)
  if kwargs[:language]
    kwargs = kwargs.merge(language: normalize_language(kwargs[:language]))
  end

  super

  validate_ob_no_sector!
end

Class Method Details

.normalize_to_s_opts(opts) ⇒ Object

Translate language: opt to i18n_lang: opt. v1 PR #38 introduced i18n_lang: to disambiguate "rendering language" from the document language attribute; language: remains a deprecated alias.



240
241
242
243
244
245
246
247
248
# File 'lib/pubid/itu/identifiers/base.rb', line 240

def self.normalize_to_s_opts(opts)
  opts = opts.dup
  if opts.key?(:language) && !opts.key?(:i18n_lang)
    opts[:i18n_lang] = opts.delete(:language)
  else
    opts.delete(:language)
  end
  opts
end

.parse(identifier) ⇒ Object

Parse an ITU identifier string into an identifier object.



8
9
10
11
12
13
# File 'lib/pubid/itu/identifiers/base.rb', line 8

def self.parse(identifier)
  parsed = Parser.parse(normalize_whitespace(identifier))
  Builder.build(parsed)
rescue Parslet::ParseFailed => e
  raise "Failed to parse ITU identifier '#{identifier}': #{e.message}"
end

Instance Method Details

#==(other) ⇒ Object

Class-strict, like Supplement#== — the ITU type is part of the identity, not just the sector/series/number. Report ITU-R BT.2020-1 and Recommendation ITU-R BT.2020-1 are two different, both-current documents; an is_a? guard would make them equal (and #matches? resolve one to the other). Using self.class rather than a hard-coded class keeps the comparison symmetric in both directions, and closes the same one-way holes that existed against CombinedIdentifier and SpecialPublication.



258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/pubid/itu/identifiers/base.rb', line 258

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

  sector == other.sector &&
    series == other.series &&
    code == other.code &&
    date == other.date &&
    version == other.version &&
    # "the E.1100 series" is not Recommendation E.1100.
    series_word == other.series_word &&
    series_dash == other.series_dash &&
    attachment == other.attachment &&
    range_end == other.range_end &&
    language == other.language &&
    common_text_twin == other.common_text_twin
end

#base_from_kv(model, value) ⇒ Object



403
404
405
406
407
# File 'lib/pubid/itu/identifiers/base.rb', line 403

def base_from_kv(model, value)
  return unless value

  model.base = Pubid::Itu::Identifier.from_hash(value)
end

#base_to_kv(model, doc) ⇒ Object

Serialize a nested base identifier via its own to_hash so it collapses to the compact shape, and re-dispatch through the flavor base on load so _type resolves to the concrete subclass (a bare polymorphic cast would rebuild a plain Identifier and lose the subclass). Mirrors ETSI/JCGM.



395
396
397
398
399
400
401
# File 'lib/pubid/itu/identifiers/base.rb', line 395

def base_to_kv(model, doc)
  return unless model.base

  doc.add_child(
    Lutaml::KeyValue::DataModel::Element.new("base", model.base.to_hash),
  )
end

#code_for(model) ⇒ Object



437
438
439
# File 'lib/pubid/itu/identifiers/base.rb', line 437

def code_for(model)
  model.code ||= Components::Code.new
end

#common_text_twin_from_kv(model, value) ⇒ Object



423
424
425
426
427
# File 'lib/pubid/itu/identifiers/base.rb', line 423

def common_text_twin_from_kv(model, value)
  return unless value

  model.common_text_twin = ::Pubid::Identifier.from_hash(value)
end

#common_text_twin_to_kv(model, doc) ⇒ Object

The common-text twin ("| ISO/IEC 13818-1:2022") is another flavor's identifier (ISO/IEC …); serialize it via its own flat to_hash and reconstruct it cross-flavor through the top-level polymorphic router so its concrete class (and custom mappings) are restored by _type.



413
414
415
416
417
418
419
420
421
# File 'lib/pubid/itu/identifiers/base.rb', line 413

def common_text_twin_to_kv(model, doc)
  return unless model.common_text_twin

  doc.add_child(
    Lutaml::KeyValue::DataModel::Element.new(
      "common_text_twin", model.common_text_twin.to_hash
    ),
  )
end

#date_for(model) ⇒ Object



441
442
443
# File 'lib/pubid/itu/identifiers/base.rb', line 441

def date_for(model)
  model.date ||= Pubid::Components::Date.new
end

#emit_kv(doc, key, value) ⇒ Object



429
430
431
432
433
434
435
# File 'lib/pubid/itu/identifiers/base.rb', line 429

def emit_kv(doc, key, value)
  return if value.nil? || value.to_s.empty?

  doc.add_child(
    Lutaml::KeyValue::DataModel::Element.new(key, value.to_s),
  )
end

#exclude_from_nested(value, args) ⇒ Object

ITU keeps the document number and its edition/part together inside one Code component (like ETSI, unlike ISO's separate part attribute), so a top-level exclude(:part) can't reach the part. Handle it during the nested-exclusion pass: when :part/:parts is excluded, return a copy of the Code with its parts cleared while keeping number/series/subseries. This lets a part-less reference (e.g. ITU-R P.838) match every edition (ITU-R P.838-3) via matches?(other, ignore: [:parts]). Any non-Code value falls through to super, so nested identifiers (common_text_twin, supplement base, combined Designations) still recurse.



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/pubid/itu/identifiers/base.rb', line 110

def exclude_from_nested(value, args)
  part_keys = args & %i[part parts]
  if value.is_a?(Pubid::Itu::Components::Code) && !part_keys.empty?
    return Pubid::Itu::Components::Code.new(
      imp_marker: value.imp_marker,
      number: value.number,
      series_suffix: value.series_suffix,
      series_suffix_spaced: value.series_suffix_spaced,
      subseries: value.subseries,
      parts: [],
      qualifier: value.qualifier,
      qualifier_glued: value.qualifier_glued,
    )
  end

  super
end

#imp_marker_from_kv(model, value) ⇒ Object



303
304
305
# File 'lib/pubid/itu/identifiers/base.rb', line 303

def imp_marker_from_kv(model, value)
  code_for(model).imp_marker = value.to_s
end

#imp_marker_to_kv(model, doc) ⇒ Object



299
300
301
# File 'lib/pubid/itu/identifiers/base.rb', line 299

def imp_marker_to_kv(model, doc)
  emit_kv(doc, "imp_marker", model.code&.imp_marker)
end

#month_from_kv(model, value) ⇒ Object



387
388
389
# File 'lib/pubid/itu/identifiers/base.rb', line 387

def month_from_kv(model, value)
  date_for(model).month = value.to_s
end

#month_to_kv(model, doc) ⇒ Object



383
384
385
# File 'lib/pubid/itu/identifiers/base.rb', line 383

def month_to_kv(model, doc)
  emit_kv(doc, "month", model.date&.month)
end

#mr_number_with_partObject



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/pubid/itu/identifiers/base.rb', line 146

def mr_number_with_part
  segments = []
  segments << series&.series&.to_s&.downcase if series&.series
  # The edition word and the qualifier letter are part of the document's
  # identity, so they must reach the slug: without them "D.200" and
  # "D.200 R" — and, worse, "Q.2931 B" and "Q.2931 C" — collapse onto
  # one MR string. They are glued to the number rather than given their
  # own "-" segment so they read as part of it ("x-50bis", "d-200r").
  number = code&.number&.to_s
  if number
    number += code.series_suffix.to_s if code.series_suffix
    number += code.qualifier.to_s.downcase if code.qualifier
    segments << number
  end
  segments << code&.subseries&.to_s if code&.subseries
  segments.concat(code&.parts&.map(&:to_s) || [])
  # Same reasoning as the suffixes above, and as the URN generator: these
  # three distinguish documents that otherwise slug identically.
  segments << "series" if series_word
  segments << "attachment" if attachment
  segments << "to-#{range_end.to_s.downcase.tr('.', '-')}" if range_end
  return nil if segments.empty?

  segments.join("-")
end

#mr_publisherObject

ITU encodes identity across sector (T/R/CD), series (G/H/J…), and code (number with optional part), not in the inherited number/ typed_stage. The generic MrString renderer would otherwise drop all three and emit just ITU. Losslessness for issue #142 requires the sector letter, series letter, and document number to appear in MR. Lowercased to match the all-lowercase MR convention.



138
139
140
# File 'lib/pubid/itu/identifiers/base.rb', line 138

def mr_publisher
  publisher&.to_s&.downcase
end

#mr_typeObject



142
143
144
# File 'lib/pubid/itu/identifiers/base.rb', line 142

def mr_type
  sector&.sector&.to_s&.downcase
end

#numberObject

The document number lives on the code component for ITU; surface it at the identifier root so consumers that key on #number — e.g. Relaton::Index's sort/bsearch narrowing on id.root.number.to_s — work without special-casing ITU. Returns the code.number string (e.g. "530"); serialization is unaffected because the flat key_value block maps "number" via number_to_kv/number_from_kv, which read/write code.number directly and never touch the inherited number attribute. Supplement/Amendment/Corrigendum/Errata declare their own :string number (the ordinal), which overrides this reader; their base document number is still reachable via root.number.



97
98
99
# File 'lib/pubid/itu/identifiers/base.rb', line 97

def number
  code&.number
end

#number_from_kv(model, value) ⇒ Object



311
312
313
# File 'lib/pubid/itu/identifiers/base.rb', line 311

def number_from_kv(model, value)
  code_for(model).number = value.to_s
end

#number_to_kv(model, doc) ⇒ Object



307
308
309
# File 'lib/pubid/itu/identifiers/base.rb', line 307

def number_to_kv(model, doc)
  emit_kv(doc, "number", model.code&.number)
end

#parts_from_kv(model, value) ⇒ Object



371
372
373
# File 'lib/pubid/itu/identifiers/base.rb', line 371

def parts_from_kv(model, value)
  code_for(model).parts = Array(value).map(&:to_s)
end

#parts_to_kv(model, doc) ⇒ Object



362
363
364
365
366
367
368
369
# File 'lib/pubid/itu/identifiers/base.rb', line 362

def parts_to_kv(model, doc)
  parts = model.code&.parts
  return if parts.nil? || parts.empty?

  doc.add_child(
    Lutaml::KeyValue::DataModel::Element.new("parts", parts.map(&:to_s)),
  )
end

#qualifier_from_kv(model, value) ⇒ Object



340
341
342
# File 'lib/pubid/itu/identifiers/base.rb', line 340

def qualifier_from_kv(model, value)
  code_for(model).qualifier = value.to_s
end

#qualifier_glued_from_kv(model, value) ⇒ Object



350
351
352
# File 'lib/pubid/itu/identifiers/base.rb', line 350

def qualifier_glued_from_kv(model, value)
  code_for(model).qualifier_glued = value
end

#qualifier_glued_to_kv(model, doc) ⇒ Object



344
345
346
347
348
# File 'lib/pubid/itu/identifiers/base.rb', line 344

def qualifier_glued_to_kv(model, doc)
  return unless model.code&.qualifier_glued

  doc["qualifier_glued"] = true
end

#qualifier_to_kv(model, doc) ⇒ Object



336
337
338
# File 'lib/pubid/itu/identifiers/base.rb', line 336

def qualifier_to_kv(model, doc)
  emit_kv(doc, "qualifier", model.code&.qualifier)
end

#render_base(**_opts) ⇒ Object



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/pubid/itu/identifiers/base.rb', line 193

def render_base(**_opts)
  result = "#{publisher}-#{sector}"

  # Add series and code. A series with no code is a series GROUP
  # ("ITU-T G-100 series"); without this branch it rendered a dangling
  # trailing dot.
  result += if series && code
              " #{series}#{series_dash ? '-' : '.'}#{code}"
            elsif series
              " #{series}"
            else
              " #{code}"
            end

  result += "-#{range_end}" if range_end
  result += " series" if series_word
  result += " attachment" if attachment

  # Add version marker if present — always between code and date
  result += " (V#{version})" if version

  result + render_date_suffix
end

#render_date_suffixObject

" (MM/YYYY)", or " (YYYY)" when the month is unknown; "" when undated. Shared by every type that prints its date this way (Supplement and its subclasses, CombinedIdentifier, AnnexOfRecommendation). NOT used by SpecialPublication, which zero-pads its month.



221
222
223
224
225
# File 'lib/pubid/itu/identifiers/base.rb', line 221

def render_date_suffix
  return "" unless date

  date.month ? " (#{date.month}/#{date.year})" : " (#{date.year})"
end

#render_language_suffixObject



227
228
229
230
231
232
233
234
235
# File 'lib/pubid/itu/identifiers/base.rb', line 227

def render_language_suffix
  return "" unless language
  # Only render canonical single-letter ITU language codes
  # (e.g. "F", "S"). Languages with no mapping (e.g. "de") get
  # no suffix — matches v1 PR #38 render_language behavior.
  return "" unless LANGUAGES.value?(language)

  "-#{language}"
end

#sector_from_kv(model, value) ⇒ Object



287
288
289
# File 'lib/pubid/itu/identifiers/base.rb', line 287

def sector_from_kv(model, value)
  model.sector = Components::Sector.new(sector: value.to_s)
end

#sector_to_kv(model, doc) ⇒ Object

--- Compact flat key_value converters (shared) -------------------- Collapse the identity components (Sector/Series/Code/Date) to bare top-level scalars so the serialized hash is flat and index-friendly, mirroring ISO/ETSI/JCGM/OIML. Used by the per-leaf key_value blocks (see StandardSerialization) and by the Supplement/Annex/Combined blocks. publisher ("ITU") is intentionally never mapped — it is reconstructed from its attribute default.



283
284
285
# File 'lib/pubid/itu/identifiers/base.rb', line 283

def sector_to_kv(model, doc)
  emit_kv(doc, "sector", model.sector&.sector)
end

#series_from_kv(model, value) ⇒ Object



295
296
297
# File 'lib/pubid/itu/identifiers/base.rb', line 295

def series_from_kv(model, value)
  model.series = Components::Series.new(series: value.to_s)
end

#series_suffix_from_kv(model, value) ⇒ Object



319
320
321
# File 'lib/pubid/itu/identifiers/base.rb', line 319

def series_suffix_from_kv(model, value)
  code_for(model).series_suffix = value.to_s
end

#series_suffix_spaced_from_kv(model, value) ⇒ Object



332
333
334
# File 'lib/pubid/itu/identifiers/base.rb', line 332

def series_suffix_spaced_from_kv(model, value)
  code_for(model).series_suffix_spaced = value
end

#series_suffix_spaced_to_kv(model, doc) ⇒ Object

The three spelling/qualifier fields of Components::Code. The two booleans emit only when true, so no already-published index row gains a key; qualifier emits only when present.



326
327
328
329
330
# File 'lib/pubid/itu/identifiers/base.rb', line 326

def series_suffix_spaced_to_kv(model, doc)
  return unless model.code&.series_suffix_spaced

  doc["series_suffix_spaced"] = true
end

#series_suffix_to_kv(model, doc) ⇒ Object



315
316
317
# File 'lib/pubid/itu/identifiers/base.rb', line 315

def series_suffix_to_kv(model, doc)
  emit_kv(doc, "series_suffix", model.code&.series_suffix)
end

#series_to_kv(model, doc) ⇒ Object



291
292
293
# File 'lib/pubid/itu/identifiers/base.rb', line 291

def series_to_kv(model, doc)
  emit_kv(doc, "series", model.series&.series)
end

#subseries_from_kv(model, value) ⇒ Object



358
359
360
# File 'lib/pubid/itu/identifiers/base.rb', line 358

def subseries_from_kv(model, value)
  code_for(model).subseries = value.to_s
end

#subseries_to_kv(model, doc) ⇒ Object



354
355
356
# File 'lib/pubid/itu/identifiers/base.rb', line 354

def subseries_to_kv(model, doc)
  emit_kv(doc, "subseries", model.code&.subseries)
end

#to_s(**opts) ⇒ Object

Render identifier as a string.

Parameters:

  • i18n_lang (Symbol, String)

    language for identifier text translation (e.g. :fr renders "Annex to" as "Annexe au"). Distinct from the identifier's language attribute, which is the document's own language and produces the trailing suffix like "-F".

  • language (Symbol, String)

    deprecated alias for i18n_lang.

  • format (Symbol)

    :long for title-style rendering of supported identifiers, otherwise the default short form.



186
187
188
189
190
191
# File 'lib/pubid/itu/identifiers/base.rb', line 186

def to_s(**opts)
  opts = self.class.normalize_to_s_opts(opts)
  result = render_base(**opts) + render_language_suffix
  result += " | #{common_text_twin}" if common_text_twin
  result
end

#year_from_kv(model, value) ⇒ Object



379
380
381
# File 'lib/pubid/itu/identifiers/base.rb', line 379

def year_from_kv(model, value)
  date_for(model).year = value.to_s
end

#year_to_kv(model, doc) ⇒ Object



375
376
377
# File 'lib/pubid/itu/identifiers/base.rb', line 375

def year_to_kv(model, doc)
  emit_kv(doc, "year", model.date&.year)
end