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
@@ -33,49 +33,43 @@ module Pubid
33
33
  # carried in the structured form and the roman volume in citations.
34
34
  BASE_YEAR_OFFSET = 1959
35
35
 
36
- # Zero-padded issue number within the year ("01".."04", plus "07"/"10"
37
- # for the online-bulletin series). Always 2 digits.
38
- attribute :issue, :string
36
+ # The issue: the zero-padded number within the volume ("01".."04",
37
+ # plus "07"/"10" for the online-bulletin series). Always 2 digits. The
38
+ # year is the volume and the issue is the number, as in the citation
39
+ # form "LXVII(2)".
40
+ #
41
+ # Declared as `number`, so it is the relaton index key
42
+ # (`id.root.number.to_s`) and every index row with an issue shows a
43
+ # `number`. The key puts the same issue number of all volumes in one
44
+ # bucket; the MR slug keeps each article distinct.
45
+ #
46
+ # Declared on this LEAF, which has no subclasses. It redefines the
47
+ # `Components::Code number` of ::Pubid::Identifier as a :string, and a
48
+ # redefinition on a class that other classes inherit is the
49
+ # multi-flavor determinism landmine (see Identifiers::CodeNumber).
50
+ #
51
+ # nil for a volume ("OIML Bulletin 1960") and for the bare periodical
52
+ # reference "OIML Bulletin", which name no issue.
53
+ attribute :number, :string
39
54
  # Zero-padded sequence within the issue ("00" = editorial, "01"+ for
40
55
  # articles). Always 2 digits.
41
56
  attribute :sequence, :string
42
57
 
43
58
  key_value do
44
- map "issue", to: :issue
59
+ map "number", to: :number
45
60
  map "sequence", to: :sequence
46
61
  end
47
62
 
48
- # relaton-index keys on `id.root.number.to_s`. Bulletin is the one OIML
49
- # leaf with no code (it does not include Identifiers::CodeNumber), so
50
- # it derives the key from the year it already stores: every issue and
51
- # article of a volume clusters into one bucket, with per-article
52
- # distinctness coming from the MR slug instead. That is the BIPM
53
- # Metrologia-volume precedent recorded in CLAUDE.md.
54
- #
55
- # A DERIVED READER, deliberately not an attribute: storing it would
56
- # duplicate `year` in every row, and the key_value block above maps no
57
- # "number", so nothing is added to the wire format. Safe as a plain
58
- # method because Bulletin declares no `number` attribute of its own
59
- # (the ITU itu/identifiers/base.rb precedent).
60
- #
61
- # nil for the bare periodical reference "OIML Bulletin", which names no
62
- # year — a deliberate gap, pinned in spec/pubid/oiml/root_number_spec.rb.
63
- def number
64
- date&.year&.to_s
65
- end
66
-
67
- # The index key above is deliberately coarse — a whole volume in one
68
- # bucket — which is only defensible because the MR slug stays
69
- # per-article. The inherited hook reads through `code`, which Bulletin
70
- # does not have, so it would return nil and collapse every article of a
71
- # year onto `oiml.bulletin.<year>`; `to_slug` is an output FILENAME, so
72
- # that is an overwrite. Emit the rest of the locator instead.
63
+ # The inherited hook reads through `code`, which Bulletin does not
64
+ # have, so it would return nil and collapse every article of a year
65
+ # onto `oiml.bulletin.<year>`; `to_slug` is an output FILENAME, so that
66
+ # is an overwrite. Emit the rest of the locator instead.
73
67
  #
74
68
  # The year is deliberately NOT included: Renderers::MrString already
75
69
  # gives it its own segment (`mr_year`), so repeating it here would
76
70
  # render `oiml.bulletin.1960-03-01.1960`.
77
71
  def mr_number_with_part
78
- segments = [issue, sequence].compact
72
+ segments = [number, sequence].compact
79
73
  return nil if segments.empty?
80
74
 
81
75
  segments.join("-")
@@ -96,18 +90,18 @@ module Pubid
96
90
  # Volume as a roman-numeral string ("LXVII"), derived from the year.
97
91
  # nil when the year is absent.
98
92
  def volume_roman
99
- number = volume_arabic&.to_i
100
- return nil unless number&.positive?
93
+ volume = volume_arabic&.to_i
94
+ return nil unless volume&.positive?
101
95
 
102
- self.class.to_roman(number)
96
+ self.class.to_roman(volume)
103
97
  end
104
98
 
105
99
  # 8-digit oiml.org article id ("20260211"). Composed by concatenating
106
- # year + issue + sequence. nil unless all three are present.
100
+ # year + issue (number) + sequence. nil unless all three are present.
107
101
  def article_id
108
- return nil unless date&.year && issue && sequence
102
+ return nil unless date&.year && number && sequence
109
103
 
110
- "#{date.year}#{issue}#{sequence}"
104
+ "#{date.year}#{number}#{sequence}"
111
105
  end
112
106
 
113
107
  class << self
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Oiml
5
+ module Identifiers
6
+ # OIML-CS certification-system documents ("OIML-CS PD-05 Edition 6").
7
+ # The number keeps its printed zero padding ("05"); family is the
8
+ # PD/OD/CID document class; the print states "Edition N" instead of a
9
+ # year, and may carry a parenthesized trailing amendment.
10
+ class CertificationSystem < SingleIdentifier
11
+ attribute :number, :string
12
+ attribute :family, :string
13
+ attribute :amendment, :string
14
+ # "PD 05" prints space-separated where "PD-05" is dash-joined.
15
+ attribute :space_separator, :boolean, default: false
16
+
17
+ key_value do
18
+ map "number", to: :number
19
+ map "family", to: :family
20
+ map "amendment", to: :amendment
21
+ map "space_separator", to: :space_separator
22
+ end
23
+
24
+ def self.subset_ignored_attributes
25
+ super + [:space_separator]
26
+ end
27
+
28
+ def type_string
29
+ "CS"
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -28,12 +28,13 @@ module Pubid
28
28
  #
29
29
  # Bulletin deliberately does NOT include this module. It carries no code
30
30
  # at all (its locator is the year/issue/sequence tuple), so it would only
31
- # gain a permanently-nil `number` and a `number` key that never appears.
32
- # It derives its index key from the year instead; see bulletin.rb.
31
+ # gain permanently-nil part/subpart/suffix columns. It declares its own
32
+ # `number` (the issue) instead; see bulletin.rb.
33
33
  module CodeNumber
34
34
  def self.included(base)
35
35
  install_attributes(base)
36
36
  install_mappings(base)
37
+ install_subset_rules(base)
37
38
  end
38
39
 
39
40
  def self.install_attributes(base)
@@ -53,6 +54,13 @@ module Pubid
53
54
  base.attribute :space_suffix, :boolean, default: -> { false }
54
55
  end
55
56
 
57
+ # A nil `part`, `subpart` or `suffix` means the document has none of
58
+ # them: `OIML R 138` is not `OIML R 138-Amend`, which is its
59
+ # amendment. Declared on each leaf, beside the columns it governs.
60
+ def self.install_subset_rules(base)
61
+ base.subset_strict :part, :subpart, :suffix
62
+ end
63
+
56
64
  # Merged by lutaml with the block SingleIdentifier declares, which
57
65
  # carries publisher/year/edition/stage/iteration.
58
66
  def self.install_mappings(base)
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Oiml
5
+ module Identifiers
6
+ # OIML sometimes co-publishes a document jointly with another SDO
7
+ # (ISO confirmed so far, per pubid issue #437). The printed reference
8
+ # carries both identifiers, joined by a bare "|":
9
+ #
10
+ # ISO 4064-1:2024|OIML R 49-1:2024
11
+ #
12
+ # A sibling of SingleIdentifier/SupplementIdentifier — like
13
+ # SupplementIdentifier, it inherits NOTHING from SingleIdentifier and
14
+ # must delegate every identity-bearing reader (code/type/stage/
15
+ # iteration/publisher, the mr_* MR-slug hooks, #root) itself. Without
16
+ # the mr_* delegations, #to_mr_string would silently come out "" (the
17
+ # same filename-collision gap already documented for
18
+ # Amendment/Errata/Annex in lib/pubid/oiml/CLAUDE.md) — this class closes
19
+ # that gap from day one instead of repeating it.
20
+ #
21
+ # `first`/`second` hold the two sides in their original left-to-right
22
+ # print order, each a real, independently-typed `::Pubid::Identifier`
23
+ # (never an attr_accessor — see IEEE's CsaDualPublished for the bug
24
+ # that pattern causes: to_hash/from_hash/#exclude silently drop it).
25
+ # Either side may be OIML; `#oiml_identifier`/`#external_identifier`
26
+ # pick the OIML-typed one out of the pair regardless of order.
27
+ class DualPublished < Identifier
28
+ attribute :first, ::Pubid::Identifier, polymorphic: true
29
+ attribute :second, ::Pubid::Identifier, polymorphic: true
30
+
31
+ # Declared locally (rather than left to the inherited default)
32
+ # because Oiml::Identifier's shared key_value block maps "language"
33
+ # and "parsed_format" to real attributes — without a local
34
+ # declaration here, to_hash/from_hash would call a getter/setter
35
+ # this class never defines. Mirrors SingleIdentifier/
36
+ # SupplementIdentifier, which do the same for the same reason.
37
+ attribute :language, :string
38
+ attribute :parsed_format, :string, default: -> { "short" }
39
+
40
+ key_value do
41
+ map "first", with: { to: :first_to_kv, from: :first_from_kv }
42
+ map "second", with: { to: :second_to_kv, from: :second_from_kv }
43
+ end
44
+
45
+ # Either side may belong to any flavor, so (de)serialization goes
46
+ # through the generic top-level Pubid.from_hash/#to_hash, not the
47
+ # OIML-scoped Identifier.from_hash (which only resolves OIML types).
48
+ def first_to_kv(model, doc)
49
+ value = model.first
50
+ return unless value
51
+
52
+ doc.add_child(
53
+ Lutaml::KeyValue::DataModel::Element.new("first", value.to_hash),
54
+ )
55
+ end
56
+
57
+ def first_from_kv(model, value)
58
+ model.first = ::Pubid.from_hash(value) if value
59
+ end
60
+
61
+ def second_to_kv(model, doc)
62
+ value = model.second
63
+ return unless value
64
+
65
+ doc.add_child(
66
+ Lutaml::KeyValue::DataModel::Element.new("second", value.to_hash),
67
+ )
68
+ end
69
+
70
+ def second_from_kv(model, value)
71
+ model.second = ::Pubid.from_hash(value) if value
72
+ end
73
+
74
+ # The member that belongs to THIS flavor, wherever print order put
75
+ # it. relaton-index sorts on this side's root.number.
76
+ def oiml_identifier
77
+ [first, second].find { |member| member.is_a?(Oiml::Identifier) }
78
+ end
79
+
80
+ # The other SDO's identifier for the same document.
81
+ def external_identifier
82
+ [first, second].find { |member| !member.is_a?(Oiml::Identifier) }
83
+ end
84
+
85
+ def root
86
+ oiml_identifier ? oiml_identifier.root : self
87
+ end
88
+
89
+ def code
90
+ oiml_identifier&.code
91
+ end
92
+
93
+ def type
94
+ oiml_identifier&.type
95
+ end
96
+
97
+ def stage
98
+ oiml_identifier&.stage
99
+ end
100
+
101
+ def iteration
102
+ oiml_identifier&.iteration
103
+ end
104
+
105
+ def publisher
106
+ oiml_identifier&.publisher
107
+ end
108
+
109
+ def mr_publisher
110
+ oiml_identifier&.mr_publisher
111
+ end
112
+
113
+ def mr_type
114
+ oiml_identifier&.mr_type
115
+ end
116
+
117
+ def mr_number_with_part
118
+ oiml_identifier&.mr_number_with_part
119
+ end
120
+
121
+ def mr_year
122
+ oiml_identifier&.mr_year
123
+ end
124
+
125
+ # Splits `identifier` on "|" and, if it looks like an OIML dual-
126
+ # published reference (exactly two non-empty sides, exactly one of
127
+ # which starts with an OIML prefix), parses each side through its
128
+ # own flavor and returns the wrapper. Returns nil for anything else,
129
+ # so the caller (Pubid::Oiml.parse) falls through to the ordinary
130
+ # grammar, which raises the standard Parslet::ParseFailed for a
131
+ # string containing "|" (the grammar defines no rule for it).
132
+ def self.build(identifier)
133
+ sides = split_sides(identifier)
134
+ return nil unless sides
135
+
136
+ left, right = parse_sides(*sides)
137
+ return nil unless left && right
138
+
139
+ new(first: left, second: right)
140
+ end
141
+
142
+ # Two non-empty, stripped sides, or nil if `identifier` isn't
143
+ # shaped like "A|B".
144
+ def self.split_sides(identifier)
145
+ parts = identifier.split("|").map(&:strip)
146
+ return nil unless parts.length == 2
147
+ return nil if parts.any?(&:empty?)
148
+
149
+ parts
150
+ end
151
+ private_class_method :split_sides
152
+
153
+ # Parses each side through its own flavor, provided exactly one side
154
+ # looks like OIML (by prefix, not position). [nil, nil] otherwise.
155
+ def self.parse_sides(left, right)
156
+ left_is_oiml = Oiml::PREFIXES.any? { |p| left.start_with?(p) }
157
+ right_is_oiml = Oiml::PREFIXES.any? { |p| right.start_with?(p) }
158
+ return [nil, nil] if left_is_oiml == right_is_oiml
159
+
160
+ [parse_side(left, oiml: left_is_oiml),
161
+ parse_side(right, oiml: right_is_oiml)]
162
+ end
163
+ private_class_method :parse_sides
164
+
165
+ def self.parse_side(str, oiml:)
166
+ oiml ? Oiml.parse(str) : ::Pubid.parse(str)
167
+ rescue Parslet::ParseFailed, Pubid::Errors::InvalidInputError
168
+ nil
169
+ end
170
+ private_class_method :parse_side
171
+ end
172
+ end
173
+ end
174
+ end
@@ -7,8 +7,10 @@ module Pubid
7
7
  autoload :Annex, "#{__dir__}/identifiers/annex"
8
8
  autoload :BasicPublication, "#{__dir__}/identifiers/basic_publication"
9
9
  autoload :Bulletin, "#{__dir__}/identifiers/bulletin"
10
+ autoload :CertificationSystem, "#{__dir__}/identifiers/certification_system"
10
11
  autoload :CodeNumber, "#{__dir__}/identifiers/code_number"
11
12
  autoload :Document, "#{__dir__}/identifiers/document"
13
+ autoload :DualPublished, "#{__dir__}/identifiers/dual_published"
12
14
  autoload :Errata, "#{__dir__}/identifiers/errata"
13
15
  autoload :ExpertReport, "#{__dir__}/identifiers/expert_report"
14
16
  autoload :Guide, "#{__dir__}/identifiers/guide"
@@ -20,14 +20,17 @@ module Pubid
20
20
  rule(:identifier) do
21
21
  amendment_identifier | amendment_short | annex_letter_identifier |
22
22
  annex_identifier | plus_supplement_identifier |
23
- trailing_supplement_identifier | bulletin_identifier | base
23
+ trailing_supplement_identifier | cs_identifier |
24
+ bulletin_identifier | base
24
25
  end
25
26
 
26
27
  # Publisher - always "OIML"
27
28
  rule(:publisher) { str("OIML").as(:publisher) >> space }
28
29
 
29
- # Document type - single letter
30
- rule(:doc_type) { match("[BDEGRSVX]").as(:type) >> space }
30
+ # Document type - single letter. Strict family set: OIML publishes
31
+ # R D B G E V S documents (the estate grammar's family letters);
32
+ # any other letter is a rejection, not a flavor.
33
+ rule(:doc_type) { match("[BDEGRSV]").as(:type) >> space }
31
34
 
32
35
  # Bulletin locator — structured form. Year optionally followed by
33
36
  # 2-digit issue and 2-digit sequence:
@@ -122,9 +125,14 @@ module Pubid
122
125
  ).as(:edition_format) # Wrap entire match to capture that Edition was used
123
126
  end
124
127
 
125
- # Date - year after colon OR edition portion (with optional space before year)
128
+ # Date - year after colon, the VIM's paren-year form ("OIML V 2-200
129
+ # (2000)" — space + (YYYY) instead of :YYYY, the shape #342 flags as
130
+ # the one real-world spelling that did not parse), OR edition portion
131
+ # (with optional space before year)
126
132
  rule(:date) do
127
- edition_portion | (colon >> space.maybe >> year_digits.as(:year))
133
+ edition_portion |
134
+ (space.maybe >> colon >> space.maybe >> year_digits.as(:year)) |
135
+ (space.maybe >> lparen >> year_digits.as(:year) >> rparen)
128
136
  end
129
137
 
130
138
  # Draft stage - WD or CD with optional iteration
@@ -162,10 +170,22 @@ module Pubid
162
170
  match("[a-z]").repeat(2, 2) # Two letters: en, fr, etc.
163
171
  end
164
172
 
173
+ # Full-word language markers as OIML prints them ("(Fra)", "(Eng)"),
174
+ # three or more letters, any case. Kept verbatim on the identifier;
175
+ # the URN lowercases.
176
+ rule(:lang_word) do
177
+ match("[A-Za-z]").repeat(3)
178
+ end
179
+
165
180
  rule(:language_code) do
166
181
  (
167
182
  (lang_single >> slash >> lang_single) | # E/F
168
183
  lang_multi_oiml | # PO, PT, PE, SR
184
+ lang_word | # Fra, eng, rus — before
185
+ # the letter rules: a
186
+ # committed "F" of "(Fra)"
187
+ # or "fr" of "(fra)" would
188
+ # never fall through
169
189
  lang_single | # E, F, D, R, S, C, A, U, X
170
190
  lang_multi # en, fr
171
191
  ).as(:language)
@@ -211,9 +231,25 @@ module Pubid
211
231
  rule(:trailing_supplement_identifier) do
212
232
  base_without_language.as(:base) >>
213
233
  space >> (str("Amendment") | str("Errata")).as(:trailing_marker) >>
234
+ (space >> digits.as(:number)).maybe >>
214
235
  language_portion.maybe.as(:language)
215
236
  end
216
237
 
238
+ # OIML-CS certification-system documents. Two head spellings
239
+ # ("OIML-CS" / "OIML CS") and two family-number separators
240
+ # ("PD-05" / "PD 05"), an "Edition N" instead of a year, and an
241
+ # optional parenthesized trailing amendment - "(Amendment 1)".
242
+ rule(:cs_identifier) do
243
+ str("OIML").as(:publisher) >>
244
+ (dash >> str("CS") | space >> str("CS")).as(:cs_series) >> space >>
245
+ (str("PD") | str("OD") | str("CID")).as(:cs_family) >>
246
+ (dash | space).as(:cs_separator) >>
247
+ digits.as(:number) >>
248
+ space >> str("Edition") >> space >> digits.as(:edition) >>
249
+ (space >> lparen >> str("Amendment") >> space >>
250
+ digits.as(:cs_amendment) >> rparen).maybe
251
+ end
252
+
217
253
  # Plus-joined supplement - "BASE:YEAR+Supplement:YEAR" form where both
218
254
  # the base and the supplement carry their own year. Used for amendments
219
255
  # and errata to dated bases (e.g. "OIML B 10:2011+Amendment:2012").
@@ -18,10 +18,14 @@ module Pubid
18
18
  @context = context
19
19
 
20
20
  case id
21
+ when Identifiers::DualPublished
22
+ render_dual_published(id)
21
23
  when Identifiers::Annex
22
24
  render_annex(id)
23
25
  when Identifiers::Bulletin
24
26
  render_bulletin(id)
27
+ when Identifiers::CertificationSystem
28
+ render_cs(id)
25
29
  when SupplementIdentifier
26
30
  render_supplement(id)
27
31
  when SingleIdentifier
@@ -33,6 +37,16 @@ module Pubid
33
37
 
34
38
  private
35
39
 
40
+ # Certification-system document: "OIML-CS PD-05 Edition 6 (Amendment 1)".
41
+ # The family-number separator keeps the parsed spelling; OIML CS
42
+ # documents state an edition instead of a year.
43
+ def render_cs(id)
44
+ result = "#{id.publisher}-CS #{id.family}" \
45
+ "#{id.space_separator ? ' ' : '-'}#{id.number} Edition #{id.edition}"
46
+ result += " (Amendment #{id.amendment})" if id.amendment
47
+ result
48
+ end
49
+
36
50
  # Render the Bulletin in the requested or parsed form. Default is the
37
51
  # structured "YYYY-II-SS" form (the dataset's primary docid). The
38
52
  # citation form ("LXVII(2) 20260211") is emitted when the user asks
@@ -65,7 +79,7 @@ module Pubid
65
79
  result = "#{id.publisher} Bulletin"
66
80
  if id.date&.year
67
81
  result += " #{id.date.year}"
68
- result += "-#{id.issue}" if id.issue
82
+ result += "-#{id.number}" if id.number
69
83
  result += "-#{id.sequence}" if id.sequence
70
84
  end
71
85
  result += " (#{id.language})" if id.language
@@ -77,9 +91,9 @@ module Pubid
77
91
  # structured since OIML's citation format doesn't define volume-only
78
92
  # or issue-only variants.
79
93
  def render_bulletin_citation(id)
80
- return render_bulletin_structured(id) unless id.date&.year && id.issue && id.sequence
94
+ return render_bulletin_structured(id) unless id.date&.year && id.number && id.sequence
81
95
 
82
- "#{id.publisher} Bulletin #{id.volume_roman}(#{id.issue.to_i}) #{id.article_id}"
96
+ "#{id.publisher} Bulletin #{id.volume_roman}(#{id.number.to_i}) #{id.article_id}"
83
97
  end
84
98
 
85
99
  def effective_format(id)
@@ -93,6 +107,12 @@ module Pubid
93
107
  str.sub(/\s*\([^)]+\)\s*$/, "").strip
94
108
  end
95
109
 
110
+ # Bare "|", original left-to-right print order, e.g.
111
+ # "ISO 4064-1:2024|OIML R 49-1:2024".
112
+ def render_dual_published(id)
113
+ "#{id.first}|#{id.second}"
114
+ end
115
+
96
116
  def render_single(id)
97
117
  format = effective_format(id)
98
118
 
@@ -147,10 +167,12 @@ module Pubid
147
167
 
148
168
  # Trailing-word shorthand: "BASE Amendment" / "BASE Errata" with the
149
169
  # publication year kept on the base identifier. The word comes from the
150
- # concrete supplement class.
170
+ # concrete supplement class; an ordinal, when printed ("Amendment 1"),
171
+ # follows it.
151
172
  if id.trailing
152
173
  base_str = strip_language(id.base.to_s)
153
174
  result = "#{base_str} #{id.supplement_type}"
175
+ result += " #{id.number}" if id.number
154
176
  result += " (#{id.language})" if id.language
155
177
  return result
156
178
  end
@@ -14,6 +14,10 @@ module Pubid
14
14
  "short"
15
15
  } # Track parsed format
16
16
 
17
+ # A nil `language` means the document states none: `OIML R 126:2015
18
+ # Errata` is not `OIML R 126:2015 Errata (E)`, its English edition.
19
+ subset_strict :language
20
+
17
21
  # Serialization delta on top of Oiml::Identifier's shared block. The
18
22
  # `date` (year) component is flattened to a top-level key rather than a
19
23
  # nested hash, mirroring ISO (lib/pubid/iso/identifier.rb). `type` is
@@ -24,7 +28,7 @@ module Pubid
24
28
  # concrete leaf, because redefining the inherited `number` on this class
25
29
  # — which every leaf inherits — is the multi-flavor determinism landmine.
26
30
  # Keeping the mapping with the attribute is also what lets Bulletin, the
27
- # one leaf with no code, omit both.
31
+ # one leaf with no code, declare its own `number` (the issue) and map it.
28
32
  key_value do
29
33
  map "publisher", to: :publisher
30
34
  map "year", with: { to: :year_to_kv, from: :year_from_kv }
@@ -7,8 +7,14 @@ module Pubid
7
7
  # These wrap a base identifier like ISO amendments
8
8
  attribute :base, Oiml::Identifier, polymorphic: true
9
9
  attribute :year, :string
10
+ # Ordinal of the trailing-word form ("OIML R 138:2009 Amendment 1").
11
+ attribute :number, :string
10
12
  attribute :language, :string
11
13
 
14
+ # A nil `language` means the document states none, mirroring
15
+ # SingleIdentifier's rule.
16
+ subset_strict :language
17
+
12
18
  # Delegate the document code to the wrapped standard, mirroring
13
19
  # Pubid::Etsi::Identifiers::SupplementIdentifier#code.
14
20
  #
@@ -54,6 +60,7 @@ module Pubid
54
60
  map "base",
55
61
  with: { to: :base_to_kv, from: :base_from_kv }
56
62
  map "year", to: :year
63
+ map "number", to: :number
57
64
  map "trailing", to: :trailing
58
65
  map "joined", to: :joined
59
66
  end
@@ -27,6 +27,25 @@ module Pubid
27
27
  identifier.language&.to_s&.downcase
28
28
  end
29
29
 
30
+ # DualPublished declares no `date` attribute of its own (it lives on
31
+ # whichever side is OIML), so the shared Base#urn_year — which gates
32
+ # on `identifier.class.attributes.key?(:date)` — would silently drop
33
+ # the year. Read it through the OIML side instead.
34
+ def urn_year
35
+ if identifier.is_a?(Identifiers::DualPublished)
36
+ oiml_date = identifier.oiml_identifier&.date
37
+ return oiml_date&.year&.to_s
38
+ end
39
+
40
+ year = super
41
+ return year if year
42
+
43
+ # The trailing-word form keeps the publication year on the base
44
+ # ("R 138:2009 Amendment 1"); the supplement itself carries only the
45
+ # ordinal.
46
+ identifier.base&.date&.year&.to_s if identifier.is_a?(SupplementIdentifier)
47
+ end
48
+
30
49
  def generate
31
50
  # Bulletin issues carry no code; the (year, issue, sequence) tuple
32
51
  # is the locator. URNs are canonical regardless of how the input was
@@ -39,6 +58,15 @@ module Pubid
39
58
  return parts.join(":")
40
59
  end
41
60
 
61
+ # Certification-system documents: the family-number pair is the
62
+ # document identity ("cs:pd-05"); the printed edition and trailing
63
+ # amendment are print states, not URN segments.
64
+ if identifier.is_a?(Identifiers::CertificationSystem)
65
+ parts = ["urn", "oiml", "cs",
66
+ "#{identifier.family.downcase}-#{identifier.number}"]
67
+ return parts.join(":")
68
+ end
69
+
42
70
  parts = ["urn", "oiml"]
43
71
  parts << urn_type
44
72
  parts << urn_number if urn_number
@@ -64,12 +92,12 @@ module Pubid
64
92
  end
65
93
 
66
94
  # Structured locator "YYYY-II-SS" (or "YYYY-II" / "YYYY") for a Bulletin.
67
- # Built directly from the date year and the issue/sequence attributes so
68
- # the URN is identical regardless of whether the input was the structured
69
- # or citation form.
95
+ # Built directly from the date year and the number (the issue) and
96
+ # sequence attributes so the URN is identical regardless of whether the
97
+ # input was the structured or citation form.
70
98
  def bulletin_locator
71
99
  locator = identifier.date.year.to_s
72
- locator += "-#{identifier.issue}" if identifier.issue
100
+ locator += "-#{identifier.number}" if identifier.number
73
101
  locator += "-#{identifier.sequence}" if identifier.sequence
74
102
  locator
75
103
  end
data/lib/pubid/oiml.rb CHANGED
@@ -29,6 +29,11 @@ module Pubid
29
29
  raise Pubid::Errors::InvalidInputError, Pubid::INPUT_TOO_LONG_MESSAGE
30
30
  end
31
31
 
32
+ if identifier.include?("|")
33
+ dual = Identifiers::DualPublished.build(identifier)
34
+ return dual if dual
35
+ end
36
+
32
37
  parser = Parser.new
33
38
  builder = Builder.new
34
39