pubid 2.0.0.pre.alpha.1 → 2.0.0.pre.alpha.2

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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/data/nist/update_codes.yaml +2 -0
  3. data/lib/pubid/amca/identifier.rb +39 -0
  4. data/lib/pubid/ansi/identifier.rb +42 -0
  5. data/lib/pubid/api/identifier.rb +47 -0
  6. data/lib/pubid/ashrae/identifier.rb +39 -0
  7. data/lib/pubid/asme/identifier.rb +46 -0
  8. data/lib/pubid/astm/identifier.rb +77 -0
  9. data/lib/pubid/bsi/identifier.rb +60 -0
  10. data/lib/pubid/ccsds/identifier.rb +68 -0
  11. data/lib/pubid/ccsds/identifiers/base.rb +11 -0
  12. data/lib/pubid/ccsds/single_identifier.rb +4 -1
  13. data/lib/pubid/cen_cenelec/identifier.rb +37 -0
  14. data/lib/pubid/cie/identifier.rb +53 -0
  15. data/lib/pubid/components/factory.rb +50 -0
  16. data/lib/pubid/components/typed_stage.rb +4 -0
  17. data/lib/pubid/components.rb +1 -0
  18. data/lib/pubid/csa/identifier.rb +56 -0
  19. data/lib/pubid/etsi/identifier.rb +43 -0
  20. data/lib/pubid/identifier.rb +8 -1
  21. data/lib/pubid/idf/identifier.rb +60 -0
  22. data/lib/pubid/iec/builder.rb +2 -1
  23. data/lib/pubid/iec/components/code.rb +2 -1
  24. data/lib/pubid/iec/components/publisher.rb +2 -1
  25. data/lib/pubid/iec/identifier.rb +235 -0
  26. data/lib/pubid/iec/identifiers/base.rb +4 -0
  27. data/lib/pubid/iec/identifiers/consolidated_identifier.rb +0 -4
  28. data/lib/pubid/iec/identifiers/fragment_identifier.rb +0 -4
  29. data/lib/pubid/iec/identifiers/sheet_identifier.rb +0 -4
  30. data/lib/pubid/iec/identifiers/vap_identifier.rb +0 -4
  31. data/lib/pubid/iec/parser.rb +7 -2
  32. data/lib/pubid/iec/urn_generator.rb +57 -171
  33. data/lib/pubid/iec/urn_parser.rb +53 -252
  34. data/lib/pubid/ieee/identifier.rb +41 -0
  35. data/lib/pubid/iho/identifier.rb +42 -0
  36. data/lib/pubid/iho/identifiers/base.rb +1 -1
  37. data/lib/pubid/iho/identifiers/bibliographic.rb +0 -4
  38. data/lib/pubid/iho/identifiers/circular_letter.rb +0 -4
  39. data/lib/pubid/iho/identifiers/miscellaneous.rb +0 -4
  40. data/lib/pubid/iho/identifiers/publication.rb +0 -4
  41. data/lib/pubid/iho/identifiers/standard.rb +0 -4
  42. data/lib/pubid/iho/urn_generator.rb +1 -1
  43. data/lib/pubid/iso/builder.rb +5 -1
  44. data/lib/pubid/iso/identifier.rb +261 -0
  45. data/lib/pubid/iso/parser.rb +4 -2
  46. data/lib/pubid/iso/scheme.rb +6 -0
  47. data/lib/pubid/iso/single_identifier.rb +6 -3
  48. data/lib/pubid/iso/urn_generator.rb +17 -3
  49. data/lib/pubid/iso/urn_parser.rb +16 -2
  50. data/lib/pubid/itu/identifier.rb +87 -22
  51. data/lib/pubid/jcgm/identifier.rb +43 -0
  52. data/lib/pubid/jis/identifier.rb +43 -0
  53. data/lib/pubid/nist/builder.rb +174 -5
  54. data/lib/pubid/nist/components/edition.rb +16 -0
  55. data/lib/pubid/nist/components/supplement.rb +88 -21
  56. data/lib/pubid/nist/identifier.rb +62 -0
  57. data/lib/pubid/nist/identifiers/base.rb +103 -24
  58. data/lib/pubid/nist/identifiers/circular_supplement.rb +1 -1
  59. data/lib/pubid/nist/identifiers/crpl_report.rb +1 -4
  60. data/lib/pubid/nist/identifiers/federal_information_processing_standards.rb +10 -0
  61. data/lib/pubid/nist/identifiers/report.rb +1 -2
  62. data/lib/pubid/nist/parser.rb +36 -3
  63. data/lib/pubid/nist/supplement_identifier.rb +8 -24
  64. data/lib/pubid/nist/urn_generator.rb +14 -8
  65. data/lib/pubid/nist.rb +1 -0
  66. data/lib/pubid/oiml/identifier.rb +50 -0
  67. data/lib/pubid/plateau/identifier.rb +57 -0
  68. data/lib/pubid/plateau.rb +1 -0
  69. data/lib/pubid/renderers/base.rb +34 -0
  70. data/lib/pubid/renderers/directives_renderer.rb +13 -14
  71. data/lib/pubid/renderers/guide_renderer.rb +5 -1
  72. data/lib/pubid/renderers/human_readable.rb +20 -8
  73. data/lib/pubid/renderers/iwa_renderer.rb +5 -1
  74. data/lib/pubid/renderers/supplement_renderer.rb +4 -1
  75. data/lib/pubid/rendering/context.rb +5 -2
  76. data/lib/pubid/sae/identifier.rb +23 -0
  77. data/lib/pubid/scheme.rb +12 -0
  78. data/lib/pubid/version.rb +1 -1
  79. metadata +5 -2
@@ -2,191 +2,77 @@
2
2
 
3
3
  module Pubid
4
4
  module Iec
5
+ # Generates IEC URNs in the legacy positional format used as ground truth
6
+ # by relaton-data-iec:
7
+ #
8
+ # urn:iec:std:{publisher}:{number}[-{part}]:{date}:{type}:{deliverable}:{language}[:{adjuncts}]
9
+ #
10
+ # e.g. +urn:iec:std:iec:60050-102:2007:::+ (type after date; trailing empty
11
+ # type/deliverable/language slots). This is a port of relaton-iec's
12
+ # +Relaton::Iec.code_to_urn+ operating on +identifier.to_s+, which
13
+ # round-trips faithfully. The all-parts series URN is the one exception: it
14
+ # omits the language slot (8 fields), so it has a dedicated branch.
5
15
  class UrnGenerator < Pubid::UrnGenerator::Base
6
- def generate
7
- case identifier
8
- when Identifiers::VapIdentifier
9
- generate_vap_urn
10
- when Identifiers::FragmentIdentifier
11
- generate_fragment_urn
12
- when Identifiers::SheetIdentifier
13
- generate_sheet_urn
14
- when SupplementIdentifier
15
- generate_supplement_urn
16
- else
17
- generate_base_urn
18
- end
19
- end
20
-
21
- private
22
-
23
- def generate_vap_urn
24
- base_gen = self.class.new(identifier.base_identifier)
25
- base_urn = base_gen.generate
26
-
27
- base_part = base_urn.sub(/^urn:iec:std:/, "")
28
-
29
- parts = ["urn", "iec", "std", base_part]
30
-
31
- if identifier.vap_suffix
32
- suffix = identifier.vap_suffix.to_s
33
- parts << "vap.#{suffix.downcase}"
34
- end
35
-
36
- if identifier.edition&.number
37
- parts << "ed.#{identifier.edition.number}"
38
- end
39
-
40
- parts.join(":")
41
- end
42
-
43
- def generate_fragment_urn
44
- base_gen = self.class.new(identifier.base_identifier)
45
- base_urn = base_gen.generate
46
-
47
- base_part = base_urn.sub(/^urn:iec:std:/, "")
48
-
49
- parts = ["urn", "iec", "std", base_part]
50
-
51
- if identifier.fragment_number
52
- frag_type = identifier.base_identifier.is_a?(Identifiers::Corrigendum) ? "fragc" : "frag"
53
- parts << "#{frag_type}.#{identifier.fragment_number}"
54
- end
55
-
56
- if identifier.edition&.number
57
- parts << "ed.#{identifier.edition.number}"
58
- end
59
-
60
- parts.join(":")
61
- end
62
-
63
- def generate_sheet_urn
64
- base_gen = self.class.new(identifier.base_identifier)
65
- base_urn = base_gen.generate
66
-
67
- base_part = base_urn.sub(/^urn:iec:std:/, "")
68
-
69
- parts = ["urn", "iec", "std", base_part]
70
-
71
- if identifier.sheet_number
72
- parts << "sheet.#{identifier.sheet_number}"
73
- end
74
-
75
- if identifier.year
76
- parts << identifier.year
77
- end
78
-
79
- parts.join(":")
80
- end
81
-
82
- def generate_base_urn
83
- parts = ["urn", "iec", "std"]
16
+ # Deliverable markers occupying the positional deliverable slot.
17
+ DELIVERABLES = /cmv|csv|exv|prv|rlv|ser/.freeze
84
18
 
85
- parts << publisher_component
86
-
87
- type_comp = type_component
88
- parts << type_comp if type_comp
89
-
90
- docnumber = docnumber_component
91
- parts << docnumber if docnumber
92
-
93
- date_str = urn_date_string(identifier.date)
94
- parts << date_str if date_str
95
-
96
- if identifier.stage_iteration
97
- parts << "iter.#{identifier.stage_iteration}"
98
- end
99
-
100
- if identifier.edition&.number
101
- parts << "ed.#{identifier.edition.number}"
102
- end
103
-
104
- if identifier.languages&.any?
105
- lang_codes = identifier.languages.map(&:code).join(",")
106
- parts << lang_codes
107
- end
19
+ def generate
20
+ return series_urn if identifier.respond_to?(:all_parts) && identifier.all_parts
108
21
 
109
- parts.join(":")
22
+ code_to_urn(identifier.to_s, urn_language)
110
23
  end
111
24
 
112
- def generate_supplement_urn
113
- current = identifier
114
- supplement_chain = []
115
-
116
- while current.is_a?(SupplementIdentifier)
117
- supplement_chain.unshift(current)
118
- current = current.base_identifier
119
- end
120
-
121
- base_id = current
122
-
123
- parts = ["urn", "iec", "std"]
124
-
125
- if base_id
126
- parts << publisher_component(base_id)
127
-
128
- type_comp = type_component(base_id)
129
- parts << type_comp if type_comp
130
-
131
- docnumber = docnumber_component(base_id)
132
- parts << docnumber if docnumber
133
-
134
- base_date_str = urn_date_string(base_id.date)
135
- parts << base_date_str if base_date_str
136
- end
137
-
138
- supplement_chain.each do |supp|
139
- suppl_type = supp.typed_stage&.type_code&.to_s
140
- parts << suppl_type if suppl_type
141
-
142
- supp_date_str = urn_date_string(supp.date)
143
- parts << supp_date_str if supp_date_str
144
-
145
- if supp.number
146
- parts << "v#{supp.number}"
147
- end
148
-
149
- if supp.stage_iteration
150
- parts << "iter.#{supp.stage_iteration}"
151
- end
152
- end
153
-
154
- parts.join(":")
155
- end
25
+ private
156
26
 
157
- def publisher_component(id = identifier)
158
- return "iec" unless id&.publisher
27
+ # Hyphen-joined language codes (e.g. "en-fr"), or nil when unset.
28
+ def urn_language
29
+ return nil unless identifier.respond_to?(:languages)
159
30
 
160
- copubs = id.copublishers || []
31
+ langs = identifier.languages
32
+ return nil unless langs&.any?
161
33
 
162
- publishers = [id.publisher] + copubs
163
- publishers.map(&:to_s).map(&:downcase).join("-")
34
+ langs.map(&:code).join("-")
164
35
  end
165
36
 
166
- def type_component(id = identifier)
167
- return nil unless id&.typed_stage
168
-
169
- type_code = id.typed_stage.type_code
170
- return nil if !type_code || type_code.to_s == "is"
171
-
172
- type_code.to_s
37
+ # The all-parts series URN drops the language slot and carries no part or
38
+ # date: urn:iec:std:iec:80000:::ser.
39
+ def series_urn
40
+ code = identifier.to_s.sub(/\s*\(all parts\)\s*\z/, "")
41
+ m = code.downcase.match(/(?<head>\S+)\s+(?<pnum>[\d-]+)/)
42
+ head = m[:head].split("/").join("-")
43
+ ["urn", "iec", "std", head, m[:pnum], "", "", "ser"].join(":")
173
44
  end
174
45
 
175
- def docnumber_component(id = identifier)
176
- return nil unless id&.number
177
-
178
- result = id.number.to_s
179
- result += "-#{id.part}" if id.part
180
- result += "-#{id.subpart}" if id.subpart
181
- result
46
+ # Port of Relaton::Iec.code_to_urn.
47
+ def code_to_urn(code, lang = nil)
48
+ rest = code.downcase.sub(%r{
49
+ (?<head>[^\s]+)\s
50
+ (?<type>is|ts|tr|pas|srd|guide|tec|wp)?(?(<type>)\s)
51
+ (?<pnum>[\d-]+)\s?
52
+ (?<_dd>:)?(?(<_dd>)(?<date>[\d-]+)\s?)
53
+ }x, "")
54
+ m = $~
55
+ return unless m && m[:head] && m[:pnum]
56
+
57
+ deliv = DELIVERABLES.match(code.downcase).to_s
58
+ urn = ["urn", "iec", "std", m[:head].split("/").join("-"), m[:pnum],
59
+ m[:date], m[:type], deliv, lang]
60
+ (urn + adjunct_to_urn(rest)).join(":")
182
61
  end
183
62
 
184
- def urn_date_string(date)
185
- return nil unless date
186
-
187
- s = date.year.to_s
188
- s += "-#{date.month}" if date.month
189
- s
63
+ # Port of Relaton::Iec.ajunct_to_urn — recursively emits amd/cor/ish
64
+ # adjuncts, prefixing "plus" for the "+" (consolidated) relation.
65
+ def adjunct_to_urn(rest)
66
+ r = rest.sub(%r{
67
+ (?<pl>\+|/)(?(<pl>)(?<adjunct>(?:amd|cor|ish))(?<adjnum>\d+)\s?)
68
+ (?<_d2>:)?(?(<_d2>)(?<adjdt>[\d-]+)\s?)
69
+ }x, "")
70
+ m = $~ || {}
71
+ return [] unless m[:adjunct]
72
+
73
+ plus = "plus" if m[:pl] == "+"
74
+ urn = [plus, m[:adjunct], m[:adjnum], m[:adjdt]]
75
+ urn + adjunct_to_urn(r)
190
76
  end
191
77
  end
192
78
  end
@@ -2,55 +2,23 @@
2
2
 
3
3
  module Pubid
4
4
  module Iec
5
- # Parses RFC 5141-bis compliant URNs into IEC identifiers
5
+ # Parses IEC URNs in the legacy positional format (relaton-data-iec ground
6
+ # truth):
6
7
  #
7
- # URN format: urn:iec:std:{publisher}:{type}:{number}[-{part}]:{year}:{supplements}
8
+ # urn:iec:std:{publisher}:{number}[-{part}]:{date}:{type}:{deliverable}:{language}[:{adjuncts}]
8
9
  #
9
10
  # Examples:
10
- # - urn:iec:std:iec:60050:2011
11
- # - urn:iec:std:iec:tr:60050:2011
12
- # - urn:iec:std:iec:60050-100:2011
13
- # - urn:iec:std:iec:60050:2011:amd:1:2020
11
+ # - urn:iec:std:iec:60050:2011:::
12
+ # - urn:iec:std:iec:62547:2013:tr:: (type after date)
13
+ # - urn:iec:std:iec:60050-102:2007:::::amd:1:2017
14
+ # - urn:iec:std:iec:60034-16-3:1996:ts::fr (deliverable empty, language fr)
15
+ # - urn:iec:std:iec:80000:::ser (all-parts series)
16
+ #
17
+ # This is a port of relaton-iec's +urn_to_code+: the positional fields are
18
+ # reassembled into a code string which is then run through the text parser
19
+ # (+Identifier.parse+), so there is a single source of truth for building
20
+ # the identifier object.
14
21
  class UrnParser
15
- # Reverse mappings from URN format to PubID components
16
- TYPED_STAGE_REVERSE_MAP = {
17
- "WD" => :wd,
18
- "WDS" => :wds,
19
- "CD" => :cd,
20
- "CDV" => :cdv,
21
- "DIS" => :dis,
22
- "FDIS" => :fdis,
23
- "PDAM" => :pdam,
24
- "DAM" => :dam,
25
- "FDAM" => :fdamd,
26
- "DCOR" => :dcor,
27
- "FDCOR" => :fdcor,
28
- "CDTS" => :cdts,
29
- "DTS" => :dts,
30
- "FDTS" => :fdts,
31
- "PRF" => :prf,
32
- "PWI" => :pwi,
33
- "NP" => :np,
34
- "AWI" => :awi,
35
- "NWIP" => :nwip,
36
- }.freeze
37
-
38
- SUPPLEMENT_TYPE_MAP = {
39
- "amd" => :amd,
40
- "cor" => :cor,
41
- }.freeze
42
-
43
- TYPE_CODE_REVERSE_MAP = {
44
- "tr" => :tr,
45
- "ts" => :ts,
46
- "pas" => :pas,
47
- "guide" => :guide,
48
- "isp" => :isp,
49
- "r" => :r,
50
- "sr" => :sr,
51
- "tap" => :tap,
52
- }.freeze
53
-
54
22
  # Parse IEC URN string
55
23
  # @param urn [String] URN string to parse
56
24
  # @return [Identifier] parsed identifier
@@ -62,227 +30,60 @@ module Pubid
62
30
  # @param urn [String] URN string
63
31
  # @return [Identifier] parsed identifier
64
32
  def parse_urn(urn)
65
- # Remove urn:iec:std: prefix
66
33
  unless urn.start_with?("urn:iec:std:")
67
- raise Errors::ParseError,
68
- "Invalid IEC URN: #{urn}"
34
+ raise Errors::ParseError, "Invalid IEC URN: #{urn}"
69
35
  end
70
36
 
71
- parts = urn.sub("urn:iec:std:", "").split(":")
72
-
73
- # Parse publisher(s) - first part
74
- publishers = parse_publisher(parts.shift)
75
-
76
- # Parse type - optional (defaults to IS)
77
- type_code = nil
78
- type_code = parse_type(parts.first) if parts.first && TYPE_CODE_REVERSE_MAP.key?(parts.first.downcase)
79
- parts.shift if type_code
80
-
81
- # Parse number
82
- number_part = parts.shift
83
- number, part, subpart = parse_number_part(number_part)
84
-
85
- # Check for stage (stage-XX.XX or typed stage like WD, CD, etc.)
86
- stage_code = nil
87
- stage_iteration = nil
88
- if parts.first&.start_with?("stage-")
89
- stage_code, stage_iteration = parse_stage_code(parts.shift)
90
- elsif TYPED_STAGE_REVERSE_MAP.key?(parts.first&.upcase)
91
- stage_abbr = parts.shift.upcase
92
- stage_code = TYPED_STAGE_REVERSE_MAP[stage_abbr]
93
- # Check for iteration (WD.2 format)
94
- if stage_abbr.include?(".")
95
- stage_code, stage_iteration = stage_abbr.split(".")
96
- stage_code = TYPED_STAGE_REVERSE_MAP[stage_code]
97
- stage_iteration = stage_iteration.to_i
98
- end
99
- end
100
-
101
- # Parse date if present (year or year-month)
102
- date = nil
103
- if parts.first&.match(/^\d{4}(-\d{2})?$/)
104
- date = parts.shift
105
- end
37
+ code, lang, all_parts = urn_to_code(urn)
38
+ raise Errors::ParseError, "Invalid IEC URN: #{urn}" unless code
106
39
 
107
- # Parse edition if present (ed.N format)
108
- edition = nil
109
- if parts.first&.start_with?("ed.")
110
- edition = parts.shift.sub("ed.", "").to_i
40
+ id = Pubid::Iec::Identifier.parse(code)
41
+ id.all_parts = true if all_parts && id.respond_to?(:all_parts=)
42
+ if lang && !lang.empty? && id.respond_to?(:languages=)
43
+ id.languages = [::Pubid::Components::Language.new(code: lang)]
111
44
  end
112
-
113
- # Check for supplements (amd, cor)
114
- supplements = []
115
- while parts.any?
116
- supp_type = nil
117
- supp_number = nil
118
- supp_date = nil
119
- supp_stage = nil
120
-
121
- # Check for supplement stage
122
- if parts.first&.start_with?("stage-")
123
- supp_stage_data = parts.shift
124
- supp_stage, = parse_stage_code(supp_stage_data)
125
- elsif TYPED_STAGE_REVERSE_MAP.key?(parts.first&.upcase)
126
- supp_stage_abbr = parts.shift.upcase
127
- supp_stage = TYPED_STAGE_REVERSE_MAP[supp_stage_abbr]
128
- end
129
-
130
- # Check for supplement type (amd, cor)
131
- if SUPPLEMENT_TYPE_MAP.key?(parts.first&.downcase)
132
- supp_type = SUPPLEMENT_TYPE_MAP[parts.shift.downcase]
133
- end
134
-
135
- # Check for version (v1, v2, etc.) or number
136
- if parts.first&.start_with?("v")
137
- version_str = parts.shift
138
- supp_number = version_str.sub("v", "").to_i
139
- elsif parts.first&.match(/^\d+$/)
140
- # Could be year or supplement number
141
- if parts.first&.match(/^\d{4}$/)
142
- # 4 digits = year
143
- supp_date = parts.shift
144
- else
145
- # 1-3 digits = supplement number
146
- supp_number = parts.shift.to_i
147
- end
148
- end
149
-
150
- # Next part might be year if not already set
151
- if supp_date.nil? && parts.first&.match(/^\d{4}(-\d{2})?$/)
152
- supp_date = parts.shift
153
- end
154
-
155
- # Safety: consume unrecognized token to prevent infinite loop
156
- unless supp_type || supp_number || supp_date || supp_stage
157
- parts.shift
158
- end
159
-
160
- supplements << {
161
- type: supp_type,
162
- number: supp_number,
163
- date: supp_date,
164
- stage: supp_stage,
165
- }
166
- end
167
-
168
- # Build the identifier hash
169
- build_identifier(publishers, number, part, subpart, type_code, stage_code, stage_iteration,
170
- date, edition, supplements)
45
+ id
171
46
  end
172
47
 
173
48
  private
174
49
 
175
- # Parse publisher component (iec, iso-iec, etc.)
176
- def parse_publisher(publisher_str)
177
- publisher_str.split("-").map(&:upcase)
178
- end
179
-
180
- # Parse type component
181
- def parse_type(type_str)
182
- TYPE_CODE_REVERSE_MAP[type_str.downcase]
183
- end
184
-
185
- # Parse number component (number, part, subpart)
186
- def parse_number_part(number_str)
187
- return [nil, nil, nil] unless number_str
188
-
189
- if number_str.include?("-")
190
- parts = number_str.split("-")
191
- number = parts[0]
192
- part = parts[1] if parts[1]
193
- subpart = parts[2] if parts[2]
194
- [number, part, subpart]
195
- else
196
- [number_str, nil, nil]
50
+ # Port of Relaton::Iec.urn_to_code. Reassembles the positional URN fields
51
+ # into a text code string. Returns [code, language, all_parts].
52
+ def urn_to_code(urn)
53
+ fields = urn.upcase.split(":")
54
+ return if fields.size < 5
55
+
56
+ head, num, date, type, deliv, lang = fields[3, 8]
57
+ all_parts = false
58
+
59
+ code = head.gsub("-", "/")
60
+ code += " #{type}" unless type.nil? || type.empty?
61
+ code += " #{num}"
62
+ code += ":#{date}" unless date.nil? || date.empty?
63
+ code += adjunct_to_code(fields[9..])
64
+
65
+ # "ser" marks an all-parts series rather than a deliverable suffix; it
66
+ # is signalled out-of-band (the code built from a series URN carries no
67
+ # part or date, so to_s renders "IEC NNNN (all parts)").
68
+ if deliv && deliv.casecmp("SER").zero?
69
+ all_parts = true
70
+ elsif deliv && !deliv.empty?
71
+ code += " #{deliv}"
197
72
  end
198
- end
199
-
200
- # Parse stage code (stage-XX.XX format)
201
- def parse_stage_code(stage_str)
202
- stage_code = stage_str.sub("stage-", "")
203
73
 
204
- if stage_code.include?(".")
205
- stage_code, iteration = stage_code.split(".")
206
- [stage_code.to_sym, iteration.to_i]
207
- else
208
- [stage_code.to_sym, nil]
209
- end
74
+ [code, lang&.downcase, all_parts]
210
75
  end
211
76
 
212
- # Build identifier from parsed components
213
- def build_identifier(publishers, number, part, subpart, type_code, stage_code, _stage_iteration,
214
- date, edition, supplements)
215
- # Start with base document hash
216
- base_hash = {
217
- publisher: publishers.first,
218
- copublishers: publishers[1..]&.map { |c| { copublisher: c } },
219
- }
220
-
221
- # Build number_with_part (expected by Builder)
222
- if part || subpart
223
- number_with_part = number
224
- number_with_part += "-#{part}" if part
225
- number_with_part += "-#{subpart}" if subpart
226
- base_hash[:number_with_part] = number_with_part
227
- else
228
- base_hash[:number] = number
229
- end
230
-
231
- base_hash[:date] = date if date
232
- base_hash[:edition] = edition if edition
233
-
234
- # Add type_with_stage if type_code is present
235
- if type_code && type_code != :is
236
- base_hash[:type_with_stage] = type_code.to_s.upcase
237
- end
238
-
239
- # Add stage if present
240
- if stage_code
241
- # Look up the typed stage abbreviation from stage_code
242
- typed_stage = Scheme.locate_typed_stage_by_stage_code(stage_code)
243
- if typed_stage
244
- base_hash[:type_with_stage] =
245
- typed_stage.abbr.is_a?(Array) ? typed_stage.abbr.first : typed_stage.abbr
246
- else
247
- # Fallback to harmonized stage notation
248
- base_hash[:stage] = stage_code.to_s.gsub(".", ".")
249
- end
250
- end
251
-
252
- # Build supplements recursively
253
- supplements.reverse_each do |supp|
254
- supp_hash = {}
255
-
256
- if supp[:stage]
257
- typed_stage = Scheme.locate_typed_stage_by_stage_code(supp[:stage])
258
- supp_hash[:type_with_stage] = if
259
- typed_stage
260
- typed_stage.abbr.is_a?(Array) ? typed_stage.abbr.first : typed_stage.abbr
261
- else
262
- supp[:stage].to_s.upcase
263
- end
264
- end
265
-
266
- supp_hash[:type_with_stage] ||= supp[:type].to_s.upcase if supp[:type]
267
-
268
- if supp[:number]
269
- supp_hash[:number] = supp[:number].to_s
270
- end
271
-
272
- if supp[:date]
273
- supp_hash[:date] = supp[:date].to_s
274
- end
275
-
276
- # Wrap current identifier with supplement
277
- base_hash = {
278
- base_identifier: base_hash,
279
- **supp_hash,
280
- }
281
- end
77
+ # Port of Relaton::Iec.ajanct_to_code recursively rebuilds amd/cor/ish
78
+ # adjuncts from (relation, type, number, date) quartets. A "PLUS"
79
+ # relation token (the consolidated "+" marker) yields "+"; otherwise "/".
80
+ def adjunct_to_code(fields)
81
+ return "" if fields.nil? || fields.empty?
282
82
 
283
- # Build the final identifier
284
- builder = Pubid::Iec::Builder.new(Pubid::Iec::Scheme)
285
- builder.build(base_hash)
83
+ rel, type, num, date = fields[0..3]
84
+ code = (rel.empty? ? "/" : "+") + type + num
85
+ code += ":#{date}" unless date.nil? || date.empty?
86
+ code + adjunct_to_code(fields[4..])
286
87
  end
287
88
  end
288
89
  end
@@ -7,6 +7,47 @@ module Pubid
7
7
  def parse(input)
8
8
  Identifiers::Base.parse(input)
9
9
  end
10
+
11
+ # Factory mirroring pubid 1.x's `Pubid::Ieee::Identifier.create` API.
12
+ # Dispatches via {Pubid::Ieee::Scheme.locate_identifier_klass_by_type_code}.
13
+ # Default subclass (no `type:`) is Identifiers::Standard — the
14
+ # canonical class produced by parsing typical IEEE identifiers.
15
+ def create(type: nil, **opts)
16
+ klass = type ?
17
+ Scheme.locate_identifier_klass_by_type_code(type) :
18
+ Identifiers::Standard
19
+ attrs = coerce_create_attrs(opts)
20
+ # ProjectDraftIdentifier renders the "P" prefix from typed_stage,
21
+ # not from `type`. Set both so the output matches a parsed form.
22
+ if klass == Identifiers::ProjectDraftIdentifier
23
+ attrs[:type] = "P"
24
+ attrs[:typed_stage] ||= Scheme.locate_typed_stage_by_abbr("P")
25
+ end
26
+ klass.new(**attrs)
27
+ end
28
+
29
+ private
30
+
31
+ def coerce_create_attrs(opts)
32
+ attrs = {}
33
+ attrs[:publisher] = opts[:publisher].to_s if opts[:publisher]
34
+ if opts[:copublisher]
35
+ attrs[:copublisher] =
36
+ Array(opts[:copublisher]).map(&:to_s)
37
+ end
38
+ # :code or :number alias
39
+ if (v = opts[:code] || opts[:number])
40
+ attrs[:code] = v.to_s
41
+ end
42
+ %i[year edition month day draft_status].each do |k|
43
+ v = opts[k]
44
+ attrs[k] = v.to_s unless v.nil?
45
+ end
46
+ attrs[:redline] = opts[:redline] if opts.key?(:redline)
47
+ # TODO(create-shim): amendments/corrigenda/revision_of/incorporates
48
+ # are nested Base relations; not yet wired through.
49
+ attrs
50
+ end
10
51
  end
11
52
  end
12
53
  end
@@ -14,6 +14,48 @@ module Pubid
14
14
  rescue Parslet::ParseFailed => e
15
15
  raise "Failed to parse IHO identifier '#{identifier}': #{e.message}"
16
16
  end
17
+
18
+ # Factory that builds an IHO identifier from a hash of primitives.
19
+ #
20
+ # IHO identifiers are simpler than ISO/IEC: attributes are plain
21
+ # strings (no Component wrapping), so coercion just calls #to_s.
22
+ #
23
+ # Dispatch:
24
+ # * `type:` accepts the type key (`:standard`, `:publication`,
25
+ # `:miscellaneous`, `:bibliographic`, `:circular_letter`) or
26
+ # the IHO series letter (`"S"`, `"P"`, `"M"`, `"B"`, `"C"`).
27
+ # * Default is {Identifiers::Standard}.
28
+ #
29
+ # @param type [Symbol, String, nil]
30
+ # @param opts [Hash] :publisher (default "IHO"), :code, :appendix,
31
+ # :part, :annex, :supplement, :version
32
+ def self.create(type: nil, **opts)
33
+ klass = resolve_create_class(type)
34
+ klass.new(**coerce_create_attrs(opts))
35
+ end
36
+
37
+ def self.resolve_create_class(type)
38
+ return Identifiers::Standard if type.nil?
39
+
40
+ by_key = Scheme::IDENTIFIERS.to_h { |k| [k.type[:key], k] }
41
+ return by_key[type.to_sym] if by_key.key?(type.to_sym)
42
+
43
+ begin
44
+ Scheme.identifier_klass_for_type_letter(type.to_s.upcase)
45
+ rescue KeyError
46
+ raise ArgumentError, "Unknown IHO type: #{type.inspect}"
47
+ end
48
+ end
49
+
50
+ def self.coerce_create_attrs(opts)
51
+ attrs = {}
52
+ %i[publisher code appendix part annex supplement version].each do |k|
53
+ v = opts[k]
54
+ attrs[k] = v.to_s unless v.nil?
55
+ end
56
+ attrs
57
+ end
58
+ private_class_method :resolve_create_class, :coerce_create_attrs
17
59
  end
18
60
  end
19
61
  end