Class: Pubid::Itu::Builder

Inherits:
Object
  • Object
show all
Defined in:
lib/pubid/itu/builder.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.build(parsed_data) ⇒ Object



6
7
8
# File 'lib/pubid/itu/builder.rb', line 6

def self.build(parsed_data)
  new.build(parsed_data)
end

Instance Method Details

#build(data) ⇒ Object



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/pubid/itu/builder.rb', line 10

def build(data)
  # "Annex to ..." identifier — wraps a Special Publication base
  if data[:annex_to]
    return build_annex(data[:annex_to])
  end

  # Common-text twin ("| ISO/IEC ..." suffix) — extract before building
  # the base ITU identifier, then attach to whichever class the base
  # resolves to.
  twin = parse_common_text_twin(data.delete(:common_text_twin))

  # Labelled annex of a Recommendation — "ITU-T A.23 Annex A (06/2014)".
  # The annexed document is the :base subtree; :year/:month at this level
  # are the annex's own date, not the base's.
  if data[:annex_number]
    annex = build_annex_of_recommendation(data)
    annex.common_text_twin = twin if twin
    return annex
  end

  # Labelled appendix of a Recommendation — "ITU-T G.101 App. I
  # (05/2000)". Same shape as the annex above.
  if data[:appendix_number]
    appendix = build_appendix_of_recommendation(data)
    appendix.common_text_twin = twin if twin
    return appendix
  end

  # Operational Bulletin (Special Publication) — series == "OB" or
  # legacy long form ("Operational Bulletin No. ...").
  # The series_dash guard keeps a series-code "ITU-T OB-1" out of this
  # branch, which would drop the dash and render it "ITU OB No. 1".
  # Belt-and-braces: `series_code_body` already refuses "OB" + dash, so
  # nothing reaches here with both set — the guard documents that coupling
  # so removing the parser's `absent?` fails loudly rather than silently
  # rerouting.
  if (data[:series].to_s == "OB" && data[:series_dash].nil?) ||
      data[:_op_bull]
    sp = build_special_publication(data)
    sp.common_text_twin = twin if twin && sp
    return sp
  end

  # Check if this is a supplement identifier
  if data[:supplement_type]
    supp = build_supplement(data)
    supp.common_text_twin = twin if twin && supp
    return supp
  end

  # Handbook — "ITU-R 42.HDB"
  if data[:handbook_marker]
    return build_handbook(data)
  end

  # Question — numeric ("ITU-R 234-1/7:") or letter-series
  # ("ITU-R P.3/BL/7"). Both carry a :study_group marker.
  if data[:study_group]
    return build_question(data)
  end

  # Build basic recommendation or combined identifier
  sector = Components::Sector.new(sector: data[:sector].to_s)
  series = Components::Series.new(series: data[:series].to_s) if data[:series]
  code = build_code(data) if data[:number]
  date = build_date(data) if data[:year]

  # Combined (joint) recommendation — one or more additional
  # "/SERIES.CODE" designations after the primary (e.g. "G.780/Y.1351",
  # "G.780/Y.1351/Z.1362"). The primary stays on the base series/code.
  if data[:combined]
    return Identifiers::CombinedIdentifier.new(
      sector: sector,
      series: series,
      code: code,
      combined: build_designations(data[:combined]),
      date: date,
      version: data[:version]&.to_s,
      # with_series offers these slots on either side of
      # combined_suffixes, so a joint id can carry them too; not
      # forwarding them let "ITU-T H.350/X.1 attachment" compare equal to
      # the un-attached document.
      series_word: !data[:series_word].nil?,
      series_dash: !data[:series_dash].nil?,
      attachment: !data[:attachment].nil?,
      range_end: data[:range_end]&.to_s,
      language: data[:language]&.to_s,
      common_text_twin: twin,
    )
  end

  # A Report ("Report ITU-R BT.2020-1") is shaped exactly like a
  # Recommendation — only the leading word, captured as :report_marker,
  # tells the two apart, and it has to, because the two series number
  # independently and the same series/number is two real documents.
  klass = data[:report_marker] ? Identifiers::Report : Identifiers::Recommendation

  klass.new(
    sector: sector,
    series: series,
    code: code,
    date: date,
    version: data[:version]&.to_s,
    series_word: !data[:series_word].nil?,
    series_dash: !data[:series_dash].nil?,
    attachment: !data[:attachment].nil?,
    range_end: data[:range_end]&.to_s,
    language: data[:language]&.to_s,
    common_text_twin: twin,
  )
end

#build_annex(inner_data) ⇒ Object

Build "Annex to ..." identifier. The inner data is the Special Publication; the annex inherits its language.



160
161
162
163
164
165
166
# File 'lib/pubid/itu/builder.rb', line 160

def build_annex(inner_data)
  base = build_special_publication(inner_data)
  Identifiers::Annex.new(
    base: base,
    language: inner_data[:language]&.to_s,
  )
end

#build_annex_of_recommendation(data) ⇒ Object

Build a labelled annex of a Recommendation. sector/series/code are deliberately NOT copied from the base: they are not serialized on the wrapper, so copies would make a from_hash-rebuilt annex differ from the parsed one.



172
173
174
175
176
177
178
179
# File 'lib/pubid/itu/builder.rb', line 172

def build_annex_of_recommendation(data)
  Identifiers::AnnexOfRecommendation.new(
    base: build(data[:base]),
    number: data[:annex_number].to_s,
    date: data[:year] ? build_date(data) : nil,
    language: data[:language]&.to_s,
  )
end

#build_appendix_of_recommendation(data) ⇒ Object

Build a labelled appendix of a Recommendation. Like the annex above, sector/series/code are deliberately NOT copied from the base — they are not serialized on the wrapper, so copies would make a from_hash-rebuilt appendix differ from the parsed one.



185
186
187
188
189
190
191
192
193
# File 'lib/pubid/itu/builder.rb', line 185

def build_appendix_of_recommendation(data)
  Identifiers::AppendixOfRecommendation.new(
    base: build(data[:base]),
    number: data[:appendix_number].to_s,
    material: data[:appendix_material]&.to_s,
    date: data[:year] ? build_date(data) : nil,
    language: data[:language]&.to_s,
  )
end

#build_handbook(data) ⇒ Object

Build Handbook ("ITU-R 42.HDB").



196
197
198
199
200
201
202
# File 'lib/pubid/itu/builder.rb', line 196

def build_handbook(data)
  Identifiers::Handbook.new(
    sector: Components::Sector.new(sector: data[:sector].to_s),
    code: build_code(data),
    date: data[:year] ? build_date(data) : nil,
  )
end

#build_question(data) ⇒ Object

Build Question (numeric or letter-series).



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/pubid/itu/builder.rb', line 205

def build_question(data)
  series = if data[:series]
             Components::Series.new(series: data[:series].to_s)
           end

  Identifiers::Question.new(
    sector: Components::Sector.new(sector: data[:sector].to_s),
    series: series,
    code: build_code(data),
    study_group: data[:study_group].to_s,
    has_bl: !data[:has_bl].nil?,
    bracketed: !data[:bracketed].nil?,
    has_colon: !data[:question_colon].nil?,
  )
end

#build_special_publication(data) ⇒ Object

Build Special Publication (OB). Sector is silently dropped — OB is a cross-bureau publication and Identifier rejects sector+OB in its constructor.



149
150
151
152
153
154
155
156
# File 'lib/pubid/itu/builder.rb', line 149

def build_special_publication(data)
  Identifiers::SpecialPublication.new(
    series: Components::Series.new(series: "OB"),
    code: data[:number] ? build_code(data) : nil,
    date: data[:year] ? build_date(data) : nil,
    language: data[:language]&.to_s,
  )
end

#build_supplement(data) ⇒ Object



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
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
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/pubid/itu/builder.rb', line 221

def build_supplement(data)
  # Build the base identifier first
  base = build(data[:base]) if data[:base]

  # Determine supplement type
  supplement_type = data[:supplement_type].to_s.gsub(".", "")
  klass = case supplement_type
          when "Amd"
            Identifiers::Amendment
          when "Add"
            Identifiers::Addendum
          when "Cor"
            Identifiers::Corrigendum
          when "Err"
            Identifiers::Errata
          when "Suppl"
            Identifiers::Supplement
          else
            # Previously fell through to a nil class and died with
            # "undefined method 'new' for nil" one frame later. Any token
            # added to Parser#supplement_type without a class here should
            # say so.
            raise ArgumentError,
                  "Unknown ITU supplement type: " \
                  "#{data[:supplement_type].inspect}"
          end

  # Build supplement date (separate from base date)
  supplement_date = if data[:supplement_year]
                      Pubid::Components::Date.new(
                        year: data[:supplement_year].to_s,
                        month: data[:supplement_month]&.to_s,
                      )
                    end

  # Sector/series are copied from the base when it has them; the fallback
  # to this level's own tokens is guarded because an annex base keeps
  # sector/series/code on *its* base, leaving all three nil here while the
  # supplement level carries no sector token of its own.
  # An annex base keeps sector/series/code on *its* base, so reach through
  # it via #root; otherwise a supplement of an annex would carry none of
  # them and its MR string would collapse to a bare "itu.<date>".
  base_identity = base&.sector ? base : base&.root
  sector = base_identity&.sector ||
    (Components::Sector.new(sector: data[:sector].to_s) if data[:sector])
  series = base_identity&.series ||
    (Components::Series.new(series: data[:series].to_s) if data[:series])

  attrs = {
    sector: sector,
    series: series,
    code: base_identity&.code,
    base: base,
    number: data[:supplement_number].to_s,
    # The marker is the captured space, so its ABSENCE is the flag.
    number_glued: data[:supplement_space].nil?,
    slash_joined: !data[:supplement_slash].nil?,
    date: supplement_date,
    language: data[:language]&.to_s,
  }
  # The "series" word rides on the supplement for the base-less,
  # series-only form; with a base it is the base's, and renders from
  # there. It is copied up anyway (like sector/series/code above) so the
  # supplement's own MR slug stays distinct — `render_supplement` and the
  # base-nil-guarded key_value/== only consult it when base is nil, so
  # the copy is render- and serialization-neutral.
  attrs[:series_word] = if base
                          !!base_identity&.series_word
                        else
                          !data[:series_word].nil?
                        end
  attrs[:technical] = true if data[:technical] &&
    klass == Identifiers::Corrigendum

  klass.new(**attrs)
end

#parse_common_text_twin(twin_raw) ⇒ Object

Parse the common-text twin string ("| ISO/IEC 13818-1:2022") into the appropriate flavor's identifier object. Returns nil if the twin is empty or unparseable.



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/pubid/itu/builder.rb', line 125

def parse_common_text_twin(twin_raw)
  return nil if twin_raw.nil?

  twin_str = twin_raw.to_s.strip
  # Strip a leading pipe if the parser captured it
  twin_str = twin_str.sub(/\A\s*\|\s*/, "")
  return nil if twin_str.empty?

  case twin_str
  when /\AISO\/IEC\b/, /\AISO\b/
    Pubid::Iso.parse(twin_str)
  when /\AIEC\b/
    Pubid::Iec.parse(twin_str)
  end
rescue StandardError
  # If the twin doesn't parse as a known flavor, leave it as nil
  # rather than blowing up the whole parse — the ITU half is still
  # usable on its own.
  nil
end