pubid 2.0.0.pre.alpha.11 → 2.0.0.pre.alpha.13

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 (274) 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/lib/pubid/adobe/builder.rb +2 -0
  6. data/lib/pubid/all_parts.rb +201 -0
  7. data/lib/pubid/all_parts_identifier.rb +19 -0
  8. data/lib/pubid/amca/CLAUDE.md +47 -0
  9. data/lib/pubid/amca/builder.rb +3 -5
  10. data/lib/pubid/amca/identifiers/base.rb +10 -0
  11. data/lib/pubid/amca/identifiers/publication.rb +13 -0
  12. data/lib/pubid/amca/parser.rb +2 -1
  13. data/lib/pubid/amca/renderer.rb +22 -33
  14. data/lib/pubid/amca/urn_generator.rb +21 -2
  15. data/lib/pubid/amca/urn_parser.rb +36 -10
  16. data/lib/pubid/ansi/builder.rb +6 -0
  17. data/lib/pubid/api/CLAUDE.md +23 -0
  18. data/lib/pubid/api/builder.rb +2 -0
  19. data/lib/pubid/ashrae/CLAUDE.md +13 -0
  20. data/lib/pubid/ashrae/builder.rb +58 -14
  21. data/lib/pubid/ashrae/identifiers/errata.rb +14 -2
  22. data/lib/pubid/ashrae/identifiers/interpretation.rb +2 -10
  23. data/lib/pubid/ashrae/parser.rb +62 -28
  24. data/lib/pubid/ashrae/renderer.rb +32 -1
  25. data/lib/pubid/ashrae/urn_generator.rb +32 -9
  26. data/lib/pubid/asme/CLAUDE.md +25 -0
  27. data/lib/pubid/asme/builder.rb +16 -9
  28. data/lib/pubid/asme/components/code.rb +2 -0
  29. data/lib/pubid/asme/identifiers/standard.rb +6 -1
  30. data/lib/pubid/asme/parser.rb +41 -14
  31. data/lib/pubid/astm/CLAUDE.md +9 -0
  32. data/lib/pubid/astm/builder.rb +2 -0
  33. data/lib/pubid/astm/components/code.rb +2 -0
  34. data/lib/pubid/astm/parser.rb +4 -1
  35. data/lib/pubid/bipm/CLAUDE.md +11 -0
  36. data/lib/pubid/bipm/builder.rb +2 -0
  37. data/lib/pubid/bsi/CLAUDE.md +93 -0
  38. data/lib/pubid/bsi/builder.rb +13 -11
  39. data/lib/pubid/bsi/components/date.rb +10 -4
  40. data/lib/pubid/bsi/identifiers/addendum_document.rb +2 -0
  41. data/lib/pubid/bsi/identifiers/adopted_european_norm.rb +6 -54
  42. data/lib/pubid/bsi/identifiers/adopted_international_standard.rb +5 -22
  43. data/lib/pubid/bsi/identifiers/amendment.rb +36 -12
  44. data/lib/pubid/bsi/identifiers/british_industrial_practice.rb +1 -1
  45. data/lib/pubid/bsi/identifiers/bundled_identifier.rb +2 -0
  46. data/lib/pubid/bsi/identifiers/consolidated_identifier.rb +23 -26
  47. data/lib/pubid/bsi/identifiers/corrigendum.rb +29 -12
  48. data/lib/pubid/bsi/identifiers/expert_commentary.rb +6 -7
  49. data/lib/pubid/bsi/identifiers/handbook.rb +1 -1
  50. data/lib/pubid/bsi/identifiers/national_annex.rb +18 -20
  51. data/lib/pubid/bsi/identifiers/practice_guide.rb +1 -1
  52. data/lib/pubid/bsi/identifiers/root_identity.rb +31 -0
  53. data/lib/pubid/bsi/identifiers/set.rb +2 -0
  54. data/lib/pubid/bsi/identifiers/supplement_document.rb +2 -0
  55. data/lib/pubid/bsi/identifiers.rb +1 -0
  56. data/lib/pubid/bsi/parser.rb +8 -8
  57. data/lib/pubid/bsi/renderer.rb +20 -20
  58. data/lib/pubid/bsi/single_identifier.rb +11 -2
  59. data/lib/pubid/bsi/urn_generator.rb +28 -18
  60. data/lib/pubid/builder/base.rb +27 -0
  61. data/lib/pubid/calconnect/builder.rb +2 -0
  62. data/lib/pubid/calconnect/identifier.rb +4 -0
  63. data/lib/pubid/ccsds/builder.rb +2 -0
  64. data/lib/pubid/ccsds/identifier.rb +12 -0
  65. data/lib/pubid/ccsds/single_identifier.rb +14 -10
  66. data/lib/pubid/cen_cenelec/CLAUDE.md +59 -0
  67. data/lib/pubid/cen_cenelec/builder.rb +6 -1
  68. data/lib/pubid/cen_cenelec/identifier.rb +10 -27
  69. data/lib/pubid/cen_cenelec/identifiers/amendment.rb +3 -10
  70. data/lib/pubid/cen_cenelec/identifiers/corrigendum.rb +3 -10
  71. data/lib/pubid/cen_cenelec/parser.rb +11 -3
  72. data/lib/pubid/cie/CLAUDE.md +58 -0
  73. data/lib/pubid/cie/builder.rb +2 -0
  74. data/lib/pubid/cie/components/language.rb +2 -0
  75. data/lib/pubid/components/adoption.rb +2 -0
  76. data/lib/pubid/components/code.rb +2 -0
  77. data/lib/pubid/components/date.rb +8 -6
  78. data/lib/pubid/components/edition.rb +2 -0
  79. data/lib/pubid/components/iteration.rb +2 -0
  80. data/lib/pubid/components/language.rb +2 -0
  81. data/lib/pubid/components/locality.rb +2 -0
  82. data/lib/pubid/components/publisher.rb +2 -0
  83. data/lib/pubid/components/relationship.rb +2 -0
  84. data/lib/pubid/components/stage.rb +2 -0
  85. data/lib/pubid/components/supplement.rb +2 -0
  86. data/lib/pubid/components/type.rb +2 -0
  87. data/lib/pubid/components/typed_stage.rb +8 -0
  88. data/lib/pubid/csa/CLAUDE.md +41 -0
  89. data/lib/pubid/csa/builder.rb +5 -3
  90. data/lib/pubid/csa/identifier.rb +13 -1
  91. data/lib/pubid/csa/identifiers/bundled.rb +2 -2
  92. data/lib/pubid/csa/identifiers/cec.rb +1 -1
  93. data/lib/pubid/csa/renderer.rb +12 -12
  94. data/lib/pubid/csa/single_identifier.rb +18 -1
  95. data/lib/pubid/csa/urn_generator.rb +1 -1
  96. data/lib/pubid/doi/builder.rb +2 -0
  97. data/lib/pubid/easc/builder.rb +2 -0
  98. data/lib/pubid/ecma/CLAUDE.md +28 -0
  99. data/lib/pubid/ecma/builder.rb +2 -0
  100. data/lib/pubid/ecma/identifier.rb +7 -0
  101. data/lib/pubid/etsi/CLAUDE.md +34 -0
  102. data/lib/pubid/etsi/builder.rb +2 -0
  103. data/lib/pubid/etsi/components/code.rb +6 -0
  104. data/lib/pubid/etsi/components/version.rb +2 -0
  105. data/lib/pubid/etsi/identifiers/etsi_standard.rb +7 -0
  106. data/lib/pubid/evs/CLAUDE.md +58 -0
  107. data/lib/pubid/evs/builder.rb +3 -1
  108. data/lib/pubid/evs/identifier.rb +6 -6
  109. data/lib/pubid/evs/identifiers/national_adoption.rb +7 -2
  110. data/lib/pubid/evs/renderer.rb +1 -1
  111. data/lib/pubid/evs/urn_generator.rb +1 -1
  112. data/lib/pubid/evs/urn_parser.rb +1 -1
  113. data/lib/pubid/gb/CLAUDE.md +140 -0
  114. data/lib/pubid/gb/builder.rb +21 -11
  115. data/lib/pubid/gb/identifier.rb +28 -7
  116. data/lib/pubid/gb/identifiers/all_parts.rb +17 -0
  117. data/lib/pubid/gb/identifiers.rb +1 -0
  118. data/lib/pubid/gb/parser.rb +4 -0
  119. data/lib/pubid/gb/renderer.rb +4 -4
  120. data/lib/pubid/gb.rb +1 -0
  121. data/lib/pubid/gost/CLAUDE.md +64 -0
  122. data/lib/pubid/gost/builder.rb +3 -1
  123. data/lib/pubid/gost/identifier.rb +5 -0
  124. data/lib/pubid/iala/CLAUDE.md +82 -0
  125. data/lib/pubid/iala/builder.rb +2 -0
  126. data/lib/pubid/iana/CLAUDE.md +7 -0
  127. data/lib/pubid/iana/builder.rb +2 -0
  128. data/lib/pubid/identifier.rb +172 -20
  129. data/lib/pubid/idf/builder.rb +6 -1
  130. data/lib/pubid/idf/identifier.rb +12 -0
  131. data/lib/pubid/idf/identifiers/all_parts.rb +17 -0
  132. data/lib/pubid/idf/identifiers/amendment.rb +1 -1
  133. data/lib/pubid/idf/identifiers/corrigendum.rb +1 -1
  134. data/lib/pubid/idf/identifiers/international_standard.rb +1 -1
  135. data/lib/pubid/idf/identifiers/reviewed_method.rb +1 -1
  136. data/lib/pubid/idf/identifiers.rb +1 -0
  137. data/lib/pubid/idf/single_identifier.rb +1 -1
  138. data/lib/pubid/iec/CLAUDE.md +31 -0
  139. data/lib/pubid/iec/builder.rb +7 -1
  140. data/lib/pubid/iec/components/consolidated_amendment.rb +4 -0
  141. data/lib/pubid/iec/components/sheet.rb +2 -0
  142. data/lib/pubid/iec/components/trf_info.rb +2 -0
  143. data/lib/pubid/iec/components/vap_suffix.rb +2 -0
  144. data/lib/pubid/iec/identifier.rb +7 -2
  145. data/lib/pubid/iec/identifiers/all_parts.rb +19 -0
  146. data/lib/pubid/iec/identifiers.rb +1 -0
  147. data/lib/pubid/iec/renderer.rb +0 -1
  148. data/lib/pubid/iec/urn_generator.rb +9 -1
  149. data/lib/pubid/iec/urn_parser.rb +3 -2
  150. data/lib/pubid/ieee/CLAUDE.md +97 -0
  151. data/lib/pubid/ieee/builder.rb +176 -7
  152. data/lib/pubid/ieee/components/code.rb +2 -0
  153. data/lib/pubid/ieee/components/draft.rb +35 -2
  154. data/lib/pubid/ieee/components/typed_stage.rb +2 -0
  155. data/lib/pubid/ieee/identifiers/base.rb +20 -0
  156. data/lib/pubid/ieee/identifiers/iec_ieee_copublished.rb +9 -0
  157. data/lib/pubid/ieee/identifiers/joint_development.rb +17 -10
  158. data/lib/pubid/ieee/identifiers/project_draft_identifier.rb +8 -1
  159. data/lib/pubid/ieee/identifiers/si_standard.rb +5 -2
  160. data/lib/pubid/ieee/ire/parser.rb +19 -1
  161. data/lib/pubid/ieee/parser.rb +337 -49
  162. data/lib/pubid/ieee/project_renderer.rb +46 -0
  163. data/lib/pubid/ieee/renderer.rb +46 -14
  164. data/lib/pubid/ieee/urn_generator.rb +31 -0
  165. data/lib/pubid/ieee.rb +2 -0
  166. data/lib/pubid/ietf/CLAUDE.md +7 -0
  167. data/lib/pubid/ietf/builder.rb +2 -0
  168. data/lib/pubid/iho/builder.rb +2 -0
  169. data/lib/pubid/isbn/builder.rb +2 -0
  170. data/lib/pubid/iso/CLAUDE.md +47 -0
  171. data/lib/pubid/iso/builder.rb +29 -16
  172. data/lib/pubid/iso/components/publisher.rb +2 -0
  173. data/lib/pubid/iso/components.rb +0 -1
  174. data/lib/pubid/iso/identifier.rb +17 -18
  175. data/lib/pubid/iso/identifiers/all_parts.rb +19 -0
  176. data/lib/pubid/iso/identifiers/directives.rb +7 -4
  177. data/lib/pubid/iso/identifiers/directives_supplement.rb +4 -2
  178. data/lib/pubid/iso/identifiers/tc_document.rb +31 -46
  179. data/lib/pubid/iso/identifiers.rb +1 -0
  180. data/lib/pubid/iso/normalizer.rb +1 -1
  181. data/lib/pubid/iso/rendering_style.rb +0 -1
  182. data/lib/pubid/iso/urn_generator.rb +10 -10
  183. data/lib/pubid/iso.rb +5 -4
  184. data/lib/pubid/itu/CLAUDE.md +69 -0
  185. data/lib/pubid/itu/builder.rb +12 -0
  186. data/lib/pubid/itu/components/code.rb +2 -0
  187. data/lib/pubid/itu/components/designation.rb +2 -0
  188. data/lib/pubid/itu/components/sector.rb +2 -0
  189. data/lib/pubid/itu/components/series.rb +2 -0
  190. data/lib/pubid/itu/identifiers/base.rb +10 -0
  191. data/lib/pubid/itu/identifiers/contribution.rb +31 -0
  192. data/lib/pubid/itu/identifiers/supplement.rb +15 -0
  193. data/lib/pubid/itu/identifiers.rb +1 -0
  194. data/lib/pubid/itu/parser.rb +19 -2
  195. data/lib/pubid/jcgm/CLAUDE.md +7 -0
  196. data/lib/pubid/jcgm/builder.rb +2 -0
  197. data/lib/pubid/jcgm/components/publisher.rb +2 -0
  198. data/lib/pubid/jis/builder.rb +5 -1
  199. data/lib/pubid/jis/identifier.rb +5 -17
  200. data/lib/pubid/jis/identifiers/all_parts.rb +19 -0
  201. data/lib/pubid/jis/identifiers.rb +1 -0
  202. data/lib/pubid/jis/renderer.rb +0 -2
  203. data/lib/pubid/jis/urn_generator.rb +0 -1
  204. data/lib/pubid/nist/CLAUDE.md +56 -0
  205. data/lib/pubid/nist/builder.rb +14 -12
  206. data/lib/pubid/nist/components/edition.rb +2 -0
  207. data/lib/pubid/nist/components/issue_number.rb +2 -0
  208. data/lib/pubid/nist/components/part.rb +2 -0
  209. data/lib/pubid/nist/components/stage.rb +2 -0
  210. data/lib/pubid/nist/components/supplement.rb +2 -0
  211. data/lib/pubid/nist/components/translation.rb +2 -0
  212. data/lib/pubid/nist/components/update.rb +2 -0
  213. data/lib/pubid/nist/components/version.rb +2 -0
  214. data/lib/pubid/nist/components/volume.rb +2 -0
  215. data/lib/pubid/nist/identifiers/base.rb +34 -6
  216. data/lib/pubid/nist/identifiers/commercial_standard_emergency.rb +1 -1
  217. data/lib/pubid/nist/identifiers/crpl_report.rb +5 -6
  218. data/lib/pubid/nist/identifiers/federal_information_processing_standards.rb +2 -2
  219. data/lib/pubid/nist/identifiers/miscellaneous_publication.rb +2 -2
  220. data/lib/pubid/nist/parser.rb +15 -2
  221. data/lib/pubid/nist/preprocessor.rb +53 -2
  222. data/lib/pubid/nist/series/ir.rb +3 -7
  223. data/lib/pubid/oasis/CLAUDE.md +19 -0
  224. data/lib/pubid/oasis/builder.rb +2 -0
  225. data/lib/pubid/oasis/identifier.rb +19 -0
  226. data/lib/pubid/ogc/CLAUDE.md +34 -0
  227. data/lib/pubid/ogc/builder.rb +2 -0
  228. data/lib/pubid/ogc/identifier.rb +11 -0
  229. data/lib/pubid/oiml/CLAUDE.md +189 -0
  230. data/lib/pubid/oiml/builder.rb +22 -2
  231. data/lib/pubid/oiml/components/code.rb +6 -0
  232. data/lib/pubid/oiml/identifier.rb +13 -0
  233. data/lib/pubid/oiml/identifiers/annex.rb +4 -0
  234. data/lib/pubid/oiml/identifiers/bulletin.rb +30 -36
  235. data/lib/pubid/oiml/identifiers/certification_system.rb +34 -0
  236. data/lib/pubid/oiml/identifiers/code_number.rb +10 -2
  237. data/lib/pubid/oiml/identifiers/dual_published.rb +174 -0
  238. data/lib/pubid/oiml/identifiers.rb +2 -0
  239. data/lib/pubid/oiml/parser.rb +41 -5
  240. data/lib/pubid/oiml/renderer.rb +26 -4
  241. data/lib/pubid/oiml/single_identifier.rb +5 -1
  242. data/lib/pubid/oiml/supplement_identifier.rb +7 -0
  243. data/lib/pubid/oiml/urn_generator.rb +32 -4
  244. data/lib/pubid/oiml.rb +5 -0
  245. data/lib/pubid/omg/CLAUDE.md +15 -0
  246. data/lib/pubid/omg/builder.rb +2 -0
  247. data/lib/pubid/parser/grammar.rb +23 -0
  248. data/lib/pubid/plateau/builder.rb +2 -0
  249. data/lib/pubid/plateau/identifiers/base.rb +4 -0
  250. data/lib/pubid/plateau/supplement_identifier.rb +14 -2
  251. data/lib/pubid/plateau/urn_generator.rb +7 -1
  252. data/lib/pubid/renderers/directives_renderer.rb +3 -1
  253. data/lib/pubid/renderers/human_readable.rb +0 -1
  254. data/lib/pubid/sae/builder.rb +3 -1
  255. data/lib/pubid/sae/components/date.rb +2 -0
  256. data/lib/pubid/sae/components/type.rb +2 -0
  257. data/lib/pubid/sae/identifiers/base.rb +4 -1
  258. data/lib/pubid/sae/urn_generator.rb +1 -1
  259. data/lib/pubid/subset_match.rb +197 -0
  260. data/lib/pubid/tgpp/CLAUDE.md +43 -0
  261. data/lib/pubid/tgpp/builder.rb +2 -0
  262. data/lib/pubid/tgpp/identifier.rb +14 -0
  263. data/lib/pubid/type_resolver.rb +14 -2
  264. data/lib/pubid/un/builder.rb +2 -0
  265. data/lib/pubid/version.rb +1 -1
  266. data/lib/pubid/w3c/CLAUDE.md +7 -0
  267. data/lib/pubid/w3c/builder.rb +2 -0
  268. data/lib/pubid/xsf/CLAUDE.md +11 -0
  269. data/lib/pubid/xsf/builder.rb +2 -0
  270. data/lib/pubid.rb +16 -3
  271. data/lib/tasks/docs.rake +13 -11
  272. metadata +46 -4
  273. data/lib/pubid/ccsds/identifiers/base_BASE_88929.rb +0 -70
  274. data/lib/pubid/iso/components/code.rb +0 -24
@@ -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)) |
@@ -582,15 +597,32 @@ module Pubid
582
597
  # ALSO handle: IEEE/CSA P844.1/293.1/D2 (CSA dual numbering)
583
598
  (str("ISO/IEC/IEEE") | str("ISO/IEEE") | str("IEC/IEEE") | str("IEEE/CSA")).as(:joint_publishers) >>
584
599
  space >>
585
- str("P") >> # P indicates IEEE-led
600
+ # P = project (the document is a draft): identity-bearing, so it
601
+ # is captured and preserved, never silently consumed.
602
+ str("P").as(:project_marker) >>
586
603
  digits.as(:number) >>
587
604
  ((dot | dash) >> digits.as(:part)).maybe >> # Optional part like .1 or -1
588
605
  # CSA dual numbering: /293.1 (second number)
589
606
  (slash >> digits >> (dot >> digits).maybe >> (dash >> digits.as(:draft_version)).maybe).maybe >>
590
607
  (
591
- # Variant 1: /D8 notation (original)
592
- (slash >> str("D") >> digits.as(:draft_version)) |
593
- # Variant 2: , CDV1 notation (comma before stage code)
608
+ # Variant 1b: the ordinal-less stage draft "D=CDV[:2020]" -
609
+ # D (draft) = CDV (the IEC stage it drafts). The year rides in
610
+ # the draft clause (a distinct key, so the builder keeps the
611
+ # date inside the designator).
612
+ (slash >> str("D") >> str("=") >>
613
+ (str("CDV") | str("FDIS") | str("PWI") | str("WD") |
614
+ str("NP") | str("DIS") | str("CD")).as(:draft_iso_stage) >>
615
+ (str(":") >> year_digits.as(:draft_stage_year)).maybe) |
616
+ # Variant 1: /D8 notation (original), with the compound
617
+ # both-systems suffix "=DDIS.3" (docs/IEEE-DRAFT-STAGES.md §1.3)
618
+ (slash >> str("D") >> digits.as(:draft_version) >>
619
+ (str("=") >> (str("D") >> (str("FDIS") | str("CDV") | str("PWI") |
620
+ str("DIS") | str("WD") | str("NP") | str("CD")) |
621
+ (str("FDIS") | str("CDV") | str("PWI") | str("DIS") | str("WD") |
622
+ str("NP") | str("CD"))).as(:draft_iso_stage) >>
623
+ (dot | dash).maybe >> digits.as(:draft_iso_iteration).maybe).maybe) |
624
+ # Variant 2: , CDV1 notation (comma before stage code) —
625
+ # the stage draft of the named ISO/IEC stage, its iteration
594
626
  (comma >> (str("CDV") | str("FDIS") | str("CD") | str("DIS")).as(:iec_stage) >> digits.maybe.as(:stage_iteration))
595
627
  ).maybe >>
596
628
  # Optional edition, from relaton's "/E-<n>" suffix normalized to
@@ -611,42 +643,101 @@ module Pubid
611
643
  # "-YYYY[-MM]" date (instead of ":YYYY"), multi-digit committee-draft
612
644
  # stage codes (CD1..CD4) plus CDV, and a wider set of joint publishers.
613
645
  # (roadmap items 2/3, phase 1). longest publisher token first.
614
- (str("ISO/IEC/IEEE") | str("IEEE/ISO/IEC") | str("IEEE/IEC/ISO") |
615
- str("ISO/IEEE") | str("IEC/IEEE") | str("IEEE/IEC") | str("ISO/IEC") |
616
- str("IEEE")).as(:joint_publishers) >>
617
- space >>
618
- # ISO stage codes: FDIS, FCD, CDV; DIS/CD with an optional round digit
619
- # (DIS2, CD1..CD4); WD/PWI/NP. (FCD before FDIS is fine — distinct.)
620
- (str("FDIS") | str("FCD") | str("CDV") |
621
- (str("DIS") >> digit.maybe) |
622
- (str("CD") >> digit.maybe) |
623
- str("WD") | str("PWI") | str("NP")).as(:iso_stage) >>
624
- # optional " Std" noise word after the stage (e.g. "FDIS Std P15288")
625
- (space >> str("Std")).maybe >>
626
- space >>
627
- str("P").maybe >> # optional project marker on the number
646
+ # The stage is OPTIONAL ONLY for ISO-led joint prefixes - the
647
+ # stage-less PUBLISHED form (pubid#317): "ISO/IEC/IEEE 26511:2018",
648
+ # "ISO/IEEE 11073-20101:2004(E)". Every other prefix keeps the
649
+ # stage REQUIREMENT so this rule cannot steal what belongs to
650
+ # iec_ieee_copublished ("IEC/IEEE 60076-2016"), the bare-IEEE
651
+ # project rules ("IEEE P802.16/D-3") or the ISO flavor
652
+ # ("ISO/IEC <n>:<year>").
653
+ iso_led = (str("ISO/IEC/IEEE") | str("IEEE/ISO/IEC") |
654
+ str("IEEE/IEC/ISO") | str("ISO/IEEE"))
655
+ staged_only = (str("IEC/IEEE") | str("IEEE/IEC") |
656
+ str("ISO/IEC") | str("IEEE"))
657
+ # ISO stage codes: FDIS, FCD, CDV; DIS/CD with an optional round
658
+ # digit (DIS2, CD1..CD4); WD/PWI/NP.
659
+ iso_stage = (str("FDIS") | str("FCD") | str("CDV") |
660
+ (str("DIS") >> digit.maybe) |
661
+ (str("CD") >> digit.maybe) |
662
+ str("WD") | str("PWI") | str("NP")).as(:iso_stage)
663
+ std_noise = (space >> str("Std")).maybe
664
+ # The empty-string marker distinguishes the stage-less PUBLISHED
665
+ # tree in the builder (absence of :iso_stage alone means IEEE format).
666
+ ((iso_led.as(:joint_publishers) >> space >>
667
+ (iso_stage >> std_noise >> space |
668
+ str("").as(:iso_published) >> space.maybe)) |
669
+ (staged_only.as(:joint_publishers) >> space >>
670
+ iso_stage >> std_noise >> space)) >>
671
+ str("P").as(:project_marker).maybe >> # project marker (P =
672
+ # project/draft; its
673
+ # presence is identity)
628
674
  digits.as(:number) >>
629
- # part must not swallow the trailing year (year_digits.absent?)
630
- ((dot | dash) >> year_digits.absent? >> digits.as(:part)).maybe >>
675
+ # part must not swallow the trailing year (year_digits.absent?). A
676
+ # DASH-joined part is tagged (:part_dash) - the catalogue-printed
677
+ # joint form spells the part with a dash ("21451-7") where the ISO
678
+ # form uses the dot.
679
+ ((dot >> year_digits.absent? >> digits.as(:part)) |
680
+ (dash >> str("").as(:part_dash) >> year_digits.absent? >>
681
+ digits.as(:part))).maybe >>
631
682
  (
632
683
  (str(":") >> year_digits.as(:year)) |
684
+ # The dash-year (and ", Month YYYY" text date below) spellings are
685
+ # the catalogue-PRINTED joint form; tag them (:printed_form) so the
686
+ # builder routes to a Standard instead of the ISO joint reference.
633
687
  (dash >> year_digits.as(:year) >>
634
- (dash >> month_numeric.as(:month)).maybe) |
688
+ (dash >> month_numeric.as(:month)).maybe >>
689
+ str("").as(:printed_dash_year)) |
635
690
  # Trailing text date ", April 2015" / " April 2015" (build_joint_development
636
691
  # already reads :month/:year). No :year collision — the iso rule has no
637
692
  # other :year capture (edition uses :edition_year).
638
- ((comma | space) >> month_name.as(:month) >> space >> year_digits.as(:year))
693
+ ((comma | space) >> month_name.as(:month) >> space >>
694
+ year_digits.as(:year) >> str("").as(:printed_month_year))
639
695
  ).maybe >>
640
696
  # Optional /D<draft> tail. normalize_relaton_suffixes repositions the
641
697
  # historical "…/D-3-2017" onto the number as "…-2017/D3", so by the
642
698
  # time this rule runs the draft usually trails the date (bucket 5);
643
699
  # a date-less "/D-4" keeps its hyphen (bucket 7), hence dash.maybe.
700
+ # A text date may trail the DRAFT itself (pubid#216:
701
+ # "CD P26515/D1, March 2017", "FDIS P15289/D3, 2017") — distinct
702
+ # keys (:draft_month/:draft_year), because a plain :month/:year
703
+ # here collides with the date clause above and parslet drops the
704
+ # subtree with a "Duplicate subtrees" warning.
705
+ # The ISO/IEC edition marker may sit between the year and an
706
+ # amendment/draft tail: "8802.11:2012 (E)/Amd 1-2014". A distinct
707
+ # key - a second :parameters capture here collides with the
708
+ # trailing parenthetical slot (parslet drops duplicate subtrees).
709
+ (space.maybe >> str("(") >>
710
+ match("[^)]").repeat(1).as(:edition_marker) >> str(")")).maybe >>
711
+ # The stage-draft clause of the ISO-led print: "/D=WD.5" -
712
+ # D (draft) = the ISO/IEC stage it drafts, with its iteration
713
+ # (docs/IEEE-DRAFT-STAGES.md §1.2).
714
+ ((slash >> str("D") >> str("=") >>
715
+ (str("FDIS") | str("FCD") | str("CDV") |
716
+ (str("DIS") >> digit.maybe) |
717
+ (str("CD") >> digit.maybe) |
718
+ str("WD") | str("PWI") | str("NP")).as(:draft_iso_stage) >>
719
+ (dot >> (digits >> match("[A-Za-z]").repeat(0, 1)).as(:draft_iso_iteration)).maybe) |
644
720
  (slash >> str("D") >> dash.maybe >>
645
- match('[0-9.]').repeat(1).as(:draft_version)).maybe >>
721
+ match('[0-9.]').repeat(1).as(:draft_version) >>
722
+ (((comma | space) >> month_name.as(:draft_month) >> space >>
723
+ year_digits.as(:draft_year)) |
724
+ (comma >> year_digits.as(:draft_year))).maybe).maybe).maybe >>
725
+ # Optional amendment tail (pubid#317:
726
+ # "8802-11:2012/Amd.1:2014(E)") - the flat tree keys reuse
727
+ # build_flat_amendment.
728
+ (slash >> str("Amd") >> (dot | space).maybe >>
729
+ digits.as(:amd_number) >>
730
+ ((str(":") | dash) >> year_digits.as(:amd_year)).maybe).maybe >>
646
731
  # Optional edition, from relaton's "/E-<n>" suffix normalized to
647
732
  # "Edition <n>.0[ YYYY]" (nil-residue hand-off item 1).
648
733
  edition.maybe >>
649
- revision_suffix.maybe
734
+ revision_suffix.maybe >>
735
+ # Published joint docs print a language marker, after either the
736
+ # year or the amendment tail: "9945:2009(E)", "...Amd.1:2014(E)".
737
+ # The crawl sometimes spaces it ("…(E), January 2017" family,
738
+ # pubid#216), and a relationship parenthetical may follow
739
+ # ("…CD2 P15288-2013-09 (Revision of …)").
740
+ parenthetical.maybe
650
741
  end
651
742
 
652
743
  # Embedded (stage-LAST) ISO-led designations: the corpus writes the ISO
@@ -659,6 +750,28 @@ module Pubid
659
750
  # the trailing `.FDIS` is the ISO stage, NOT a second part. Routes through
660
751
  # the same build_joint_development (via :joint_publishers/:iso_stage), so the
661
752
  # stage is modeled correctly and the numeric part stays separate.
753
+ # The stage-less ISO/IEC label with a colon year and the ISO/IEC
754
+ # edition marker: "ISO/IEC 13210:1994 (E)" - the ISO/IEC portion of a
755
+ # double-labeled standard. The (E) parenthetical is REQUIRED: without
756
+ # it this spelling is the ISO flavor's own form and must not be
757
+ # stolen. Routes through build_joint_development like every ISO-led
758
+ # joint reference.
759
+ rule(:joint_development_iso_iec_edition) do
760
+ str("ISO/IEC").as(:joint_publishers) >> space >>
761
+ str("").as(:iso_published) >>
762
+ str("P").as(:project_marker).maybe >>
763
+ digits.as(:number) >>
764
+ # The part may be dot- or dash-joined ("8802-9"); a dash-year
765
+ # ("1234-2012") is not a part.
766
+ # A distinct key: the iso-format route's :part_dash prints as a
767
+ # dot in the joint code (the "P24748.2:2018" convention); only
768
+ # the ISO/IEC LABEL keeps its printed dash.
769
+ ((dot | (dash >> str("").as(:iec_label_dash) >> year_digits.absent?)) >>
770
+ digits.as(:part)).maybe >>
771
+ (str(":") >> space.maybe >> year_digits.as(:year)) >>
772
+ space.maybe >> parenthetical
773
+ end
774
+
662
775
  rule(:joint_development_embedded_stage) do
663
776
  # publisher set mirrors joint_development_iso_format (incl. bare IEEE);
664
777
  # longest token first.
@@ -666,7 +779,9 @@ module Pubid
666
779
  str("ISO/IEEE") | str("IEC/IEEE") | str("IEEE/IEC") | str("ISO/IEC") |
667
780
  str("IEEE")).as(:joint_publishers) >>
668
781
  space >>
669
- str("P").maybe >> # optional project marker on the number
782
+ str("P").as(:project_marker).maybe >> # project marker (P =
783
+ # project/draft; its
784
+ # presence is identity)
670
785
  digits.as(:number) >>
671
786
  # optional numeric part (dot or dash); must not swallow a year
672
787
  ((dot | dash) >> year_digits.absent? >> digits.as(:part)).maybe >>
@@ -701,9 +816,13 @@ module Pubid
701
816
  end
702
817
 
703
818
  # IEEE P pattern (without Std): "IEEE P1003.1..." OR just "P1003.1..." (prefix optional)
819
+ # `P` is not consumed here: the `number` rule carries its own optional
820
+ # leading P and captures it into :number ("P1003.1"), so Code.parse
821
+ # peels it as code.prefix and the project marker survives the parse —
822
+ # a bare `str("P")` swallowed it and "P1201/D0.3" came back without
823
+ # its P at all (pubid#18's data-loss complaint).
704
824
  rule(:ieee_p_identifier) do
705
825
  (str("IEEE").as(:publisher) >> space).maybe >> # Make IEEE prefix optional
706
- str("P") >> space.maybe >> # Make space after P optional
707
826
  number >>
708
827
  (part_subpart_year | edition).maybe >>
709
828
  # Pattern for /08 style drafts (digits without D prefix) - MUST come before corrigendum
@@ -759,15 +878,17 @@ module Pubid
759
878
  # status is captured (draft_status ends in a space, so "Approved Draft"
760
879
  # splits cleanly) and round-trips as the draft_status attribute; the literal
761
880
  # "Draft" stays a bare marker (dropped on render, like the plain form).
881
+ # `P` is not consumed here: the `number` rule carries its own optional
882
+ # leading P and captures it into :number ("P802.11"), so Code.parse peels
883
+ # it as code.prefix and the project marker round-trips (pubid#318) — a
884
+ # bare `str("P").maybe` here swallowed it and the render lost the P.
762
885
  rule(:ieee_draft_p_identifier) do
763
886
  (str("IEEE").as(:publisher) >> space).maybe >> # Make IEEE prefix optional
764
887
  draft_status.as(:draft_status).maybe >>
765
888
  str("Draft") >> space >>
766
- # `P` is optional — a status-word draft may carry a bare number
767
- # ("IEEE Unapproved Draft 802.1ah/D4.2"), mirroring
768
- # ieee_approved_draft_identifier's str("P").maybe. The `number` rule
889
+ # A status-word draft may carry a bare number
890
+ # ("IEEE Unapproved Draft 802.1ah/D4.2"). The `number` rule
769
891
  # already accepts the bare forms (802.1ah, C57.15, 11073-10471).
770
- str("P").maybe >>
771
892
  number >>
772
893
  (part_subpart_year | edition).maybe >>
773
894
  # Trailing "Month YYYY" date under distinct keys so it never collides
@@ -775,7 +896,17 @@ module Pubid
775
896
  ((space >> month_name.as(:trailing_month) >> space >> year_digits.as(:trailing_year)) |
776
897
  (space >> month_numeric.as(:trailing_month) >> (space | dash) >> year_digits.as(:trailing_year))).maybe >>
777
898
  draft.maybe >>
899
+ # A print date may trail a draft that carries its own date
900
+ # ("IEEE Unapproved Draft 24765/D1 2009, Oct 2009" - the draft's
901
+ # year is "2009", the print date is ", Oct 2009").
902
+ (comma >> space? >> month_name.as(:trailing_month) >> space >>
903
+ year_digits.as(:trailing_year)).maybe >>
778
904
  revision_suffix.maybe >>
905
+ # …and the print date may also trail the repositioned revision
906
+ # ("IEEE Unapproved Draft P802.16Rev2/D9a, March 2009" reaches the
907
+ # grammar as "…/D9a/R-2, March 2009" via normalize_revision_notation).
908
+ (comma >> space? >> month_name.as(:trailing_month) >> space >>
909
+ year_digits.as(:trailing_year)).maybe >>
779
910
  # Trailing corrigendum after the draft ("…/D2.0/Cor. 1", or
780
911
  # "…/D1.0, Dec 2007/Cor. 1" where the draft's own draft_date consumes
781
912
  # the date, leaving "/Cor. N"). The flat corrigendum+draft tree (no
@@ -836,6 +967,11 @@ module Pubid
836
967
  # Year-first pattern: "52 IRE 7.S2" or "60 IRE 28 PS7"
837
968
  ((match("[1-6]") >> digit >> space >> str("IRE")) | # 2-digit year format
838
969
  (str("19") >> digit.repeat(2, 2) >> space >> str("IRE"))) |
970
+ # Prefix-first pattern: "IRE 7.S2-1952" (pubid#316 family 2) —
971
+ # IRE then a designation (digits or a type word). Loose on
972
+ # purpose: the sub-parser is strict, and a failed delegation just
973
+ # falls through to the remaining alternatives.
974
+ (str("IRE") >> space >> (digits | str("Standard") | str("Std") | str("Trans"))) |
839
975
  # IEEE-IRE transitional pattern
840
976
  (str("IEEE-IRE") >> space)
841
977
  ).present? >>
@@ -855,29 +991,60 @@ module Pubid
855
991
  # Draft NESC pattern
856
992
  (str("Draft") >> space >> (str("NESC") | str("National Electrical Safety Code"))) |
857
993
  # Name-first pattern (NEW)
858
- (str("National Electrical Safety Code") >> str(",") >> space >> str("C2-"))
994
+ (str("National Electrical Safety Code") >> str(",") >> space >> str("C2-")) |
995
+ # Catalogue form: "IEEE Std YYYY NESC ..." / "IEEE Std YYYY National
996
+ # Electrical Safety Code" - the prefix is consumed here (the NESC
997
+ # renderer prepends "IEEE Std" itself) but must not fall through to
998
+ # the generic standard grammar, which would read YYYY as a number.
999
+ (str("IEEE") >> space >> str("Std") >> space >> year_digits >> space >>
1000
+ (str("NESC") | str("National Electrical Safety Code")))
859
1001
  ).present? >>
860
1002
  # Delegate to NESC parser if pattern detected
861
- Nesc::Parser.new.nesc_identifier.as(:nesc)
1003
+ ((str("IEEE") >> space >> str("Std") >> space).maybe >>
1004
+ Nesc::Parser.new.nesc_identifier).as(:nesc)
1005
+ end
1006
+
1007
+ # Draft notation for PSI (e.g., /D2, /D3, rawbib's /D-2)
1008
+ rule(:psi_draft) do
1009
+ slash >> str("D") >> dash.maybe >> digits.as(:draft_version)
1010
+ end
1011
+
1012
+ # Date on an SI/PSI identifier: ", Month Year" or "-YEAR[-MM]". The
1013
+ # comma branch also accepts a NUMERIC month and a DASH before the
1014
+ # year — the renderer emits ", 05 2010" for a numeric-month draft,
1015
+ # and the preprocessing gsub rewrites a trailing " <digits> <year>"
1016
+ # to "<digits>-<year>" (the same load-bearing coupling draft_date
1017
+ # documents), so the re-parse must read its own rendering back.
1018
+ rule(:psi_date) do
1019
+ (comma >> (month_name | month_numeric).as(:month) >> (space | dash) >>
1020
+ year_digits.as(:year)) |
1021
+ (dash >> year_digits.as(:year) >>
1022
+ (dash >> digit.repeat(2, 2).as(:month)).maybe)
862
1023
  end
863
1024
 
864
1025
  # IEEE/ASTM SI/PSI (Système International) patterns
865
1026
  # SI = Published metric system standard
866
1027
  # PSI = Proposed SI (draft)
1028
+ # Rawbib spellings (pubid#316 family 3): a bare "IEEE" publisher, a dot
1029
+ # separator after the type ("PSI.10", "SI 10.1997"), relaton's
1030
+ # hyphenated "/D-<n>" draft, and a dash year-month ("-2010-05").
867
1031
  rule(:ieee_astm_si_psi) do
868
- str("IEEE/ASTM").as(:publishers) >>
1032
+ (str("IEEE/ASTM") | str("IEEE")).as(:publishers) >>
869
1033
  space >>
870
1034
  (str("PSI") | str("SI")).as(:si_type) >>
871
- space >>
1035
+ (space | str(".")) >>
872
1036
  digits.as(:number) >>
873
- # Draft notation for PSI (e.g., /D2, /D3)
874
- (slash >> str("D") >> digits.as(:draft_version)).maybe >>
875
- # Year with optional month
1037
+ # Glued ".YEAR" edition date ("IEEE/ASTM SI 10.1997")
1038
+ (str(".") >> year_digits.as(:year)).maybe >>
1039
+ # The date and the draft appear in EITHER order: the legacy
1040
+ # spelling puts the draft first ("PSI 10/D2, October 2015"),
1041
+ # while normalize_relaton_suffixes repositions the rawbib
1042
+ # hyphenated form onto the number ("PSI 10/D-3-2010" →
1043
+ # "PSI 10-2010/D3"). Draft-first is tried first so the legacy
1044
+ # comma-date keeps its original match; each side is optional so
1045
+ # a date-only or draft-less form still parses.
876
1046
  (
877
- # Format: ", Month Year"
878
- (comma >> month_name.as(:month) >> space >> year_digits.as(:year)) |
879
- # Format: "-YEAR"
880
- (dash >> year_digits.as(:year))
1047
+ (psi_draft >> psi_date.maybe) | (psi_date >> psi_draft.maybe)
881
1048
  ).maybe >>
882
1049
  # Optional parenthetical (revision relationships)
883
1050
  parenthetical.maybe
@@ -988,13 +1155,13 @@ module Pubid
988
1155
  str("CSA") >> space >>
989
1156
  # CSA number formats (various patterns observed)
990
1157
  (
991
- # Format 1: C22.2 No. 293.1-17 (with NO.)
992
- (str("C") >> digit.repeat(2) >> dot >> digit >> space >> str("No") >> dot >> space >>
1158
+ # Format 1: C22.2 No. 293.1-17 (with NO., either case)
1159
+ (str("C") >> digit.repeat(2) >> dot >> digit >> space >> (str("No") | str("NO")) >> dot >> space >>
993
1160
  match("[0-9.]").repeat(1) >> (dash | str(":")) >> digit.repeat(2)) |
994
1161
  # Format 2: C293.2-17 (without NO., dash year)
995
1162
  (str("C") >> match("[0-9.]").repeat(1) >> dash >> digit.repeat(2)) |
996
- # Format 3: C22.2 No. 293.3:19 (with NO., colon year)
997
- (str("C") >> digit.repeat(2) >> dot >> digit >> space >> str("No") >> dot >> space >>
1163
+ # Format 3: C22.2 No. 293.3:19 (with NO., either case, colon year)
1164
+ (str("C") >> digit.repeat(2) >> dot >> digit >> space >> (str("No") | str("NO")) >> dot >> space >>
998
1165
  match("[0-9.]").repeat(1) >> str(":") >> digit.repeat(2)) |
999
1166
  # Format 4: C293.4:19 (without NO., colon year)
1000
1167
  (str("C") >> match("[0-9.]").repeat(1) >> str(":") >> digit.repeat(2))
@@ -1017,6 +1184,8 @@ module Pubid
1017
1184
  conformance_identifier | # NEW: Try conformance identifier before generic patterns
1018
1185
  joint_development_ieee_format |
1019
1186
  joint_development_iso_format |
1187
+ joint_development_iso_iec_edition | # stage-less ISO/IEC label with (E) - double-label portion
1188
+
1020
1189
  joint_development_embedded_stage | # stage-LAST embedded form (before generic)
1021
1190
  iec_ieee_copublished |
1022
1191
  number_first_identifier |
@@ -1042,6 +1211,15 @@ module Pubid
1042
1211
  # never collides with the base -YYYY identity year (see
1043
1212
  # ieee_p_identifier). The builder promotes it only when no base year.
1044
1213
  trailing_month_year.maybe >>
1214
+ # The ASHRAE joint suffix may also trail a draft+date
1215
+ # ("IEEE P1635/D13, December, 2017/ASHRAE Guideline 21"), so a
1216
+ # second slot catches it after the date.
1217
+ ashrae_copub.maybe >>
1218
+ # A bracketed narrative may sit between the base and a trailing
1219
+ # corrigendum: "IEEE Std 671-1985 [Corrigendum to IEEE Std 671-1985
1220
+ # (Reaff 2008)]/Cor. 1-2010" captures the bracket as the base's
1221
+ # nickname before the wrapper is parsed.
1222
+ book_nickname.maybe >>
1045
1223
  # Trailing corrigendum after the draft+date ("IEEE Approved P1015/D1,
1046
1224
  # Jan 2007/Cor. 1"): the generic bucket is the only path a status-word
1047
1225
  # form reaches. Routes via build_flat_corrigendum (disjoint from the
@@ -1159,12 +1337,15 @@ module Pubid
1159
1337
  # A "\d+" right after "Rev" both selects the numbered subset and keeps
1160
1338
  # these off the English word "Revision". Three source positions:
1161
1339
  # after a draft : "PC37.30.2/D043 Rev 18" -> ".../D043/R-18"
1340
+ # The draft captures accept relaton's hyphenated "/D-<n>" spelling
1341
+ # (pubid#316): without the "-?" the before-draft regex matched only
1342
+ # "/D" of "/D-3" and emitted the garbage "/D/R-2-3-2008-02".
1162
1343
  cleaned = cleaned.sub(
1163
- %r{(/D[0-9A-Za-z.]*)\s+[Rr][Ee][Vv]\s*(\d+)}, '\1/R-\2'
1344
+ %r{(/D-?[0-9A-Za-z.]+)\s+[Rr][Ee][Vv]\s*(\d+)}, '\1/R-\2'
1164
1345
  )
1165
1346
  # before a draft: "P802.16Rev2/D3" -> "P802.16/D3/R-2"
1166
1347
  cleaned = cleaned.sub(
1167
- %r{[-/_.]?\s?[Rr][Ee][Vv][-\s]?(\d+)(/D[0-9A-Za-z.]*)}, '\2/R-\1'
1348
+ %r{[-/_.]?\s?[Rr][Ee][Vv][-\s]?(\d+)(/D-?[0-9A-Za-z.]+)}, '\2/R-\1'
1168
1349
  )
1169
1350
  # no draft, trailing: "P1722-rev1" -> "P1722/R-1"
1170
1351
  cleaned = cleaned.sub(
@@ -1176,7 +1357,7 @@ module Pubid
1176
1357
  # became "/R-<n>", so these regexes only see the lettered residue.
1177
1358
  # Revision token that PRECEDES a draft: drop it (keep the /D…).
1178
1359
  cleaned = cleaned.sub(
1179
- %r{[-/_.]?\s?[Rr][Ee][Vv][-\s]?[A-Za-z0-9]+(?=/D[0-9])},
1360
+ %r{[-/_.]?\s?[Rr][Ee][Vv][-\s]?[A-Za-z0-9]+(?=/D-?[0-9A-Za-z])},
1180
1361
  "",
1181
1362
  )
1182
1363
  # Trailing revision glued to the number with no draft ("P802.11REVmb");
@@ -1185,6 +1366,108 @@ module Pubid
1185
1366
  cleaned.sub(%r{(\d)[Rr][Ee][Vv][A-Za-z0-9]+\s*\z}, '\1')
1186
1367
  end
1187
1368
 
1369
+ # Rewrite the mechanical spellings of the joint ISO-stage forms that no
1370
+ # grammar branch reaches (pubid#216 residuals). Two safety rules keep
1371
+ # this from stealing inputs other rules already parse:
1372
+ # 1. only BROKEN separators fire — underscore/space glue between
1373
+ # number, draft and stage, a slash-SPACE before the stage, a
1374
+ # dash/underscore month after it. A plain "/FDIS" tail is
1375
+ # natively accepted by ieee_p_identifier (fdraft) and the
1376
+ # stage-LAST embedded rule, so it is never rewritten.
1377
+ # 2. the number carries at most ONE part — joint_development_iso_format
1378
+ # reads a single optional part, while ieee_p_identifier takes
1379
+ # multi-part numbers ("P62271-37-013"); a rewrite that moved a
1380
+ # multi-part number onto the joint rule would break it.
1381
+ def self.normalize_joint_stage_spellings(cleaned)
1382
+ # The separator (slash-optional-space or a space) is INSIDE the
1383
+ # capture so a rewrite re-emits it — an uncaptured separator was
1384
+ # silently eaten, gluing the publisher to the stage.
1385
+ pubs = %r{((?:ISO/IEC/IEEE|IEEE/ISO/IEC|IEEE/IEC/ISO|ISO/IEEE|IEC/IEEE|IEEE/IEC|ISO/IEC|IEEE)(?:/ ?| ))}
1386
+ stage = /(FDIS|FCD|CDV|DIS\d?|CD\d?|WD|PWI|NP)/
1387
+ num = /(P?\d+(?:[.-]\d+)?)/
1388
+ # What the joint grammar can finish reading AFTER the stage: the end
1389
+ # of the string, a (comma-)month-year date, or a dash-year[-month].
1390
+ # Captured and re-emitted so a rewrite never drops the date.
1391
+ tail = /(,? (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec)[a-z]* \d{4}|\z|-\d{4}(?:-\d\d)?)/
1392
+
1393
+ # Draft glued to the number with an underscore: "P15289_D3" →
1394
+ # "P15289/D3" (a digit must precede the underscore; "_FDIS" is the
1395
+ # stage, not a draft, and is already slash-rewritten earlier).
1396
+ cleaned = cleaned.gsub(%r{(\d)_D(\d)}, '\1/D\2')
1397
+
1398
+ # Crawl typo: a comma with no space before a month (",March 2021").
1399
+ # In these identifiers a comma is always a date separator, so
1400
+ # spacing it is safe.
1401
+ cleaned = cleaned.sub(/,(?=[A-Za-z])/, ", ")
1402
+
1403
+ # Stage AFTER the draft, both glued with underscores (the early
1404
+ # stage gsub already turned "_CD" into "/CD"):
1405
+ # "…P24748-3/D3/FDIS, April 2020 (E)" → "…FDIS P24748-3/D3, …"
1406
+ cleaned = cleaned.sub(
1407
+ %r{\A#{pubs}#{num}/(D[\d.]+)/ ?#{stage}\b},
1408
+ '\1\4 \2/\3',
1409
+ )
1410
+
1411
+ # Slash-SPACE before the stage ("…P16085/ FDIS, August 2020" —
1412
+ # ieee_p's fdraft has no inner space, so this spelling has no other
1413
+ # home): → "…FDIS P16085, August 2020"
1414
+ cleaned = cleaned.sub(
1415
+ %r{\A#{pubs}#{num}/ #{stage}#{tail}},
1416
+ '\1\3 \2\4',
1417
+ )
1418
+
1419
+ # A space-year after a slash-stage ("…P26511.2/FDIS 2018" — no rule
1420
+ # reads "/FDIS 2018"): → "…FDIS P26511.2-2018" (dash-year clause).
1421
+ cleaned = cleaned.sub(
1422
+ %r{\A#{pubs}#{num}/#{stage} (\d{4})\b},
1423
+ '\1\3 \2-\4',
1424
+ )
1425
+
1426
+ # Stage separated from the number by a SPACE ("IEC/IEEE P63113 CD4,
1427
+ # April 2019"): → "IEC/IEEE CD4 P63113, April 2019". The tail guard
1428
+ # keeps "…FDIS March 2019" (space-month-year, readable after the
1429
+ # swap) while leaving prose suffixes alone.
1430
+ cleaned = cleaned.sub(
1431
+ %r{\A#{pubs}#{num} #{stage}#{tail}},
1432
+ '\1\3 \2\4',
1433
+ )
1434
+
1435
+ # The same space-stage with a space-DRAFT chained after it
1436
+ # ("IEC/IEEE P60980-344 CDV D1, June 2019"): → stage-first with a
1437
+ # slash draft ("…CDV P60980-344/D1, June 2019").
1438
+ cleaned = cleaned.sub(
1439
+ %r{\A#{pubs}#{num} #{stage} (D\d+)\b},
1440
+ '\1\3 \2/\4',
1441
+ )
1442
+
1443
+ # Slash-stage carrying a dash-date ("…P15288/CD2-2013-09 …" — the
1444
+ # dash-year tail is what the joint grammar reads after the number, so
1445
+ # this cannot steal the plain "/FDIS, June 2021" spellings
1446
+ # ieee_p_identifier finishes): → "…CD2 P15288-2013-09 …". The date
1447
+ # must be a plausible year — a 4-digit monthcode (YYMM, e.g.
1448
+ # "…P24748-4/DIS-1404" = April 2014) is a DRAFT designator, not a
1449
+ # dash-date, and rewriting it onto the number breaks the parse.
1450
+ cleaned = cleaned.sub(
1451
+ %r{\A#{pubs}#{num}/ ?#{stage}(-(?:19|20)\d\d(?:-\d\d)?)},
1452
+ '\1\3 \2\4',
1453
+ )
1454
+
1455
+ # Draft separated from a stage-first number by a SPACE:
1456
+ # "…DIS P11073-10418 D13, January 2011" → "…DIS P11073-10418/D13, …"
1457
+ cleaned = cleaned.sub(
1458
+ %r{\A#{pubs}#{stage} #{num}/? ?(D\d+)\b},
1459
+ '\1\2 \3/\4',
1460
+ )
1461
+
1462
+ # NOT rewritten: a month glued to the stage with a dash/underscore
1463
+ # ("…/FDIS_Dec 2012", "…/FDIS-Dec 2015"). Even routed onto the joint
1464
+ # grammar, the joint render of a month+draft is itself not yet
1465
+ # idempotent, and the bare-IEEE fdraft path drops the stage and the
1466
+ # date from to_hash — rewriting would trade a visible parse failure
1467
+ # for silent garbage. Deferred with the pubid#216 semantic residue.
1468
+ cleaned
1469
+ end
1470
+
1188
1471
  def self.parse(string)
1189
1472
  # Strip .pdf extension if present (Pattern 3: File Extensions)
1190
1473
  cleaned = string.sub(/\.pdf$/i, "")
@@ -1265,6 +1548,11 @@ module Pubid
1265
1548
  /\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec)(\d{4})\b/, '\1 \2'
1266
1549
  )
1267
1550
 
1551
+ # Rewrite the joint ISO-stage glued spellings (pubid#216) AFTER the
1552
+ # month unglue above, so "Dec2015" is already spaced when the
1553
+ # dash-month rewrite looks for it.
1554
+ cleaned = normalize_joint_stage_spellings(cleaned)
1555
+
1268
1556
  # NEW: Convert IEC/IEEE space-separated to semicolon format
1269
1557
  # Pattern: "IEC 61523-3 First edition 2004-09; IEEE 1497" → already semicolon
1270
1558
  # Pattern: "IEC 62539 First Edition 2007-07 IEEE 930" → needs semicolon
@@ -0,0 +1,46 @@
1
+ # frozen_string: true
2
+
3
+ module Pubid
4
+ module Ieee
5
+ # Renders the IEEE PROJECT DESIGNATOR — the short form IEEE itself uses
6
+ # for draft documents ("P1201/D0.3", "P10000/D1.2"): the project marker,
7
+ # the number with its parts, and the draft. No publisher, no type word,
8
+ # no date (pubid#18: metanorma-ieee builds these for drafts and had to
9
+ # post-process the full rendering).
10
+ class ProjectRenderer
11
+ def initialize(id)
12
+ @id = id
13
+ end
14
+
15
+ def render(context: nil, **_opts)
16
+ id = @id
17
+ body = if id.respond_to?(:code_obj) && id.code_obj
18
+ id.code_obj.to_s
19
+ else
20
+ id.number.to_s
21
+ end
22
+ body = "P#{body}" if project?(id) && !body.start_with?("P")
23
+
24
+ draft = id.draft_obj if id.respond_to?(:draft_obj)
25
+ body += "/D#{draft.version}" if draft&.version
26
+
27
+ body
28
+ end
29
+
30
+ private
31
+
32
+ # A project identifier carries its marker in one of three places
33
+ # depending on which grammar path built it: the code prefix (the bare
34
+ # "P1201/D0.3" spelling), the "P" type code ("IEEE P802.16/D-3"), or a
35
+ # typed stage with project_status (the D1-D6 draft stages).
36
+ def project?(id)
37
+ return true if id.code_obj&.prefix.to_s == "P"
38
+ return true if id.type.to_s == "P"
39
+
40
+ id.respond_to?(:typed_stage) &&
41
+ id.typed_stage&.respond_to?(:project_status) &&
42
+ id.typed_stage.project_status
43
+ end
44
+ end
45
+ end
46
+ end