pubid 2.0.0.pre.alpha.12 → 2.0.0.pre.alpha.14

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 (285) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +43 -1
  3. data/data/ieee/update_codes.yaml +17 -4
  4. data/data/nist/update_codes.yaml +7 -3
  5. data/data/parg/tables/bipm_groups.yaml +14 -0
  6. data/data/parg/tables/bipm_type_codes.yaml +5 -0
  7. data/data/parg/tables/bipm_type_names_en.yaml +6 -0
  8. data/data/parg/tables/bipm_type_names_fr.yaml +6 -0
  9. data/data/parg/tables/directives_supplements_typed_stages.yaml +3 -0
  10. data/data/parg/tables/directives_typed_stages.yaml +5 -0
  11. data/data/parg/tables/idf_typed_stages.yaml +27 -0
  12. data/data/parg/tables/idf_typed_stages_supplements.yaml +2 -0
  13. data/data/parg/tables/iec_typed_stages.yaml +130 -0
  14. data/data/parg/tables/iso_publishers.yaml +4 -0
  15. data/data/parg/tables/organizations.yaml +12 -0
  16. data/data/parg/tables/tc_types.yaml +42 -0
  17. data/data/parg/tables/typed_stages.yaml +114 -0
  18. data/data/parg/tables/typed_stages_supplements.yaml +64 -0
  19. data/data/parg/tables/wg_types.yaml +21 -0
  20. data/lib/pubid/adobe/builder.rb +2 -0
  21. data/lib/pubid/adobe/identifier.rb +11 -1
  22. data/lib/pubid/all_parts.rb +201 -0
  23. data/lib/pubid/all_parts_identifier.rb +19 -0
  24. data/lib/pubid/amca/CLAUDE.md +47 -0
  25. data/lib/pubid/amca/builder.rb +3 -5
  26. data/lib/pubid/amca/identifiers/base.rb +11 -1
  27. data/lib/pubid/amca/identifiers/publication.rb +13 -0
  28. data/lib/pubid/amca/parser.rb +2 -1
  29. data/lib/pubid/amca/renderer.rb +22 -33
  30. data/lib/pubid/amca/urn_generator.rb +21 -2
  31. data/lib/pubid/amca/urn_parser.rb +36 -10
  32. data/lib/pubid/ansi/builder.rb +6 -0
  33. data/lib/pubid/ansi/identifier.rb +1 -1
  34. data/lib/pubid/api/CLAUDE.md +23 -0
  35. data/lib/pubid/api/builder.rb +2 -0
  36. data/lib/pubid/api/identifier.rb +1 -1
  37. data/lib/pubid/api/parser.rb +8 -4
  38. data/lib/pubid/ashrae/CLAUDE.md +13 -0
  39. data/lib/pubid/ashrae/builder.rb +58 -14
  40. data/lib/pubid/ashrae/identifiers/base.rb +10 -1
  41. data/lib/pubid/ashrae/identifiers/errata.rb +14 -2
  42. data/lib/pubid/ashrae/identifiers/interpretation.rb +2 -10
  43. data/lib/pubid/ashrae/parser.rb +80 -39
  44. data/lib/pubid/ashrae/renderer.rb +32 -1
  45. data/lib/pubid/ashrae/urn_generator.rb +32 -9
  46. data/lib/pubid/asme/CLAUDE.md +25 -0
  47. data/lib/pubid/asme/builder.rb +16 -9
  48. data/lib/pubid/asme/components/code.rb +2 -0
  49. data/lib/pubid/asme/identifier.rb +1 -1
  50. data/lib/pubid/asme/identifiers/standard.rb +6 -1
  51. data/lib/pubid/asme/parser.rb +41 -14
  52. data/lib/pubid/astm/CLAUDE.md +9 -0
  53. data/lib/pubid/astm/builder.rb +2 -0
  54. data/lib/pubid/astm/components/code.rb +2 -0
  55. data/lib/pubid/astm/identifier.rb +1 -1
  56. data/lib/pubid/astm/parser.rb +4 -1
  57. data/lib/pubid/bipm/CLAUDE.md +11 -0
  58. data/lib/pubid/bipm/builder.rb +2 -0
  59. data/lib/pubid/bipm/identifier.rb +1 -1
  60. data/lib/pubid/bsi/CLAUDE.md +93 -0
  61. data/lib/pubid/bsi/builder.rb +13 -11
  62. data/lib/pubid/bsi/identifiers/addendum_document.rb +2 -0
  63. data/lib/pubid/bsi/identifiers/adopted_european_norm.rb +6 -54
  64. data/lib/pubid/bsi/identifiers/adopted_international_standard.rb +5 -22
  65. data/lib/pubid/bsi/identifiers/amendment.rb +36 -12
  66. data/lib/pubid/bsi/identifiers/bundled_identifier.rb +2 -0
  67. data/lib/pubid/bsi/identifiers/consolidated_identifier.rb +23 -26
  68. data/lib/pubid/bsi/identifiers/corrigendum.rb +29 -12
  69. data/lib/pubid/bsi/identifiers/expert_commentary.rb +6 -7
  70. data/lib/pubid/bsi/identifiers/national_annex.rb +18 -20
  71. data/lib/pubid/bsi/identifiers/root_identity.rb +31 -0
  72. data/lib/pubid/bsi/identifiers/set.rb +2 -0
  73. data/lib/pubid/bsi/identifiers/supplement_document.rb +2 -0
  74. data/lib/pubid/bsi/identifiers.rb +1 -0
  75. data/lib/pubid/bsi/parser.rb +8 -8
  76. data/lib/pubid/bsi/renderer.rb +20 -20
  77. data/lib/pubid/bsi/single_identifier.rb +1 -3
  78. data/lib/pubid/bsi/urn_generator.rb +28 -18
  79. data/lib/pubid/builder/base.rb +27 -0
  80. data/lib/pubid/calconnect/builder.rb +2 -0
  81. data/lib/pubid/calconnect/identifier.rb +5 -1
  82. data/lib/pubid/ccsds/builder.rb +2 -0
  83. data/lib/pubid/ccsds/identifier.rb +9 -1
  84. data/lib/pubid/cen_cenelec/CLAUDE.md +59 -0
  85. data/lib/pubid/cen_cenelec/builder.rb +6 -1
  86. data/lib/pubid/cen_cenelec/identifier.rb +12 -28
  87. data/lib/pubid/cen_cenelec/identifiers/amendment.rb +3 -10
  88. data/lib/pubid/cen_cenelec/identifiers/corrigendum.rb +3 -10
  89. data/lib/pubid/cen_cenelec/parser.rb +20 -5
  90. data/lib/pubid/cie/CLAUDE.md +58 -0
  91. data/lib/pubid/cie/builder.rb +2 -0
  92. data/lib/pubid/cie/components/language.rb +2 -0
  93. data/lib/pubid/cie/identifier.rb +1 -1
  94. data/lib/pubid/cie/parser.rb +9 -2
  95. data/lib/pubid/components/adoption.rb +2 -0
  96. data/lib/pubid/components/code.rb +2 -0
  97. data/lib/pubid/components/date.rb +8 -6
  98. data/lib/pubid/components/edition.rb +2 -0
  99. data/lib/pubid/components/iteration.rb +2 -0
  100. data/lib/pubid/components/language.rb +2 -0
  101. data/lib/pubid/components/locality.rb +2 -0
  102. data/lib/pubid/components/publisher.rb +2 -0
  103. data/lib/pubid/components/relationship.rb +2 -0
  104. data/lib/pubid/components/stage.rb +2 -0
  105. data/lib/pubid/components/supplement.rb +2 -0
  106. data/lib/pubid/components/type.rb +2 -0
  107. data/lib/pubid/components/typed_stage.rb +8 -0
  108. data/lib/pubid/conformance/checks.rb +1 -1
  109. data/lib/pubid/csa/CLAUDE.md +41 -0
  110. data/lib/pubid/csa/builder.rb +2 -0
  111. data/lib/pubid/csa/identifier.rb +19 -3
  112. data/lib/pubid/csa/parser.rb +25 -8
  113. data/lib/pubid/csa/renderer.rb +12 -12
  114. data/lib/pubid/csa/single_identifier.rb +17 -0
  115. data/lib/pubid/doi/builder.rb +2 -0
  116. data/lib/pubid/doi/identifier.rb +1 -1
  117. data/lib/pubid/easc/builder.rb +2 -0
  118. data/lib/pubid/easc/identifier.rb +10 -1
  119. data/lib/pubid/ecma/CLAUDE.md +28 -0
  120. data/lib/pubid/ecma/builder.rb +2 -0
  121. data/lib/pubid/ecma/identifier.rb +8 -1
  122. data/lib/pubid/etsi/CLAUDE.md +34 -0
  123. data/lib/pubid/etsi/builder.rb +2 -0
  124. data/lib/pubid/etsi/components/code.rb +6 -0
  125. data/lib/pubid/etsi/components/version.rb +2 -0
  126. data/lib/pubid/etsi/identifiers/base.rb +1 -1
  127. data/lib/pubid/etsi/identifiers/etsi_standard.rb +7 -0
  128. data/lib/pubid/evs/CLAUDE.md +58 -0
  129. data/lib/pubid/evs/builder.rb +2 -0
  130. data/lib/pubid/evs.rb +1 -1
  131. data/lib/pubid/gb/CLAUDE.md +140 -0
  132. data/lib/pubid/gb/builder.rb +7 -2
  133. data/lib/pubid/gb/identifier.rb +6 -4
  134. data/lib/pubid/gb/identifiers/all_parts.rb +17 -0
  135. data/lib/pubid/gb/identifiers.rb +1 -0
  136. data/lib/pubid/gb/renderer.rb +0 -1
  137. data/lib/pubid/gost/CLAUDE.md +64 -0
  138. data/lib/pubid/gost/builder.rb +3 -1
  139. data/lib/pubid/gost/identifier.rb +16 -1
  140. data/lib/pubid/gost/parser.rb +8 -1
  141. data/lib/pubid/iala/CLAUDE.md +82 -0
  142. data/lib/pubid/iala/builder.rb +2 -0
  143. data/lib/pubid/iala/identifier.rb +10 -1
  144. data/lib/pubid/iana/CLAUDE.md +7 -0
  145. data/lib/pubid/iana/builder.rb +2 -0
  146. data/lib/pubid/iana/identifier.rb +1 -1
  147. data/lib/pubid/identifier.rb +161 -17
  148. data/lib/pubid/idf/builder.rb +11 -1
  149. data/lib/pubid/idf/identifier.rb +5 -0
  150. data/lib/pubid/idf/identifiers/all_parts.rb +17 -0
  151. data/lib/pubid/idf/identifiers.rb +1 -0
  152. data/lib/pubid/iec/CLAUDE.md +31 -0
  153. data/lib/pubid/iec/builder.rb +7 -1
  154. data/lib/pubid/iec/components/consolidated_amendment.rb +4 -0
  155. data/lib/pubid/iec/components/sheet.rb +2 -0
  156. data/lib/pubid/iec/components/trf_info.rb +2 -0
  157. data/lib/pubid/iec/components/vap_suffix.rb +2 -0
  158. data/lib/pubid/iec/identifier.rb +8 -3
  159. data/lib/pubid/iec/identifiers/all_parts.rb +19 -0
  160. data/lib/pubid/iec/identifiers.rb +1 -0
  161. data/lib/pubid/iec/parser.rb +9 -4
  162. data/lib/pubid/iec/renderer.rb +0 -1
  163. data/lib/pubid/iec/urn_generator.rb +9 -1
  164. data/lib/pubid/iec/urn_parser.rb +3 -2
  165. data/lib/pubid/ieee/CLAUDE.md +97 -0
  166. data/lib/pubid/ieee/builder.rb +194 -27
  167. data/lib/pubid/ieee/components/code.rb +2 -0
  168. data/lib/pubid/ieee/components/draft.rb +35 -2
  169. data/lib/pubid/ieee/components/typed_stage.rb +2 -0
  170. data/lib/pubid/ieee/identifiers/base.rb +21 -1
  171. data/lib/pubid/ieee/identifiers/iec_ieee_copublished.rb +9 -0
  172. data/lib/pubid/ieee/identifiers/joint_development.rb +66 -23
  173. data/lib/pubid/ieee/identifiers/project_draft_identifier.rb +8 -1
  174. data/lib/pubid/ieee/parser.rb +156 -29
  175. data/lib/pubid/ieee/renderer.rb +42 -7
  176. data/lib/pubid/ieee/urn_generator.rb +31 -0
  177. data/lib/pubid/ietf/CLAUDE.md +7 -0
  178. data/lib/pubid/ietf/builder.rb +2 -0
  179. data/lib/pubid/ietf/identifiers/base.rb +1 -1
  180. data/lib/pubid/iho/builder.rb +2 -0
  181. data/lib/pubid/isbn/builder.rb +2 -0
  182. data/lib/pubid/isbn/identifier.rb +1 -1
  183. data/lib/pubid/iso/CLAUDE.md +47 -0
  184. data/lib/pubid/iso/builder.rb +19 -5
  185. data/lib/pubid/iso/components/publisher.rb +2 -0
  186. data/lib/pubid/iso/identifier.rb +10 -15
  187. data/lib/pubid/iso/identifiers/all_parts.rb +19 -0
  188. data/lib/pubid/iso/identifiers/directives_supplement.rb +4 -2
  189. data/lib/pubid/iso/identifiers.rb +1 -0
  190. data/lib/pubid/iso/normalizer.rb +4 -1
  191. data/lib/pubid/iso/rendering_style.rb +0 -1
  192. data/lib/pubid/itu/CLAUDE.md +115 -0
  193. data/lib/pubid/itu/builder.rb +26 -4
  194. data/lib/pubid/itu/components/code.rb +2 -0
  195. data/lib/pubid/itu/components/designation.rb +2 -0
  196. data/lib/pubid/itu/components/sector.rb +2 -0
  197. data/lib/pubid/itu/components/series.rb +2 -0
  198. data/lib/pubid/itu/identifiers/base.rb +11 -18
  199. data/lib/pubid/itu/identifiers/radio_regulations.rb +27 -0
  200. data/lib/pubid/itu/identifiers/special_publication.rb +48 -14
  201. data/lib/pubid/itu/identifiers/standard_serialization.rb +2 -0
  202. data/lib/pubid/itu/identifiers/supplement.rb +15 -0
  203. data/lib/pubid/itu/identifiers.rb +1 -0
  204. data/lib/pubid/itu/parser.rb +108 -22
  205. data/lib/pubid/itu/urn_generator.rb +9 -2
  206. data/lib/pubid/jcgm/CLAUDE.md +7 -0
  207. data/lib/pubid/jcgm/builder.rb +2 -0
  208. data/lib/pubid/jcgm/components/publisher.rb +2 -0
  209. data/lib/pubid/jcgm.rb +1 -1
  210. data/lib/pubid/jis/builder.rb +5 -1
  211. data/lib/pubid/jis/identifier.rb +6 -18
  212. data/lib/pubid/jis/identifiers/all_parts.rb +19 -0
  213. data/lib/pubid/jis/identifiers.rb +1 -0
  214. data/lib/pubid/jis/renderer.rb +0 -2
  215. data/lib/pubid/jis/urn_generator.rb +0 -1
  216. data/lib/pubid/nist/CLAUDE.md +56 -0
  217. data/lib/pubid/nist/builder.rb +3 -0
  218. data/lib/pubid/nist/components/edition.rb +2 -0
  219. data/lib/pubid/nist/components/issue_number.rb +2 -0
  220. data/lib/pubid/nist/components/part.rb +2 -0
  221. data/lib/pubid/nist/components/stage.rb +2 -0
  222. data/lib/pubid/nist/components/supplement.rb +2 -0
  223. data/lib/pubid/nist/components/translation.rb +2 -0
  224. data/lib/pubid/nist/components/update.rb +2 -0
  225. data/lib/pubid/nist/components/version.rb +2 -0
  226. data/lib/pubid/nist/components/volume.rb +2 -0
  227. data/lib/pubid/nist/identifiers/base.rb +39 -7
  228. data/lib/pubid/nist/parser.rb +24 -2
  229. data/lib/pubid/nist/preprocessor.rb +53 -2
  230. data/lib/pubid/nist/urn_parser.rb +10 -1
  231. data/lib/pubid/oasis/CLAUDE.md +19 -0
  232. data/lib/pubid/oasis/builder.rb +2 -0
  233. data/lib/pubid/oasis/identifier.rb +20 -1
  234. data/lib/pubid/ogc/CLAUDE.md +34 -0
  235. data/lib/pubid/ogc/builder.rb +2 -0
  236. data/lib/pubid/ogc/identifier.rb +12 -1
  237. data/lib/pubid/oiml/CLAUDE.md +189 -0
  238. data/lib/pubid/oiml/builder.rb +20 -0
  239. data/lib/pubid/oiml/components/code.rb +6 -0
  240. data/lib/pubid/oiml/identifier.rb +13 -0
  241. data/lib/pubid/oiml/identifiers/annex.rb +4 -0
  242. data/lib/pubid/oiml/identifiers/certification_system.rb +34 -0
  243. data/lib/pubid/oiml/identifiers/code_number.rb +8 -0
  244. data/lib/pubid/oiml/identifiers/dual_published.rb +174 -0
  245. data/lib/pubid/oiml/identifiers.rb +2 -0
  246. data/lib/pubid/oiml/parser.rb +35 -4
  247. data/lib/pubid/oiml/renderer.rb +23 -1
  248. data/lib/pubid/oiml/single_identifier.rb +4 -0
  249. data/lib/pubid/oiml/supplement_identifier.rb +7 -0
  250. data/lib/pubid/oiml/urn_generator.rb +28 -0
  251. data/lib/pubid/oiml.rb +6 -1
  252. data/lib/pubid/omg/CLAUDE.md +15 -0
  253. data/lib/pubid/omg/builder.rb +2 -0
  254. data/lib/pubid/omg/identifier.rb +1 -1
  255. data/lib/pubid/parg/artifact.rb +46 -0
  256. data/lib/pubid/parg/backend.rb +92 -0
  257. data/lib/pubid/parg.rb +8 -0
  258. data/lib/pubid/parser/grammar.rb +23 -0
  259. data/lib/pubid/pg.rb +8 -0
  260. data/lib/pubid/plateau/builder.rb +2 -0
  261. data/lib/pubid/plateau/identifiers/base.rb +4 -0
  262. data/lib/pubid/plateau/supplement_identifier.rb +14 -2
  263. data/lib/pubid/plateau/urn_generator.rb +7 -1
  264. data/lib/pubid/plateau.rb +1 -2
  265. data/lib/pubid/renderers/human_readable.rb +0 -1
  266. data/lib/pubid/sae/builder.rb +2 -0
  267. data/lib/pubid/sae/components/date.rb +2 -0
  268. data/lib/pubid/sae/components/type.rb +2 -0
  269. data/lib/pubid/sae/identifiers/base.rb +1 -1
  270. data/lib/pubid/subset_match.rb +197 -0
  271. data/lib/pubid/tgpp/CLAUDE.md +43 -0
  272. data/lib/pubid/tgpp/builder.rb +2 -0
  273. data/lib/pubid/tgpp/identifier.rb +15 -1
  274. data/lib/pubid/type_resolver.rb +14 -2
  275. data/lib/pubid/un/builder.rb +2 -0
  276. data/lib/pubid/un/identifier.rb +1 -1
  277. data/lib/pubid/version.rb +1 -1
  278. data/lib/pubid/w3c/CLAUDE.md +7 -0
  279. data/lib/pubid/w3c/builder.rb +2 -0
  280. data/lib/pubid/w3c/identifier.rb +1 -1
  281. data/lib/pubid/xsf/CLAUDE.md +11 -0
  282. data/lib/pubid/xsf/builder.rb +2 -0
  283. data/lib/pubid/xsf/identifier.rb +1 -1
  284. data/lib/pubid.rb +17 -3
  285. metadata +78 -2
@@ -16,6 +16,8 @@ module Pubid
16
16
  # +original_separator+ to round-trip dot vs dash notation, and uses
17
17
  # +number+ rather than +value+.
18
18
  class Code < Lutaml::Model::Serializable
19
+ include ::Pubid::SubsetMatch
20
+
19
21
  attribute :prefix, :string # C, P for some identifiers
20
22
  attribute :number, :string # Main number (802, 1234, etc.)
21
23
  attribute :parts, :string, collection: true # Array of part strings
@@ -11,6 +11,8 @@ module Pubid
11
11
  # /D3.4 - version with revision
12
12
  # /D7, July 2019 - with date
13
13
  class Draft < Lutaml::Model::Serializable
14
+ include ::Pubid::SubsetMatch
15
+
14
16
  attribute :version, :string # D5, D3, etc.
15
17
  attribute :revision, :string # .4, .2 in D3.4
16
18
  attribute :year, :string
@@ -20,6 +22,11 @@ module Pubid
20
22
  attribute :comma_before_month, :boolean, default: -> {
21
23
  false
22
24
  } # Track if comma was in input
25
+ # The joint stage-draft half of the compound form "D5=DDIS.3":
26
+ # the ISO/IEC stage this draft iterates (DIS) and its iteration
27
+ # numeral (3). See docs/IEEE-DRAFT-STAGES.md §1.3.
28
+ attribute :iso_stage, :string
29
+ attribute :iso_iteration, :string
23
30
 
24
31
  # Month name to number mapping
25
32
  MONTH_NAMES = {
@@ -32,7 +39,7 @@ module Pubid
32
39
  }.freeze
33
40
 
34
41
  def initialize(version: nil, revision: nil, year: nil, month: nil,
35
- day: nil)
42
+ day: nil, iso_stage: nil, iso_iteration: nil)
36
43
  super()
37
44
  self.version = version
38
45
  self.revision = revision
@@ -40,6 +47,15 @@ day: nil)
40
47
  self.original_month = month # Store original format
41
48
  self.month = convert_month(month)
42
49
  self.day = day
50
+ # The grammar's ordered alternation captures the stage WITH its
51
+ # draft-marker D when the spelling doubles it ("=DDIS"): the
52
+ # marker is notation, the stage is the bare ISO/IEC token.
53
+ if iso_stage.to_s.start_with?("D") &&
54
+ %w[PWI NP WD CD CDV DIS FDIS].include?(iso_stage[1..])
55
+ iso_stage = iso_stage[1..]
56
+ end
57
+ self.iso_stage = iso_stage
58
+ self.iso_iteration = iso_iteration
43
59
  end
44
60
 
45
61
  # Month names as an alternation, longest-first so "September" wins over
@@ -61,12 +77,27 @@ day: nil)
61
77
  # this method after `from_hash` (a mismatch breaks `to_hash` round-trip).
62
78
  # @param value [String, Draft] the value to coerce
63
79
  # @return [Draft]
80
+ # The compound stage suffix (docs/IEEE-DRAFT-STAGES.md §1.3):
81
+ # "=DIS.3" is canonical — D (draft) = DIS (the stage), iteration 3.
82
+ # "=DDIS.3", "=DDIS3", "=DDIS-3" (the stage echoed with its own D)
83
+ # are accepted aliases. Captures [stage, iteration].
84
+ COMPOUND_SUFFIX = /=D?(PWI|NP|WD|CDV|FDIS|DIS|CD)(?:[.-]?(\d+))?\z/.freeze
85
+ private_constant :COMPOUND_SUFFIX
86
+
64
87
  def self.parse(value)
65
88
  return value if value.is_a?(Draft)
66
89
 
67
90
  body = strip_prefix(value.to_s)
91
+ iso_stage = nil
92
+ iso_iteration = nil
93
+ if (m = body.match(COMPOUND_SUFFIX))
94
+ iso_stage = m[1]
95
+ iso_iteration = m[2]
96
+ body = body[0...m.begin(0)]
97
+ end
68
98
  version, month, day, year, comma = split_date(body)
69
- draft = new(version: version, month: month, day: day, year: year)
99
+ draft = new(version: version, month: month, day: day, year: year,
100
+ iso_stage: iso_stage, iso_iteration: iso_iteration)
70
101
  draft.comma_before_month = comma
71
102
  draft
72
103
  end
@@ -110,6 +141,8 @@ day: nil)
110
141
 
111
142
  def to_s
112
143
  result = "/D#{version}"
144
+ result += "=#{iso_stage}" if iso_stage
145
+ result += ".#{iso_iteration}" if iso_stage && iso_iteration
113
146
  result += ".#{revision}" if revision
114
147
 
115
148
  if year
@@ -19,6 +19,8 @@ module Pubid
19
19
  # - Approved Draft: Board-approved but unpublished (D7-D9)
20
20
  # - Published: Final standards (no P prefix)
21
21
  class TypedStage < Lutaml::Model::Serializable
22
+ include ::Pubid::SubsetMatch
23
+
22
24
  # All recognized abbreviations for this stage
23
25
  # First in array is canonical abbreviation
24
26
  attribute :abbr, :string, collection: true
@@ -201,6 +201,18 @@ module Pubid
201
201
  hash
202
202
  end
203
203
 
204
+ # Three more IEEE discriminators the default %i[date year edition
205
+ # version] list misses, alongside `edition`/`year`: `revision` (e.g.
206
+ # "2" in "P802.16Rev2"); `reaffirmed` (the "(R2010)" year, read by
207
+ # renderer.rb and urn_generator.rb — two reaffirmations of the same
208
+ # standard failed to collapse under #to_all_parts/#===); and
209
+ # `edition_month`, the month half of "Edition N YYYY-MM"
210
+ # (renderer.rb's `"Edition #{edition} #{year}-#{edition_month}"`),
211
+ # which survived even though its sibling `year` is already stripped.
212
+ def self.all_parts_edition_keys
213
+ super + %i[revision reaffirmed edition_month]
214
+ end
215
+
204
216
  # Inverse of the to_hash compaction: expand a scalar `stage` back into the
205
217
  # `typed_stage` sub-hash (on a deep copy, recursively) before lutaml
206
218
  # deserializes, so nested bases rebuild their component too. `draft` needs
@@ -351,7 +363,15 @@ module Pubid
351
363
  def self.parse_single(input)
352
364
  # Apply legacy update_codes normalization first, before Parser's extensive preprocessing
353
365
  normalized = Core::UpdateCodes.apply(input, :ieee)
354
- parsed = Parser.parse(normalized) # Use class method for preprocessing
366
+ # UpdateCodes may introduce the "; " double-label separator
367
+ # ("ISO/IEC13210: 1994 (E) ANSI/IEEE Std ..." → the ISO/IEC and
368
+ # IEEE labels of one document), but the dispatch ran on the raw
369
+ # input - re-dispatch so the dual construction sees it.
370
+ if normalized != input && normalized.include?("; ")
371
+ result = PreParser.preprocess(normalized)
372
+ return build_dual(result.parts) if result.dispatch == :dual_semicolon
373
+ end
374
+ parsed = Pubid::Parg::Backend.parse(:ieee, Parser.normalize_input(normalized))
355
375
  builder = Builder.new(Identifier)
356
376
  # Pass the original input string to builder for context
357
377
  builder.original_input = input
@@ -26,6 +26,15 @@ module Pubid
26
26
  attribute :iec_year, :string # IEC year like "2013"
27
27
  attribute :date_info, :string # Date information like "(10/07)"
28
28
 
29
+ # `year_sep` only describes how a year is attached. When the reference
30
+ # omits the year, a subset match skips it: its default "-" would
31
+ # otherwise refuse a candidate printed with ":".
32
+ def subset_attribute_match?(name, mine, theirs)
33
+ return true if name == :year_sep && year.nil?
34
+
35
+ super
36
+ end
37
+
29
38
  # The printed IEC/IEEE number, rebuilt losslessly from the split
30
39
  # columns (the renderer/urn read this). Not stored — number/parts/
31
40
  # separators/year fully describe it, so the verbatim string never
@@ -42,6 +42,8 @@ module Pubid
42
42
  attribute :year, :string
43
43
  attribute :iso_stage, :string # For ISO stage if present
44
44
  attribute :ieee_draft, :string # For IEEE P/D notation if present
45
+ attribute :parenthetical_content, :string # e.g. the "(E)" edition
46
+ # marker of an ISO/IEC label
45
47
 
46
48
  # Accepts keyword args or a single positional hash (the base
47
49
  # #exclude/matching rebuild passes the latter) — see
@@ -77,6 +79,11 @@ module Pubid
77
79
  # Canonical format based on lead party
78
80
  # @return [Symbol] :ieee or :iso
79
81
  def canonical_format
82
+ # A stage-tracked D= designator is IEEE draft notation — its
83
+ # canonical face is the designator spelling (§1.3 spelling 7:
84
+ # "JOINT PNUMBER/D=<STAGE>[:year]"), whatever the lead party.
85
+ return :ieee if ieee_draft.to_s.start_with?("D=")
86
+
80
87
  case lead_party
81
88
  when "IEEE", "AIEE"
82
89
  :ieee
@@ -128,24 +135,44 @@ module Pubid
128
135
  # Publishers (slash-separated)
129
136
  parts << publishers.join("/") if publishers && !publishers.empty?
130
137
 
131
- # ISO stage code (only if this was originally ISO-led)
132
- # For IEEE-led conversions, we skip the stage since we don't have ISO equivalent
133
- if lead_party == "ISO" && (typed_stage || iso_stage)
134
- if typed_stage
135
- parts << typed_stage.to_iso_format
136
- elsif iso_stage
137
- parts << iso_stage
138
- end
138
+ # The stage word is the ISO format's own position convention —
139
+ # it prints whenever the identifier carries one, regardless of
140
+ # lead party (the format face decides, not the arrangement). A
141
+ # stage-tracked D= designator decomposes into its stage word
142
+ # and date ("D=CD.2-2020" → "CD2", 2020); the iteration glues
143
+ # onto the word only for the stages whose printed ISO spellings
144
+ # carry it (CD2, DIS2 — the grammar's multi-digit families);
145
+ # the P project stage is IEEE convention and never prints here.
146
+ # The printed word is always the bare stage ("CD4" prints "CD",
147
+ # "DIS3" prints "DIS" — every iso_stage_spellings_spec
148
+ # expectation); the iteration is draft machinery, not ISO-face
149
+ # identity.
150
+ stage_word = iso_stage.to_s.sub(/\A([A-Z]+?)\d+\z/, '\1')
151
+ stage_word = nil if stage_word.empty?
152
+ draft_year = nil
153
+ if stage_word.nil? &&
154
+ ieee_draft.to_s.match(/\AD=([A-Z]+)(?:\.(\d+[a-z]?))?(?:[-:](\d{4}))?\z/)
155
+ stage_word = Regexp.last_match(1)
156
+ draft_year = Regexp.last_match(3)
139
157
  end
158
+ parts << stage_word if stage_word
140
159
 
141
- # Code (NO P prefix in ISO format, NO draft notation)
142
- code_str = code.to_s.gsub(/^P/, "")
160
+ # The P project marker is identity-bearing and prints on both
161
+ # faces (the standing trademark_leaf_to_s_spec contract:
162
+ # "ISO/IEC/IEEE P26511:2018").
163
+ code_str = code.to_s
143
164
  code_str += mark unless code_str.empty?
144
165
  parts << code_str if code_str && !code_str.empty?
145
166
 
146
- # Join with space and add year with colon
167
+ # Join with space and add year with colon (the ISO position
168
+ # convention; a decomposed D= date rides here as the year).
147
169
  result = parts.join(" ")
148
- result += ":#{year}" if year
170
+ result += ":#{year || draft_year}" if year || draft_year
171
+ # Only the language/edition marker ("(E)", "(E/F)") prints; a
172
+ # trailing relationship narrative is metadata, not identity.
173
+ if parenthetical_content&.match?(%r{\A[A-Z](?:\s*[/&]\s*[A-Z])*\z})
174
+ result += " (#{parenthetical_content})"
175
+ end
149
176
 
150
177
  result
151
178
  end
@@ -159,29 +186,45 @@ module Pubid
159
186
  # Publishers (slash-separated)
160
187
  parts << publishers.join("/") if publishers && !publishers.empty?
161
188
 
162
- # Build code part
163
- code_str = code.to_s.gsub(/^P/, "") # Remove any existing P first
164
-
165
- # Add P prefix for projects (IEEE format always shows P for drafts)
166
- if typed_stage&.project_status || type == "P"
167
- code_str = "P#{code_str}"
168
- end
189
+ # Build code part — the P-state prints as spelled (P = project
190
+ # draft; no P = standard). Never added or stripped.
191
+ code_str = code.to_s
169
192
 
170
193
  # Mark after the number, before the draft and the year
171
194
  code_str += mark unless code_str.empty?
172
195
 
173
- # Add IEEE draft notation if available (e.g., /D8)
174
- if ieee_draft
196
+ # Add IEEE draft notation if available (e.g., /D8). An ISO stage
197
+ # word renders in IEEE's stage-tracked position as the ordinal-less
198
+ # stage draft (docs/IEEE-DRAFT-STAGES.md §1.3, spelling 7):
199
+ # "IEEE/ISO/IEC CD P42010:2020" → "IEEE/ISO/IEC P42010/D=CD-2020".
200
+ # The iso_stage branch is checked FIRST — the typed_stage registry
201
+ # lookup for a stage word answers a draft-equivalent ordinal ("D2"),
202
+ # which is not the canonical stage-tracked spelling.
203
+ if ieee_draft.to_s.start_with?("D=") && year
204
+ # The ordinal-less stage draft's canonical face (the
205
+ # UpdateCodes rewrite): the publication year colon-joins the
206
+ # code and the designator trails — "P16326:2017/D=WD.5".
207
+ code_str += ":#{year}"
208
+ code_str += "/#{ieee_draft}"
209
+ @designator_carries_year = true
210
+ elsif ieee_draft
175
211
  code_str += "/#{ieee_draft}"
212
+ elsif iso_stage
213
+ # The ordinal-less stage draft: an iteration glued onto the
214
+ # printed word ("CD2") renders in the doctrine's ".iter" slot
215
+ # ("D=CD.2" — docs/IEEE-DRAFT-STAGES.md §1.3).
216
+ stage = iso_stage.match(/\A([A-Z]+?)(\d+)\z/)
217
+ code_str += stage ? "/D=#{stage[1]}.#{stage[2]}" : "/D=#{iso_stage}"
176
218
  elsif typed_stage&.ieee_draft_equivalent
177
219
  code_str += "/#{typed_stage.ieee_draft_equivalent}"
178
220
  end
179
221
 
180
222
  parts << code_str if code_str && !code_str.empty?
181
223
 
182
- # Join with space and add year with dash
224
+ # Join with space and add year with dash — unless the D=
225
+ # designator face already carried it above.
183
226
  result = parts.join(" ")
184
- result += "-#{year}" if year
227
+ result += "-#{year}" if year && !@designator_carries_year
185
228
 
186
229
  result
187
230
  end
@@ -33,10 +33,17 @@ module Pubid
33
33
  # object. The "P" is reflected in the typed_stage, not in the Code
34
34
  # component itself. Accepts keyword args or a single positional hash (the
35
35
  # base #exclude/matching rebuild passes the latter).
36
+ attribute :project_marker, :boolean, default: -> { false }
37
+
36
38
  def initialize(args = {}, **kwargs)
37
39
  args = kwargs.empty? ? args.dup : args.merge(kwargs)
38
- # If code is provided with "P" prefix (e.g., "P1234"), strip it
40
+ # If code is provided with "P" prefix (e.g., "P1234"), strip it.
41
+ # P = project (the document is a draft): the marker is
42
+ # identity-bearing, so its presence is remembered for the render —
43
+ # stripping the letter must not strip the fact (docs/
44
+ # IEEE-DRAFT-STAGES.md §1.1).
39
45
  if args[:code].is_a?(String) && args[:code].start_with?("P")
46
+ args[:project_marker] = true unless args.key?(:project_marker)
40
47
  args[:code] = args[:code][1..] # Strip leading "P"
41
48
  end
42
49
 
@@ -269,6 +269,10 @@ module Pubid
269
269
  rule(:draft_date) do
270
270
  # Enhanced to handle: ", Sept 2008" or " Sept 2008" or ", Month Year"
271
271
  ((comma | space) >> month_name.as(:month) >> space >> year_digits.as(:year)) |
272
+ # Space-separated bare year after the draft designator
273
+ # ("IEEE Draft Std P14764/D1 2004, Nov 2004" - the "D1 2004" draft
274
+ # carries its own year, with the print date still trailing).
275
+ (space >> year_digits.as(:year)) |
272
276
  # Numeric-month form ", 05 2007" / " 05 2007" — no text month name. The
273
277
  # year separator may be a dash: preprocessing (parser.rb ~1278) rewrites
274
278
  # a trailing " <digits> <year>" to "<digits>-<year>", so "05 2007"
@@ -302,6 +306,15 @@ module Pubid
302
306
  rule(:draft) do
303
307
  (draft_prefix >> draft_version.repeat(1, 2) >>
304
308
  (dot >> digits.as(:revision)).maybe >>
309
+ # The compound both-systems form (docs/IEEE-DRAFT-STAGES.md
310
+ # §1.3): "=DDIS.3" - IEEE draft ordinal = draft of the ISO/IEC
311
+ # stage, with its iteration. The glued "=DDIS3" / "=DDIS-3"
312
+ # spellings are accepted aliases of "=DDIS.3".
313
+ (str("=") >> (str("D") >> (str("FDIS") | str("CDV") | str("PWI") |
314
+ str("DIS") | str("WD") | str("NP") | str("CD")) |
315
+ (str("FDIS") | str("CDV") | str("PWI") | str("DIS") | str("WD") |
316
+ str("NP") | str("CD"))).as(:draft_iso_stage) >>
317
+ (dot | dash).maybe >> digits.as(:draft_iso_iteration).maybe).maybe >>
305
318
  draft_date.maybe).as(:draft)
306
319
  end
307
320
 
@@ -532,7 +545,9 @@ module Pubid
532
545
 
533
546
  # Additional parameters (inside parentheses)
534
547
  rule(:additional_parameters) do
535
- (space.maybe >> str("(") >> # Make space before '(' optional
548
+ (space.maybe >> str("(") >> space.maybe >> # Space before/after '(' optional
549
+ # ("( Revision of …)" - a crawl space inside the paren must not push
550
+ # the narrative to the catch-all and into the render).
536
551
  (reaffirmed |
537
552
  # Handle "Revision of IEEE Std ..." with optional space after Std
538
553
  (str("Revision of IEEE Std ") >> space.maybe >> match("[^)]").repeat(1).as(:revision_of)) |
@@ -580,17 +595,42 @@ module Pubid
580
595
  # ISO/IEC/IEEE P26511/D8-2018 or ISO/IEEE P1003.1-2008 or IEC/IEEE P62582-1-2011
581
596
  # ALSO handle: IEC/IEEE P60780-323, CDV1 2014 (comma before stage code)
582
597
  # ALSO handle: IEEE/CSA P844.1/293.1/D2 (CSA dual numbering)
583
- (str("ISO/IEC/IEEE") | str("ISO/IEEE") | str("IEC/IEEE") | str("IEEE/CSA")).as(:joint_publishers) >>
598
+ # ALSO handle: IEEE/ISO/IEC P42010/D=CD-2020 — the printed publisher
599
+ # order is the organization's perspective (pubid#469) and parses as
600
+ # printed; it is no longer rewritten to ISO-first.
601
+ (str("ISO/IEC/IEEE") | str("IEEE/ISO/IEC") | str("ISO/IEEE") | str("IEC/IEEE") | str("IEEE/CSA")).as(:joint_publishers) >>
584
602
  space >>
585
- str("P") >> # P indicates IEEE-led
603
+ # P = project (the document is a draft): identity-bearing, so it
604
+ # is captured and preserved, never silently consumed.
605
+ str("P").as(:project_marker) >>
586
606
  digits.as(:number) >>
587
607
  ((dot | dash) >> digits.as(:part)).maybe >> # Optional part like .1 or -1
588
608
  # CSA dual numbering: /293.1 (second number)
589
609
  (slash >> digits >> (dot >> digits).maybe >> (dash >> digits.as(:draft_version)).maybe).maybe >>
590
610
  (
591
- # Variant 1: /D8 notation (original)
592
- (slash >> str("D") >> digits.as(:draft_version)) |
593
- # Variant 2: , CDV1 notation (comma before stage code)
611
+ # Variant 1b: the ordinal-less stage draft "D=CDV-2020" -
612
+ # D (draft) = CDV (the IEC stage it drafts). The year rides in
613
+ # the draft clause (a distinct key, so the builder keeps the
614
+ # date inside the designator). The year follows IEEE format:
615
+ # dash-joined ("D=CD-2020", the ruled canonical); the colon
616
+ # spelling ("D=CDV:2020") stays accepted as the alias it was
617
+ # frozen with. A dash-year is guarded to 19xx/20xx so a
618
+ # YYMM monthcode tail can never be read as a year.
619
+ (slash >> str("D") >> str("=") >>
620
+ (str("CDV") | str("FDIS") | str("PWI") | str("WD") |
621
+ str("NP") | str("DIS") | str("CD")).as(:draft_iso_stage) >>
622
+ ((str(":") | str("-")) >>
623
+ ((str("19") | str("20")) >> digit >> digit).as(:draft_stage_year)).maybe) |
624
+ # Variant 1: /D8 notation (original), with the compound
625
+ # both-systems suffix "=DDIS.3" (docs/IEEE-DRAFT-STAGES.md §1.3)
626
+ (slash >> str("D") >> digits.as(:draft_version) >>
627
+ (str("=") >> (str("D") >> (str("FDIS") | str("CDV") | str("PWI") |
628
+ str("DIS") | str("WD") | str("NP") | str("CD")) |
629
+ (str("FDIS") | str("CDV") | str("PWI") | str("DIS") | str("WD") |
630
+ str("NP") | str("CD"))).as(:draft_iso_stage) >>
631
+ (dot | dash).maybe >> digits.as(:draft_iso_iteration).maybe).maybe) |
632
+ # Variant 2: , CDV1 notation (comma before stage code) —
633
+ # the stage draft of the named ISO/IEC stage, its iteration
594
634
  (comma >> (str("CDV") | str("FDIS") | str("CD") | str("DIS")).as(:iec_stage) >> digits.maybe.as(:stage_iteration))
595
635
  ).maybe >>
596
636
  # Optional edition, from relaton's "/E-<n>" suffix normalized to
@@ -636,18 +676,30 @@ module Pubid
636
676
  str("").as(:iso_published) >> space.maybe)) |
637
677
  (staged_only.as(:joint_publishers) >> space >>
638
678
  iso_stage >> std_noise >> space)) >>
639
- str("P").maybe >> # optional project marker on the number
679
+ str("P").as(:project_marker).maybe >> # project marker (P =
680
+ # project/draft; its
681
+ # presence is identity)
640
682
  digits.as(:number) >>
641
- # part must not swallow the trailing year (year_digits.absent?)
642
- ((dot | dash) >> year_digits.absent? >> digits.as(:part)).maybe >>
683
+ # part must not swallow the trailing year (year_digits.absent?). A
684
+ # DASH-joined part is tagged (:part_dash) - the catalogue-printed
685
+ # joint form spells the part with a dash ("21451-7") where the ISO
686
+ # form uses the dot.
687
+ ((dot >> year_digits.absent? >> digits.as(:part)) |
688
+ (dash >> str("").as(:part_dash) >> year_digits.absent? >>
689
+ digits.as(:part))).maybe >>
643
690
  (
644
691
  (str(":") >> year_digits.as(:year)) |
692
+ # The dash-year (and ", Month YYYY" text date below) spellings are
693
+ # the catalogue-PRINTED joint form; tag them (:printed_form) so the
694
+ # builder routes to a Standard instead of the ISO joint reference.
645
695
  (dash >> year_digits.as(:year) >>
646
- (dash >> month_numeric.as(:month)).maybe) |
696
+ (dash >> month_numeric.as(:month)).maybe >>
697
+ str("").as(:printed_dash_year)) |
647
698
  # Trailing text date ", April 2015" / " April 2015" (build_joint_development
648
699
  # already reads :month/:year). No :year collision — the iso rule has no
649
700
  # other :year capture (edition uses :edition_year).
650
- ((comma | space) >> month_name.as(:month) >> space >> year_digits.as(:year))
701
+ ((comma | space) >> month_name.as(:month) >> space >>
702
+ year_digits.as(:year) >> str("").as(:printed_month_year))
651
703
  ).maybe >>
652
704
  # Optional /D<draft> tail. normalize_relaton_suffixes repositions the
653
705
  # historical "…/D-3-2017" onto the number as "…-2017/D3", so by the
@@ -658,11 +710,26 @@ module Pubid
658
710
  # keys (:draft_month/:draft_year), because a plain :month/:year
659
711
  # here collides with the date clause above and parslet drops the
660
712
  # subtree with a "Duplicate subtrees" warning.
713
+ # The ISO/IEC edition marker may sit between the year and an
714
+ # amendment/draft tail: "8802.11:2012 (E)/Amd 1-2014". A distinct
715
+ # key - a second :parameters capture here collides with the
716
+ # trailing parenthetical slot (parslet drops duplicate subtrees).
717
+ (space.maybe >> str("(") >>
718
+ match("[^)]").repeat(1).as(:edition_marker) >> str(")")).maybe >>
719
+ # The stage-draft clause of the ISO-led print: "/D=WD.5" -
720
+ # D (draft) = the ISO/IEC stage it drafts, with its iteration
721
+ # (docs/IEEE-DRAFT-STAGES.md §1.2).
722
+ ((slash >> str("D") >> str("=") >>
723
+ (str("FDIS") | str("FCD") | str("CDV") |
724
+ (str("DIS") >> digit.maybe) |
725
+ (str("CD") >> digit.maybe) |
726
+ str("WD") | str("PWI") | str("NP")).as(:draft_iso_stage) >>
727
+ (dot >> (digits >> match("[A-Za-z]").repeat(0, 1)).as(:draft_iso_iteration)).maybe) |
661
728
  (slash >> str("D") >> dash.maybe >>
662
729
  match('[0-9.]').repeat(1).as(:draft_version) >>
663
730
  (((comma | space) >> month_name.as(:draft_month) >> space >>
664
731
  year_digits.as(:draft_year)) |
665
- (comma >> year_digits.as(:draft_year))).maybe).maybe >>
732
+ (comma >> year_digits.as(:draft_year))).maybe).maybe).maybe >>
666
733
  # Optional amendment tail (pubid#317:
667
734
  # "8802-11:2012/Amd.1:2014(E)") - the flat tree keys reuse
668
735
  # build_flat_amendment.
@@ -678,7 +745,6 @@ module Pubid
678
745
  # The crawl sometimes spaces it ("…(E), January 2017" family,
679
746
  # pubid#216), and a relationship parenthetical may follow
680
747
  # ("…CD2 P15288-2013-09 (Revision of …)").
681
- space.maybe >> (str("(E)") | str("(F)")).maybe >>
682
748
  parenthetical.maybe
683
749
  end
684
750
 
@@ -692,6 +758,28 @@ module Pubid
692
758
  # the trailing `.FDIS` is the ISO stage, NOT a second part. Routes through
693
759
  # the same build_joint_development (via :joint_publishers/:iso_stage), so the
694
760
  # stage is modeled correctly and the numeric part stays separate.
761
+ # The stage-less ISO/IEC label with a colon year and the ISO/IEC
762
+ # edition marker: "ISO/IEC 13210:1994 (E)" - the ISO/IEC portion of a
763
+ # double-labeled standard. The (E) parenthetical is REQUIRED: without
764
+ # it this spelling is the ISO flavor's own form and must not be
765
+ # stolen. Routes through build_joint_development like every ISO-led
766
+ # joint reference.
767
+ rule(:joint_development_iso_iec_edition) do
768
+ str("ISO/IEC").as(:joint_publishers) >> space >>
769
+ str("").as(:iso_published) >>
770
+ str("P").as(:project_marker).maybe >>
771
+ digits.as(:number) >>
772
+ # The part may be dot- or dash-joined ("8802-9"); a dash-year
773
+ # ("1234-2012") is not a part.
774
+ # A distinct key: the iso-format route's :part_dash prints as a
775
+ # dot in the joint code (the "P24748.2:2018" convention); only
776
+ # the ISO/IEC LABEL keeps its printed dash.
777
+ ((dot | (dash >> str("").as(:iec_label_dash) >> year_digits.absent?)) >>
778
+ digits.as(:part)).maybe >>
779
+ (str(":") >> space.maybe >> year_digits.as(:year)) >>
780
+ space.maybe >> parenthetical
781
+ end
782
+
695
783
  rule(:joint_development_embedded_stage) do
696
784
  # publisher set mirrors joint_development_iso_format (incl. bare IEEE);
697
785
  # longest token first.
@@ -699,7 +787,9 @@ module Pubid
699
787
  str("ISO/IEEE") | str("IEC/IEEE") | str("IEEE/IEC") | str("ISO/IEC") |
700
788
  str("IEEE")).as(:joint_publishers) >>
701
789
  space >>
702
- str("P").maybe >> # optional project marker on the number
790
+ str("P").as(:project_marker).maybe >> # project marker (P =
791
+ # project/draft; its
792
+ # presence is identity)
703
793
  digits.as(:number) >>
704
794
  # optional numeric part (dot or dash); must not swallow a year
705
795
  ((dot | dash) >> year_digits.absent? >> digits.as(:part)).maybe >>
@@ -814,7 +904,17 @@ module Pubid
814
904
  ((space >> month_name.as(:trailing_month) >> space >> year_digits.as(:trailing_year)) |
815
905
  (space >> month_numeric.as(:trailing_month) >> (space | dash) >> year_digits.as(:trailing_year))).maybe >>
816
906
  draft.maybe >>
907
+ # A print date may trail a draft that carries its own date
908
+ # ("IEEE Unapproved Draft 24765/D1 2009, Oct 2009" - the draft's
909
+ # year is "2009", the print date is ", Oct 2009").
910
+ (comma >> space? >> month_name.as(:trailing_month) >> space >>
911
+ year_digits.as(:trailing_year)).maybe >>
817
912
  revision_suffix.maybe >>
913
+ # …and the print date may also trail the repositioned revision
914
+ # ("IEEE Unapproved Draft P802.16Rev2/D9a, March 2009" reaches the
915
+ # grammar as "…/D9a/R-2, March 2009" via normalize_revision_notation).
916
+ (comma >> space? >> month_name.as(:trailing_month) >> space >>
917
+ year_digits.as(:trailing_year)).maybe >>
818
918
  # Trailing corrigendum after the draft ("…/D2.0/Cor. 1", or
819
919
  # "…/D1.0, Dec 2007/Cor. 1" where the draft's own draft_date consumes
820
920
  # the date, leaving "/Cor. N"). The flat corrigendum+draft tree (no
@@ -899,10 +999,17 @@ module Pubid
899
999
  # Draft NESC pattern
900
1000
  (str("Draft") >> space >> (str("NESC") | str("National Electrical Safety Code"))) |
901
1001
  # Name-first pattern (NEW)
902
- (str("National Electrical Safety Code") >> str(",") >> space >> str("C2-"))
1002
+ (str("National Electrical Safety Code") >> str(",") >> space >> str("C2-")) |
1003
+ # Catalogue form: "IEEE Std YYYY NESC ..." / "IEEE Std YYYY National
1004
+ # Electrical Safety Code" - the prefix is consumed here (the NESC
1005
+ # renderer prepends "IEEE Std" itself) but must not fall through to
1006
+ # the generic standard grammar, which would read YYYY as a number.
1007
+ (str("IEEE") >> space >> str("Std") >> space >> year_digits >> space >>
1008
+ (str("NESC") | str("National Electrical Safety Code")))
903
1009
  ).present? >>
904
1010
  # Delegate to NESC parser if pattern detected
905
- Nesc::Parser.new.nesc_identifier.as(:nesc)
1011
+ ((str("IEEE") >> space >> str("Std") >> space).maybe >>
1012
+ Nesc::Parser.new.nesc_identifier).as(:nesc)
906
1013
  end
907
1014
 
908
1015
  # Draft notation for PSI (e.g., /D2, /D3, rawbib's /D-2)
@@ -1056,13 +1163,13 @@ module Pubid
1056
1163
  str("CSA") >> space >>
1057
1164
  # CSA number formats (various patterns observed)
1058
1165
  (
1059
- # Format 1: C22.2 No. 293.1-17 (with NO.)
1060
- (str("C") >> digit.repeat(2) >> dot >> digit >> space >> str("No") >> dot >> space >>
1166
+ # Format 1: C22.2 No. 293.1-17 (with NO., either case)
1167
+ (str("C") >> digit.repeat(2) >> dot >> digit >> space >> (str("No") | str("NO")) >> dot >> space >>
1061
1168
  match("[0-9.]").repeat(1) >> (dash | str(":")) >> digit.repeat(2)) |
1062
1169
  # Format 2: C293.2-17 (without NO., dash year)
1063
1170
  (str("C") >> match("[0-9.]").repeat(1) >> dash >> digit.repeat(2)) |
1064
- # Format 3: C22.2 No. 293.3:19 (with NO., colon year)
1065
- (str("C") >> digit.repeat(2) >> dot >> digit >> space >> str("No") >> dot >> space >>
1171
+ # Format 3: C22.2 No. 293.3:19 (with NO., either case, colon year)
1172
+ (str("C") >> digit.repeat(2) >> dot >> digit >> space >> (str("No") | str("NO")) >> dot >> space >>
1066
1173
  match("[0-9.]").repeat(1) >> str(":") >> digit.repeat(2)) |
1067
1174
  # Format 4: C293.4:19 (without NO., colon year)
1068
1175
  (str("C") >> match("[0-9.]").repeat(1) >> str(":") >> digit.repeat(2))
@@ -1085,6 +1192,8 @@ module Pubid
1085
1192
  conformance_identifier | # NEW: Try conformance identifier before generic patterns
1086
1193
  joint_development_ieee_format |
1087
1194
  joint_development_iso_format |
1195
+ joint_development_iso_iec_edition | # stage-less ISO/IEC label with (E) - double-label portion
1196
+
1088
1197
  joint_development_embedded_stage | # stage-LAST embedded form (before generic)
1089
1198
  iec_ieee_copublished |
1090
1199
  number_first_identifier |
@@ -1110,6 +1219,15 @@ module Pubid
1110
1219
  # never collides with the base -YYYY identity year (see
1111
1220
  # ieee_p_identifier). The builder promotes it only when no base year.
1112
1221
  trailing_month_year.maybe >>
1222
+ # The ASHRAE joint suffix may also trail a draft+date
1223
+ # ("IEEE P1635/D13, December, 2017/ASHRAE Guideline 21"), so a
1224
+ # second slot catches it after the date.
1225
+ ashrae_copub.maybe >>
1226
+ # A bracketed narrative may sit between the base and a trailing
1227
+ # corrigendum: "IEEE Std 671-1985 [Corrigendum to IEEE Std 671-1985
1228
+ # (Reaff 2008)]/Cor. 1-2010" captures the bracket as the base's
1229
+ # nickname before the wrapper is parsed.
1230
+ book_nickname.maybe >>
1113
1231
  # Trailing corrigendum after the draft+date ("IEEE Approved P1015/D1,
1114
1232
  # Jan 2007/Cor. 1"): the generic bucket is the only path a status-word
1115
1233
  # form reaches. Routes via build_flat_corrigendum (disjoint from the
@@ -1333,9 +1451,12 @@ module Pubid
1333
1451
  # Slash-stage carrying a dash-date ("…P15288/CD2-2013-09 …" — the
1334
1452
  # dash-year tail is what the joint grammar reads after the number, so
1335
1453
  # this cannot steal the plain "/FDIS, June 2021" spellings
1336
- # ieee_p_identifier finishes): → "…CD2 P15288-2013-09 …"
1454
+ # ieee_p_identifier finishes): → "…CD2 P15288-2013-09 …". The date
1455
+ # must be a plausible year — a 4-digit monthcode (YYMM, e.g.
1456
+ # "…P24748-4/DIS-1404" = April 2014) is a DRAFT designator, not a
1457
+ # dash-date, and rewriting it onto the number breaks the parse.
1337
1458
  cleaned = cleaned.sub(
1338
- %r{\A#{pubs}#{num}/ ?#{stage}(-\d{4}(?:-\d\d)?)},
1459
+ %r{\A#{pubs}#{num}/ ?#{stage}(-(?:19|20)\d\d(?:-\d\d)?)},
1339
1460
  '\1\3 \2\4',
1340
1461
  )
1341
1462
 
@@ -1355,7 +1476,10 @@ module Pubid
1355
1476
  cleaned
1356
1477
  end
1357
1478
 
1358
- def self.parse(string)
1479
+ # Pre-parse ingestion normalizations (R2): every parse path —
1480
+ # parslet and PG artifact alike — feeds the grammar the same
1481
+ # normalized string.
1482
+ def self.normalize_input(string)
1359
1483
  # Strip .pdf extension if present (Pattern 3: File Extensions)
1360
1484
  cleaned = string.sub(/\.pdf$/i, "")
1361
1485
 
@@ -1709,11 +1833,10 @@ module Pubid
1709
1833
  # Fix 2H: "IEC XXXX First edition YYYY-MM; IEEE NNNN" -> normalize semicolon
1710
1834
  # Already handled by earlier semicolon normalization
1711
1835
 
1712
- # Fix 2I: "IEEE/ISO/IEC PXXX/DIS" -> normalize to "ISO/IEC/IEEE PXXX/DIS"
1713
- cleaned = cleaned.gsub(/^IEEE\/ISO\/IEC\s+(P[\w.-]+)/,
1714
- 'ISO/IEC/IEEE \1')
1715
- cleaned = cleaned.gsub(/^IEEE\/IEC\/ISO\s+(P[\w.-]+)/,
1716
- 'IEC/ISO/IEEE \1')
1836
+ # (Fix 2I removed: "IEEE/ISO/IEC PXXX/…" is no longer rewritten to
1837
+ # ISO-first — the printed publisher order is the organization's
1838
+ # perspective (pubid#469) and the joint P-form rule parses it as
1839
+ # printed.)
1717
1840
 
1718
1841
  # Fix 2J: "IEEE/IEC PXXX D5" -> normalize space to slash before D
1719
1842
  cleaned = cleaned.gsub(/^(IEEE\/IEC P[\w.-]+)\s+D(\d)/, '\1/D\2')
@@ -1812,7 +1935,11 @@ module Pubid
1812
1935
  # Fix 2AF: "IEEE Std 1003.1/2003.l/lNT" -> fix typos
1813
1936
  # .l -> .1 and lNT -> INT handled by existing fixes
1814
1937
 
1815
- new.parse(cleaned)
1938
+ cleaned
1939
+ end
1940
+
1941
+ def self.parse(string)
1942
+ new.parse(normalize_input(string))
1816
1943
  end
1817
1944
  end
1818
1945
  end