pubid 1.15.21 → 2.0.0.alpha.10

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 (863) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.adoc +2044 -53
  4. data/archived-gems/pubid-ccsds/update_codes.yaml +1 -0
  5. data/archived-gems/pubid-iec/stages.yaml +129 -0
  6. data/archived-gems/pubid-iec/update_codes.yaml +67 -0
  7. data/archived-gems/pubid-ieee/update_codes.yaml +104 -0
  8. data/archived-gems/pubid-iso/stages.yaml +106 -0
  9. data/archived-gems/pubid-iso/update_codes.yaml +4 -0
  10. data/archived-gems/pubid-itu/i18n.yaml +13 -0
  11. data/archived-gems/pubid-itu/series.yaml +42 -0
  12. data/archived-gems/pubid-nist/publishers.yaml +6 -0
  13. data/archived-gems/pubid-nist/series.yaml +121 -0
  14. data/archived-gems/pubid-nist/stages.yaml +16 -0
  15. data/archived-gems/pubid-nist/update_codes.yaml +93 -0
  16. data/archived-gems/pubid-plateau/update_codes.yaml +6 -0
  17. data/data/bipm/update_codes.yaml +9 -0
  18. data/data/ccsds/update_codes.yaml +1 -0
  19. data/data/iec/update_codes.yaml +70 -0
  20. data/data/ieee/update_codes.yaml +210 -0
  21. data/data/iso/update_codes.yaml +21 -0
  22. data/data/nist/series.yaml +155 -0
  23. data/data/nist/update_codes.yaml +122 -0
  24. data/data/plateau/update_codes.yaml +6 -0
  25. data/lib/pubid/adobe/builder.rb +52 -0
  26. data/lib/pubid/adobe/identifier.rb +47 -0
  27. data/lib/pubid/adobe/identifiers/publication.rb +31 -0
  28. data/lib/pubid/adobe/identifiers/tech_note.rb +32 -0
  29. data/lib/pubid/adobe/identifiers.rb +10 -0
  30. data/lib/pubid/adobe/parser.rb +136 -0
  31. data/lib/pubid/adobe/renderer.rb +39 -0
  32. data/lib/pubid/adobe/urn_generator.rb +42 -0
  33. data/lib/pubid/adobe/urn_parser.rb +69 -0
  34. data/lib/pubid/adobe.rb +60 -0
  35. data/lib/pubid/amca/builder.rb +176 -0
  36. data/lib/pubid/amca/identifier.rb +4 -0
  37. data/lib/pubid/amca/identifiers/base.rb +92 -0
  38. data/lib/pubid/amca/identifiers/interpretation.rb +38 -0
  39. data/lib/pubid/amca/identifiers/publication.rb +34 -0
  40. data/lib/pubid/amca/identifiers/standard.rb +20 -0
  41. data/lib/pubid/amca/identifiers.rb +11 -0
  42. data/lib/pubid/amca/parser.rb +153 -0
  43. data/lib/pubid/amca/renderer.rb +82 -0
  44. data/lib/pubid/amca/single_identifier.rb +10 -0
  45. data/lib/pubid/amca/urn_generator.rb +51 -0
  46. data/lib/pubid/amca/urn_parser.rb +28 -0
  47. data/lib/pubid/amca.rb +73 -0
  48. data/lib/pubid/ansi/builder.rb +54 -0
  49. data/lib/pubid/ansi/identifier.rb +31 -0
  50. data/lib/pubid/ansi/identifiers/american_national_standard.rb +13 -0
  51. data/lib/pubid/ansi/identifiers/standard.rb +15 -0
  52. data/lib/pubid/ansi/identifiers.rb +11 -0
  53. data/lib/pubid/ansi/parser.rb +91 -0
  54. data/lib/pubid/ansi/renderer.rb +53 -0
  55. data/lib/pubid/ansi/single_identifier.rb +16 -0
  56. data/lib/pubid/ansi/urn_generator.rb +41 -0
  57. data/lib/pubid/ansi/urn_parser.rb +23 -0
  58. data/lib/pubid/ansi.rb +68 -0
  59. data/lib/pubid/api/builder.rb +58 -0
  60. data/lib/pubid/api/components/code.rb +9 -0
  61. data/lib/pubid/api/identifier.rb +42 -0
  62. data/lib/pubid/api/identifiers/base.rb +12 -0
  63. data/lib/pubid/api/identifiers/bulletin.rb +13 -0
  64. data/lib/pubid/api/identifiers/continuous_operations_standard.rb +13 -0
  65. data/lib/pubid/api/identifiers/mpms.rb +26 -0
  66. data/lib/pubid/api/identifiers/publication.rb +13 -0
  67. data/lib/pubid/api/identifiers/recommended_practice.rb +13 -0
  68. data/lib/pubid/api/identifiers/specification.rb +13 -0
  69. data/lib/pubid/api/identifiers/standard.rb +13 -0
  70. data/lib/pubid/api/identifiers/technical_report.rb +13 -0
  71. data/lib/pubid/api/identifiers/typeless_standard.rb +11 -0
  72. data/lib/pubid/api/identifiers.rb +18 -0
  73. data/lib/pubid/api/parser.rb +140 -0
  74. data/lib/pubid/api/renderer.rb +98 -0
  75. data/lib/pubid/api/single_identifier.rb +24 -0
  76. data/lib/pubid/api/urn_generator.rb +23 -0
  77. data/lib/pubid/api/urn_parser.rb +35 -0
  78. data/lib/pubid/api.rb +68 -0
  79. data/lib/pubid/ashrae/builder.rb +519 -0
  80. data/lib/pubid/ashrae/identifier.rb +8 -0
  81. data/lib/pubid/ashrae/identifiers/addenda_package.rb +42 -0
  82. data/lib/pubid/ashrae/identifiers/addendum.rb +44 -0
  83. data/lib/pubid/ashrae/identifiers/base.rb +119 -0
  84. data/lib/pubid/ashrae/identifiers/combined_addenda.rb +43 -0
  85. data/lib/pubid/ashrae/identifiers/errata.rb +39 -0
  86. data/lib/pubid/ashrae/identifiers/guideline.rb +41 -0
  87. data/lib/pubid/ashrae/identifiers/interpretation.rb +45 -0
  88. data/lib/pubid/ashrae/identifiers/standard.rb +41 -0
  89. data/lib/pubid/ashrae/identifiers.rb +15 -0
  90. data/lib/pubid/ashrae/parser.rb +748 -0
  91. data/lib/pubid/ashrae/renderer.rb +117 -0
  92. data/lib/pubid/ashrae/single_identifier.rb +10 -0
  93. data/lib/pubid/ashrae/supplement_identifier.rb +38 -0
  94. data/lib/pubid/ashrae/urn_generator.rb +57 -0
  95. data/lib/pubid/ashrae/urn_parser.rb +27 -0
  96. data/lib/pubid/ashrae.rb +77 -0
  97. data/lib/pubid/asme/builder.rb +162 -0
  98. data/lib/pubid/asme/components/code.rb +26 -0
  99. data/lib/pubid/asme/identifier.rb +24 -0
  100. data/lib/pubid/asme/identifiers/base.rb +10 -0
  101. data/lib/pubid/asme/identifiers/standard.rb +96 -0
  102. data/lib/pubid/asme/identifiers.rb +10 -0
  103. data/lib/pubid/asme/parser.rb +308 -0
  104. data/lib/pubid/asme/renderer.rb +66 -0
  105. data/lib/pubid/asme/single_identifier.rb +38 -0
  106. data/lib/pubid/asme/urn_generator.rb +133 -0
  107. data/lib/pubid/asme/urn_parser.rb +31 -0
  108. data/lib/pubid/asme.rb +67 -0
  109. data/lib/pubid/astm/builder.rb +166 -0
  110. data/lib/pubid/astm/components/code.rb +42 -0
  111. data/lib/pubid/astm/components.rb +9 -0
  112. data/lib/pubid/astm/identifier.rb +24 -0
  113. data/lib/pubid/astm/identifiers/adjunct.rb +28 -0
  114. data/lib/pubid/astm/identifiers/base.rb +13 -0
  115. data/lib/pubid/astm/identifiers/code_number.rb +81 -0
  116. data/lib/pubid/astm/identifiers/data_series.rb +13 -0
  117. data/lib/pubid/astm/identifiers/iso_dual_published.rb +65 -0
  118. data/lib/pubid/astm/identifiers/manual.rb +23 -0
  119. data/lib/pubid/astm/identifiers/monograph.rb +13 -0
  120. data/lib/pubid/astm/identifiers/research_report.rb +21 -0
  121. data/lib/pubid/astm/identifiers/standard.rb +23 -0
  122. data/lib/pubid/astm/identifiers/technical_report.rb +12 -0
  123. data/lib/pubid/astm/identifiers/work_in_progress.rb +12 -0
  124. data/lib/pubid/astm/identifiers.rb +19 -0
  125. data/lib/pubid/astm/parser.rb +244 -0
  126. data/lib/pubid/astm/renderer.rb +172 -0
  127. data/lib/pubid/astm/single_identifier.rb +47 -0
  128. data/lib/pubid/astm/urn_generator.rb +101 -0
  129. data/lib/pubid/astm/urn_parser.rb +30 -0
  130. data/lib/pubid/astm.rb +55 -0
  131. data/lib/pubid/bipm/builder.rb +163 -0
  132. data/lib/pubid/bipm/identifier.rb +285 -0
  133. data/lib/pubid/bipm/identifiers/committee_document.rb +36 -0
  134. data/lib/pubid/bipm/identifiers/guide.rb +35 -0
  135. data/lib/pubid/bipm/identifiers/meeting.rb +53 -0
  136. data/lib/pubid/bipm/identifiers/mep.rb +33 -0
  137. data/lib/pubid/bipm/identifiers/metrologia_article.rb +48 -0
  138. data/lib/pubid/bipm/identifiers/si_brochure.rb +33 -0
  139. data/lib/pubid/bipm/identifiers.rb +15 -0
  140. data/lib/pubid/bipm/parser.rb +237 -0
  141. data/lib/pubid/bipm/renderer.rb +118 -0
  142. data/lib/pubid/bipm/urn_generator.rb +58 -0
  143. data/lib/pubid/bipm/urn_parser.rb +60 -0
  144. data/lib/pubid/bipm.rb +91 -0
  145. data/lib/pubid/bsi/builder.rb +1495 -0
  146. data/lib/pubid/bsi/components/code.rb +11 -0
  147. data/lib/pubid/bsi/components/date.rb +11 -0
  148. data/lib/pubid/bsi/components/publisher.rb +11 -0
  149. data/lib/pubid/bsi/components/type.rb +11 -0
  150. data/lib/pubid/bsi/identifier.rb +6 -0
  151. data/lib/pubid/bsi/identifiers/addendum_document.rb +30 -0
  152. data/lib/pubid/bsi/identifiers/adopted_european_norm.rb +67 -0
  153. data/lib/pubid/bsi/identifiers/adopted_international_standard.rb +51 -0
  154. data/lib/pubid/bsi/identifiers/aerospace_standard.rb +65 -0
  155. data/lib/pubid/bsi/identifiers/amendment.rb +48 -0
  156. data/lib/pubid/bsi/identifiers/british_industrial_practice.rb +21 -0
  157. data/lib/pubid/bsi/identifiers/british_standard.rb +34 -0
  158. data/lib/pubid/bsi/identifiers/bundled_identifier.rb +40 -0
  159. data/lib/pubid/bsi/identifiers/committee_document.rb +44 -0
  160. data/lib/pubid/bsi/identifiers/consolidated_identifier.rb +84 -0
  161. data/lib/pubid/bsi/identifiers/corrigendum.rb +44 -0
  162. data/lib/pubid/bsi/identifiers/detailed_specification.rb +33 -0
  163. data/lib/pubid/bsi/identifiers/disc.rb +27 -0
  164. data/lib/pubid/bsi/identifiers/draft_document.rb +27 -0
  165. data/lib/pubid/bsi/identifiers/electronic_book.rb +16 -0
  166. data/lib/pubid/bsi/identifiers/expert_commentary.rb +36 -0
  167. data/lib/pubid/bsi/identifiers/explanatory_supplement.rb +35 -0
  168. data/lib/pubid/bsi/identifiers/flex.rb +30 -0
  169. data/lib/pubid/bsi/identifiers/handbook.rb +23 -0
  170. data/lib/pubid/bsi/identifiers/index.rb +30 -0
  171. data/lib/pubid/bsi/identifiers/method.rb +35 -0
  172. data/lib/pubid/bsi/identifiers/national_annex.rb +48 -0
  173. data/lib/pubid/bsi/identifiers/practice_guide.rb +21 -0
  174. data/lib/pubid/bsi/identifiers/publicly_available_specification.rb +27 -0
  175. data/lib/pubid/bsi/identifiers/published_document.rb +27 -0
  176. data/lib/pubid/bsi/identifiers/section.rb +32 -0
  177. data/lib/pubid/bsi/identifiers/set.rb +40 -0
  178. data/lib/pubid/bsi/identifiers/standalone_amendment.rb +40 -0
  179. data/lib/pubid/bsi/identifiers/supplement_document.rb +29 -0
  180. data/lib/pubid/bsi/identifiers/supplementary_index.rb +35 -0
  181. data/lib/pubid/bsi/identifiers/technical_specification.rb +34 -0
  182. data/lib/pubid/bsi/identifiers/test_method.rb +35 -0
  183. data/lib/pubid/bsi/identifiers/value_added_publication.rb +37 -0
  184. data/lib/pubid/bsi/identifiers.rb +51 -0
  185. data/lib/pubid/bsi/model.rb +196 -0
  186. data/lib/pubid/bsi/parser.rb +673 -0
  187. data/lib/pubid/bsi/renderer.rb +1064 -0
  188. data/lib/pubid/bsi/single_identifier.rb +141 -0
  189. data/lib/pubid/bsi/urn_generator.rb +92 -0
  190. data/lib/pubid/bsi/urn_parser.rb +52 -0
  191. data/lib/pubid/bsi.rb +282 -0
  192. data/lib/pubid/builder/base.rb +187 -0
  193. data/lib/pubid/bundled_identifier.rb +134 -0
  194. data/lib/pubid/calconnect/builder.rb +38 -0
  195. data/lib/pubid/calconnect/identifier.rb +121 -0
  196. data/lib/pubid/calconnect/identifiers/standard.rb +29 -0
  197. data/lib/pubid/calconnect/identifiers.rb +9 -0
  198. data/lib/pubid/calconnect/parser.rb +54 -0
  199. data/lib/pubid/calconnect/renderer.rb +36 -0
  200. data/lib/pubid/calconnect/urn_generator.rb +33 -0
  201. data/lib/pubid/calconnect/urn_parser.rb +34 -0
  202. data/lib/pubid/calconnect.rb +82 -0
  203. data/lib/pubid/ccsds/builder.rb +57 -0
  204. data/lib/pubid/ccsds/identifier.rb +76 -0
  205. data/lib/pubid/ccsds/identifiers/base.rb +41 -0
  206. data/lib/pubid/ccsds/identifiers/base_BASE_88929.rb +70 -0
  207. data/lib/pubid/ccsds/identifiers/corrigendum.rb +40 -0
  208. data/lib/pubid/ccsds/identifiers.rb +10 -0
  209. data/lib/pubid/ccsds/parser.rb +73 -0
  210. data/lib/pubid/ccsds/single_identifier.rb +77 -0
  211. data/lib/pubid/ccsds/supplement_identifier.rb +37 -0
  212. data/lib/pubid/ccsds/urn_generator.rb +115 -0
  213. data/lib/pubid/ccsds/urn_parser.rb +20 -0
  214. data/lib/pubid/ccsds.rb +64 -0
  215. data/lib/pubid/cen_cenelec/builder.rb +362 -0
  216. data/lib/pubid/cen_cenelec/identifier.rb +38 -0
  217. data/lib/pubid/cen_cenelec/identifiers/adopted_european_norm.rb +45 -0
  218. data/lib/pubid/cen_cenelec/identifiers/amendment.rb +19 -0
  219. data/lib/pubid/cen_cenelec/identifiers/base.rb +34 -0
  220. data/lib/pubid/cen_cenelec/identifiers/cen_report.rb +29 -0
  221. data/lib/pubid/cen_cenelec/identifiers/cen_workshop_agreement.rb +28 -0
  222. data/lib/pubid/cen_cenelec/identifiers/cenelec_harmonization_document.rb +28 -0
  223. data/lib/pubid/cen_cenelec/identifiers/consolidated_identifier.rb +40 -0
  224. data/lib/pubid/cen_cenelec/identifiers/corrigendum.rb +20 -0
  225. data/lib/pubid/cen_cenelec/identifiers/european_norm.rb +99 -0
  226. data/lib/pubid/cen_cenelec/identifiers/european_prestandard.rb +50 -0
  227. data/lib/pubid/cen_cenelec/identifiers/european_specification.rb +29 -0
  228. data/lib/pubid/cen_cenelec/identifiers/fragment.rb +18 -0
  229. data/lib/pubid/cen_cenelec/identifiers/guide.rb +27 -0
  230. data/lib/pubid/cen_cenelec/identifiers/harmonization_document.rb +28 -0
  231. data/lib/pubid/cen_cenelec/identifiers/technical_report.rb +28 -0
  232. data/lib/pubid/cen_cenelec/identifiers/technical_specification.rb +36 -0
  233. data/lib/pubid/cen_cenelec/identifiers.rb +32 -0
  234. data/lib/pubid/cen_cenelec/parser.rb +144 -0
  235. data/lib/pubid/cen_cenelec/renderer.rb +261 -0
  236. data/lib/pubid/cen_cenelec/single_identifier.rb +49 -0
  237. data/lib/pubid/cen_cenelec/supplement_identifier.rb +47 -0
  238. data/lib/pubid/cen_cenelec/urn_generator.rb +129 -0
  239. data/lib/pubid/cen_cenelec/urn_parser.rb +28 -0
  240. data/lib/pubid/cen_cenelec.rb +208 -0
  241. data/lib/pubid/cie/builder.rb +524 -0
  242. data/lib/pubid/cie/components/language.rb +58 -0
  243. data/lib/pubid/cie/identifier.rb +33 -0
  244. data/lib/pubid/cie/identifiers/bundle.rb +50 -0
  245. data/lib/pubid/cie/identifiers/code_attributes.rb +52 -0
  246. data/lib/pubid/cie/identifiers/conference.rb +37 -0
  247. data/lib/pubid/cie/identifiers/corrigendum.rb +34 -0
  248. data/lib/pubid/cie/identifiers/dual_published.rb +41 -0
  249. data/lib/pubid/cie/identifiers/identical.rb +64 -0
  250. data/lib/pubid/cie/identifiers/joint_published.rb +52 -0
  251. data/lib/pubid/cie/identifiers/proceedings.rb +41 -0
  252. data/lib/pubid/cie/identifiers/standard.rb +93 -0
  253. data/lib/pubid/cie/identifiers/supplement.rb +47 -0
  254. data/lib/pubid/cie/identifiers/tutorial_bundle.rb +24 -0
  255. data/lib/pubid/cie/identifiers.rb +19 -0
  256. data/lib/pubid/cie/parser.rb +396 -0
  257. data/lib/pubid/cie/single_identifier.rb +69 -0
  258. data/lib/pubid/cie/supplement_identifier.rb +39 -0
  259. data/lib/pubid/cie/urn_generator.rb +126 -0
  260. data/lib/pubid/cie/urn_parser.rb +28 -0
  261. data/lib/pubid/cie.rb +83 -0
  262. data/lib/pubid/components/adoption.rb +104 -0
  263. data/lib/pubid/components/code.rb +47 -0
  264. data/lib/pubid/components/date.rb +72 -0
  265. data/lib/pubid/components/edition.rb +35 -0
  266. data/lib/pubid/components/iteration.rb +32 -0
  267. data/lib/pubid/components/language.rb +39 -0
  268. data/lib/pubid/components/locality.rb +19 -0
  269. data/lib/pubid/components/publisher.rb +39 -0
  270. data/lib/pubid/components/relationship.rb +151 -0
  271. data/lib/pubid/components/stage.rb +45 -0
  272. data/lib/pubid/components/supplement.rb +184 -0
  273. data/lib/pubid/components/type.rb +48 -0
  274. data/lib/pubid/components/typed_stage.rb +58 -0
  275. data/lib/pubid/components.rb +19 -0
  276. data/lib/pubid/core/pattern_doc_generator.rb +272 -0
  277. data/lib/pubid/core/update_codes.rb +98 -0
  278. data/lib/pubid/core.rb +8 -0
  279. data/lib/pubid/csa/builder.rb +671 -0
  280. data/lib/pubid/csa/components/code.rb +9 -0
  281. data/lib/pubid/csa/components.rb +9 -0
  282. data/lib/pubid/csa/composite_identifier.rb +38 -0
  283. data/lib/pubid/csa/identifier.rb +570 -0
  284. data/lib/pubid/csa/identifiers/base.rb +10 -0
  285. data/lib/pubid/csa/identifiers/bundled.rb +141 -0
  286. data/lib/pubid/csa/identifiers/canadian_adopted.rb +82 -0
  287. data/lib/pubid/csa/identifiers/cec.rb +66 -0
  288. data/lib/pubid/csa/identifiers/combined.rb +158 -0
  289. data/lib/pubid/csa/identifiers/csa_adopted.rb +78 -0
  290. data/lib/pubid/csa/identifiers/package.rb +65 -0
  291. data/lib/pubid/csa/identifiers/series.rb +23 -0
  292. data/lib/pubid/csa/identifiers/standard.rb +10 -0
  293. data/lib/pubid/csa/identifiers.rb +17 -0
  294. data/lib/pubid/csa/parser.rb +449 -0
  295. data/lib/pubid/csa/renderer.rb +300 -0
  296. data/lib/pubid/csa/single_identifier.rb +119 -0
  297. data/lib/pubid/csa/urn_generator.rb +87 -0
  298. data/lib/pubid/csa/urn_parser.rb +33 -0
  299. data/lib/pubid/csa/wrapper_identifier.rb +64 -0
  300. data/lib/pubid/csa.rb +80 -0
  301. data/lib/pubid/doi/builder.rb +18 -0
  302. data/lib/pubid/doi/identifier.rb +49 -0
  303. data/lib/pubid/doi/identifiers/resource.rb +30 -0
  304. data/lib/pubid/doi/identifiers.rb +9 -0
  305. data/lib/pubid/doi/parser.rb +52 -0
  306. data/lib/pubid/doi/renderer.rb +13 -0
  307. data/lib/pubid/doi.rb +55 -0
  308. data/lib/pubid/easc/builder.rb +55 -0
  309. data/lib/pubid/easc/identifier.rb +59 -0
  310. data/lib/pubid/easc/identifiers/pmg.rb +20 -0
  311. data/lib/pubid/easc/identifiers/rmg.rb +19 -0
  312. data/lib/pubid/easc/identifiers.rb +10 -0
  313. data/lib/pubid/easc/parser.rb +88 -0
  314. data/lib/pubid/easc/renderer.rb +48 -0
  315. data/lib/pubid/easc/urn_generator.rb +46 -0
  316. data/lib/pubid/easc/urn_parser.rb +57 -0
  317. data/lib/pubid/easc.rb +52 -0
  318. data/lib/pubid/ecma/builder.rb +45 -0
  319. data/lib/pubid/ecma/identifier.rb +147 -0
  320. data/lib/pubid/ecma/identifiers/memento.rb +30 -0
  321. data/lib/pubid/ecma/identifiers/standard.rb +31 -0
  322. data/lib/pubid/ecma/identifiers/technical_report.rb +31 -0
  323. data/lib/pubid/ecma/identifiers.rb +11 -0
  324. data/lib/pubid/ecma/parser.rb +76 -0
  325. data/lib/pubid/ecma/renderer.rb +67 -0
  326. data/lib/pubid/ecma/urn_generator.rb +46 -0
  327. data/lib/pubid/ecma/urn_parser.rb +51 -0
  328. data/lib/pubid/ecma.rb +67 -0
  329. data/lib/pubid/errors.rb +79 -0
  330. data/lib/pubid/etsi/builder.rb +142 -0
  331. data/lib/pubid/etsi/components/code.rb +43 -0
  332. data/lib/pubid/etsi/components/version.rb +32 -0
  333. data/lib/pubid/etsi/components.rb +10 -0
  334. data/lib/pubid/etsi/identifier.rb +4 -0
  335. data/lib/pubid/etsi/identifiers/amendment.rb +15 -0
  336. data/lib/pubid/etsi/identifiers/base.rb +74 -0
  337. data/lib/pubid/etsi/identifiers/corrigendum.rb +15 -0
  338. data/lib/pubid/etsi/identifiers/etsi_standard.rb +222 -0
  339. data/lib/pubid/etsi/identifiers/supplement_identifier.rb +123 -0
  340. data/lib/pubid/etsi/identifiers.rb +13 -0
  341. data/lib/pubid/etsi/parser.rb +139 -0
  342. data/lib/pubid/etsi/renderer.rb +48 -0
  343. data/lib/pubid/etsi/urn_generator.rb +76 -0
  344. data/lib/pubid/etsi/urn_parser.rb +34 -0
  345. data/lib/pubid/etsi.rb +71 -0
  346. data/lib/pubid/export/auditor.rb +89 -0
  347. data/lib/pubid/export/exporter.rb +33 -0
  348. data/lib/pubid/export/flavor_exporter.rb +250 -0
  349. data/lib/pubid/export/result.rb +97 -0
  350. data/lib/pubid/export.rb +12 -0
  351. data/lib/pubid/format_detector.rb +16 -0
  352. data/lib/pubid/format_registry.rb +42 -0
  353. data/lib/pubid/gb/builder.rb +45 -0
  354. data/lib/pubid/gb/identifier.rb +67 -0
  355. data/lib/pubid/gb/identifiers/standard.rb +34 -0
  356. data/lib/pubid/gb/identifiers.rb +9 -0
  357. data/lib/pubid/gb/parser.rb +63 -0
  358. data/lib/pubid/gb/renderer.rb +35 -0
  359. data/lib/pubid/gb.rb +87 -0
  360. data/lib/pubid/gost/builder.rb +148 -0
  361. data/lib/pubid/gost/identifier.rb +47 -0
  362. data/lib/pubid/gost/identifiers/foreign_reference.rb +25 -0
  363. data/lib/pubid/gost/identifiers/harmonized.rb +39 -0
  364. data/lib/pubid/gost/identifiers/identical_adoption.rb +39 -0
  365. data/lib/pubid/gost/identifiers/interstate_standard.rb +20 -0
  366. data/lib/pubid/gost/identifiers/national_standard.rb +19 -0
  367. data/lib/pubid/gost/identifiers.rb +13 -0
  368. data/lib/pubid/gost/parser.rb +105 -0
  369. data/lib/pubid/gost/renderer.rb +48 -0
  370. data/lib/pubid/gost/urn_generator.rb +46 -0
  371. data/lib/pubid/gost/urn_parser.rb +52 -0
  372. data/lib/pubid/gost.rb +49 -0
  373. data/lib/pubid/iala/builder.rb +102 -0
  374. data/lib/pubid/iala/identifier.rb +90 -0
  375. data/lib/pubid/iala/identifiers/advice.rb +15 -0
  376. data/lib/pubid/iala/identifiers/annex.rb +57 -0
  377. data/lib/pubid/iala/identifiers/general_assembly.rb +19 -0
  378. data/lib/pubid/iala/identifiers/guideline.rb +17 -0
  379. data/lib/pubid/iala/identifiers/letter.rb +16 -0
  380. data/lib/pubid/iala/identifiers/manual.rb +18 -0
  381. data/lib/pubid/iala/identifiers/model_course.rb +17 -0
  382. data/lib/pubid/iala/identifiers/recommendation.rb +17 -0
  383. data/lib/pubid/iala/identifiers/report.rb +16 -0
  384. data/lib/pubid/iala/identifiers/resolution.rb +16 -0
  385. data/lib/pubid/iala/identifiers/standard.rb +17 -0
  386. data/lib/pubid/iala/identifiers.rb +19 -0
  387. data/lib/pubid/iala/parser.rb +138 -0
  388. data/lib/pubid/iala/renderer.rb +36 -0
  389. data/lib/pubid/iala/urn_generator.rb +49 -0
  390. data/lib/pubid/iala/urn_parser.rb +67 -0
  391. data/lib/pubid/iala.rb +71 -0
  392. data/lib/pubid/iana/builder.rb +22 -0
  393. data/lib/pubid/iana/identifier.rb +140 -0
  394. data/lib/pubid/iana/identifiers/registry.rb +69 -0
  395. data/lib/pubid/iana/identifiers.rb +9 -0
  396. data/lib/pubid/iana/parser.rb +38 -0
  397. data/lib/pubid/iana/renderer.rb +32 -0
  398. data/lib/pubid/iana/urn_generator.rb +19 -0
  399. data/lib/pubid/iana/urn_parser.rb +24 -0
  400. data/lib/pubid/iana.rb +69 -0
  401. data/lib/pubid/identifier.rb +1118 -0
  402. data/lib/pubid/identifier_metadata.rb +148 -0
  403. data/lib/pubid/idf/builder.rb +82 -0
  404. data/lib/pubid/idf/identifier.rb +79 -0
  405. data/lib/pubid/idf/identifiers/amendment.rb +28 -0
  406. data/lib/pubid/idf/identifiers/corrigendum.rb +28 -0
  407. data/lib/pubid/idf/identifiers/international_standard.rb +124 -0
  408. data/lib/pubid/idf/identifiers/reviewed_method.rb +101 -0
  409. data/lib/pubid/idf/identifiers.rb +13 -0
  410. data/lib/pubid/idf/parser.rb +144 -0
  411. data/lib/pubid/idf/renderer.rb +84 -0
  412. data/lib/pubid/idf/single_identifier.rb +19 -0
  413. data/lib/pubid/idf/supplement_identifier.rb +35 -0
  414. data/lib/pubid/idf/urn_generator.rb +49 -0
  415. data/lib/pubid/idf/urn_parser.rb +25 -0
  416. data/lib/pubid/idf.rb +89 -0
  417. data/lib/pubid/iec/builder.rb +458 -0
  418. data/lib/pubid/iec/components/code.rb +36 -0
  419. data/lib/pubid/iec/components/consolidated_amendment.rb +59 -0
  420. data/lib/pubid/iec/components/publisher.rb +35 -0
  421. data/lib/pubid/iec/components/sheet.rb +32 -0
  422. data/lib/pubid/iec/components/trf_info.rb +38 -0
  423. data/lib/pubid/iec/components/vap_suffix.rb +41 -0
  424. data/lib/pubid/iec/components.rb +14 -0
  425. data/lib/pubid/iec/identifier.rb +341 -0
  426. data/lib/pubid/iec/identifiers/amendment.rb +97 -0
  427. data/lib/pubid/iec/identifiers/base.rb +54 -0
  428. data/lib/pubid/iec/identifiers/component_specification.rb +29 -0
  429. data/lib/pubid/iec/identifiers/conformity_assessment.rb +28 -0
  430. data/lib/pubid/iec/identifiers/consolidated_identifier.rb +85 -0
  431. data/lib/pubid/iec/identifiers/corrigendum.rb +93 -0
  432. data/lib/pubid/iec/identifiers/fragment_identifier.rb +148 -0
  433. data/lib/pubid/iec/identifiers/guide.rb +86 -0
  434. data/lib/pubid/iec/identifiers/international_standard.rb +162 -0
  435. data/lib/pubid/iec/identifiers/interpretation_sheet.rb +103 -0
  436. data/lib/pubid/iec/identifiers/operational_document.rb +29 -0
  437. data/lib/pubid/iec/identifiers/publicly_available_specification.rb +88 -0
  438. data/lib/pubid/iec/identifiers/sheet_identifier.rb +68 -0
  439. data/lib/pubid/iec/identifiers/societal_technology_trend_report.rb +30 -0
  440. data/lib/pubid/iec/identifiers/systems_reference_document.rb +29 -0
  441. data/lib/pubid/iec/identifiers/technical_group.rb +30 -0
  442. data/lib/pubid/iec/identifiers/technical_report.rb +114 -0
  443. data/lib/pubid/iec/identifiers/technical_specification.rb +114 -0
  444. data/lib/pubid/iec/identifiers/technology_report.rb +28 -0
  445. data/lib/pubid/iec/identifiers/test_report_form.rb +35 -0
  446. data/lib/pubid/iec/identifiers/vap_identifier.rb +76 -0
  447. data/lib/pubid/iec/identifiers/white_paper.rb +29 -0
  448. data/lib/pubid/iec/identifiers/working_document.rb +62 -0
  449. data/lib/pubid/iec/identifiers.rb +31 -0
  450. data/lib/pubid/iec/parser.rb +443 -0
  451. data/lib/pubid/iec/renderer.rb +267 -0
  452. data/lib/pubid/iec/rendering_style.rb +113 -0
  453. data/lib/pubid/iec/single_identifier.rb +72 -0
  454. data/lib/pubid/iec/supplement_identifier.rb +178 -0
  455. data/lib/pubid/iec/urn_generator.rb +245 -0
  456. data/lib/pubid/iec/urn_parser.rb +128 -0
  457. data/lib/pubid/iec.rb +64 -0
  458. data/lib/pubid/ieee/aiee/builder.rb +85 -0
  459. data/lib/pubid/ieee/aiee/identifier.rb +89 -0
  460. data/lib/pubid/ieee/aiee/parser.rb +130 -0
  461. data/lib/pubid/ieee/aiee.rb +11 -0
  462. data/lib/pubid/ieee/builder.rb +1554 -0
  463. data/lib/pubid/ieee/compaction.rb +121 -0
  464. data/lib/pubid/ieee/components/code.rb +110 -0
  465. data/lib/pubid/ieee/components/draft.rb +147 -0
  466. data/lib/pubid/ieee/components/relationship.rb +13 -0
  467. data/lib/pubid/ieee/components/typed_stage.rb +100 -0
  468. data/lib/pubid/ieee/identifier.rb +5 -0
  469. data/lib/pubid/ieee/identifiers/adopted_standard.rb +81 -0
  470. data/lib/pubid/ieee/identifiers/amendment.rb +40 -0
  471. data/lib/pubid/ieee/identifiers/base.rb +384 -0
  472. data/lib/pubid/ieee/identifiers/code_number.rb +82 -0
  473. data/lib/pubid/ieee/identifiers/conformance_identifier.rb +29 -0
  474. data/lib/pubid/ieee/identifiers/corrigendum.rb +40 -0
  475. data/lib/pubid/ieee/identifiers/csa_dual_published.rb +62 -0
  476. data/lib/pubid/ieee/identifiers/dual_identifier.rb +14 -0
  477. data/lib/pubid/ieee/identifiers/dual_published.rb +79 -0
  478. data/lib/pubid/ieee/identifiers/iec_ieee_copublished.rb +115 -0
  479. data/lib/pubid/ieee/identifiers/interpretation_identifier.rb +48 -0
  480. data/lib/pubid/ieee/identifiers/joint_development.rb +223 -0
  481. data/lib/pubid/ieee/identifiers/multi_numbered_identifier.rb +85 -0
  482. data/lib/pubid/ieee/identifiers/nesc/base.rb +127 -0
  483. data/lib/pubid/ieee/identifiers/nesc/draft.rb +43 -0
  484. data/lib/pubid/ieee/identifiers/nesc/edition.rb +32 -0
  485. data/lib/pubid/ieee/identifiers/nesc/handbook.rb +43 -0
  486. data/lib/pubid/ieee/identifiers/nesc/redline.rb +35 -0
  487. data/lib/pubid/ieee/identifiers/nesc/standard.rb +41 -0
  488. data/lib/pubid/ieee/identifiers/nesc.rb +16 -0
  489. data/lib/pubid/ieee/identifiers/parenthetical_identifier.rb +14 -0
  490. data/lib/pubid/ieee/identifiers/project_draft_identifier.rb +49 -0
  491. data/lib/pubid/ieee/identifiers/redlined_standard.rb +26 -0
  492. data/lib/pubid/ieee/identifiers/si_standard.rb +38 -0
  493. data/lib/pubid/ieee/identifiers/standard.rb +44 -0
  494. data/lib/pubid/ieee/identifiers/supplement_identifier.rb +42 -0
  495. data/lib/pubid/ieee/identifiers.rb +34 -0
  496. data/lib/pubid/ieee/ire/builder.rb +62 -0
  497. data/lib/pubid/ieee/ire/identifier.rb +73 -0
  498. data/lib/pubid/ieee/ire/parser.rb +91 -0
  499. data/lib/pubid/ieee/ire.rb +11 -0
  500. data/lib/pubid/ieee/nesc/builder.rb +114 -0
  501. data/lib/pubid/ieee/nesc/parser.rb +155 -0
  502. data/lib/pubid/ieee/nesc.rb +10 -0
  503. data/lib/pubid/ieee/parser.rb +1644 -0
  504. data/lib/pubid/ieee/pre_parser.rb +301 -0
  505. data/lib/pubid/ieee/renderer.rb +408 -0
  506. data/lib/pubid/ieee/typed_stages.rb +183 -0
  507. data/lib/pubid/ieee/urn_generator.rb +215 -0
  508. data/lib/pubid/ieee/urn_parser.rb +34 -0
  509. data/lib/pubid/ieee.rb +163 -0
  510. data/lib/pubid/ieee_debug.rb +30 -0
  511. data/lib/pubid/ietf/builder.rb +78 -0
  512. data/lib/pubid/ietf/identifier.rb +5 -0
  513. data/lib/pubid/ietf/identifiers/base.rb +72 -0
  514. data/lib/pubid/ietf/identifiers/bcp.rb +36 -0
  515. data/lib/pubid/ietf/identifiers/fyi.rb +36 -0
  516. data/lib/pubid/ietf/identifiers/internet_draft.rb +45 -0
  517. data/lib/pubid/ietf/identifiers/rfc.rb +27 -0
  518. data/lib/pubid/ietf/identifiers/serialization.rb +51 -0
  519. data/lib/pubid/ietf/identifiers/std.rb +36 -0
  520. data/lib/pubid/ietf/identifiers.rb +14 -0
  521. data/lib/pubid/ietf/parser.rb +61 -0
  522. data/lib/pubid/ietf/renderer.rb +64 -0
  523. data/lib/pubid/ietf/urn_generator.rb +37 -0
  524. data/lib/pubid/ietf/urn_parser.rb +43 -0
  525. data/lib/pubid/ietf.rb +85 -0
  526. data/lib/pubid/iho/builder.rb +37 -0
  527. data/lib/pubid/iho/identifier.rb +7 -0
  528. data/lib/pubid/iho/identifiers/base.rb +75 -0
  529. data/lib/pubid/iho/identifiers/bibliographic.rb +16 -0
  530. data/lib/pubid/iho/identifiers/circular_letter.rb +15 -0
  531. data/lib/pubid/iho/identifiers/miscellaneous.rb +16 -0
  532. data/lib/pubid/iho/identifiers/publication.rb +15 -0
  533. data/lib/pubid/iho/identifiers/standard.rb +15 -0
  534. data/lib/pubid/iho/identifiers.rb +13 -0
  535. data/lib/pubid/iho/parser.rb +68 -0
  536. data/lib/pubid/iho/renderer.rb +30 -0
  537. data/lib/pubid/iho/urn_generator.rb +29 -0
  538. data/lib/pubid/iho/urn_parser.rb +58 -0
  539. data/lib/pubid/iho.rb +77 -0
  540. data/lib/pubid/isbn/builder.rb +44 -0
  541. data/lib/pubid/isbn/check_digit.rb +53 -0
  542. data/lib/pubid/isbn/identifier.rb +81 -0
  543. data/lib/pubid/isbn/identifiers/book.rb +30 -0
  544. data/lib/pubid/isbn/identifiers.rb +9 -0
  545. data/lib/pubid/isbn/parser.rb +45 -0
  546. data/lib/pubid/isbn/renderer.rb +16 -0
  547. data/lib/pubid/isbn.rb +52 -0
  548. data/lib/pubid/iso/builder.rb +316 -0
  549. data/lib/pubid/iso/bundled_identifier.rb +138 -0
  550. data/lib/pubid/iso/combined_identifier.rb +24 -0
  551. data/lib/pubid/iso/components/code.rb +24 -0
  552. data/lib/pubid/iso/components/publisher.rb +62 -0
  553. data/lib/pubid/iso/components.rb +10 -0
  554. data/lib/pubid/iso/format_resolver.rb +45 -0
  555. data/lib/pubid/iso/identifier.rb +318 -0
  556. data/lib/pubid/iso/identifiers/addendum.rb +107 -0
  557. data/lib/pubid/iso/identifiers/amendment.rb +132 -0
  558. data/lib/pubid/iso/identifiers/corrigendum.rb +108 -0
  559. data/lib/pubid/iso/identifiers/data.rb +75 -0
  560. data/lib/pubid/iso/identifiers/directives.rb +65 -0
  561. data/lib/pubid/iso/identifiers/directives_supplement.rb +148 -0
  562. data/lib/pubid/iso/identifiers/extract.rb +30 -0
  563. data/lib/pubid/iso/identifiers/guide.rb +99 -0
  564. data/lib/pubid/iso/identifiers/international_standard.rb +168 -0
  565. data/lib/pubid/iso/identifiers/international_standardized_profile.rb +94 -0
  566. data/lib/pubid/iso/identifiers/international_workshop_agreement.rb +95 -0
  567. data/lib/pubid/iso/identifiers/pas.rb +93 -0
  568. data/lib/pubid/iso/identifiers/recommendation.rb +45 -0
  569. data/lib/pubid/iso/identifiers/supplement.rb +95 -0
  570. data/lib/pubid/iso/identifiers/tc_document.rb +137 -0
  571. data/lib/pubid/iso/identifiers/technical_report.rb +103 -0
  572. data/lib/pubid/iso/identifiers/technical_specification.rb +102 -0
  573. data/lib/pubid/iso/identifiers/technology_trends_assessments.rb +95 -0
  574. data/lib/pubid/iso/identifiers.rb +32 -0
  575. data/lib/pubid/iso/normalizer.rb +89 -0
  576. data/lib/pubid/iso/parser.rb +534 -0
  577. data/lib/pubid/iso/rendering_style.rb +120 -0
  578. data/lib/pubid/iso/single_identifier.rb +60 -0
  579. data/lib/pubid/iso/supplement_identifier.rb +53 -0
  580. data/lib/pubid/iso/urn_generator.rb +313 -0
  581. data/lib/pubid/iso/urn_parser.rb +442 -0
  582. data/lib/pubid/iso/utilities.rb +86 -0
  583. data/lib/pubid/iso.rb +236 -0
  584. data/lib/pubid/itu/builder.rb +360 -0
  585. data/lib/pubid/itu/components/code.rb +84 -0
  586. data/lib/pubid/itu/components/designation.rb +35 -0
  587. data/lib/pubid/itu/components/sector.rb +46 -0
  588. data/lib/pubid/itu/components/series.rb +25 -0
  589. data/lib/pubid/itu/components.rb +12 -0
  590. data/lib/pubid/itu/i18n.rb +9 -0
  591. data/lib/pubid/itu/i18n.yaml +30 -0
  592. data/lib/pubid/itu/identifier.rb +4 -0
  593. data/lib/pubid/itu/identifiers/addendum.rb +15 -0
  594. data/lib/pubid/itu/identifiers/amendment.rb +19 -0
  595. data/lib/pubid/itu/identifiers/annex.rb +83 -0
  596. data/lib/pubid/itu/identifiers/annex_of_recommendation.rb +87 -0
  597. data/lib/pubid/itu/identifiers/appendix_of_recommendation.rb +92 -0
  598. data/lib/pubid/itu/identifiers/base.rb +482 -0
  599. data/lib/pubid/itu/identifiers/combined_identifier.rb +121 -0
  600. data/lib/pubid/itu/identifiers/corrigendum.rb +35 -0
  601. data/lib/pubid/itu/identifiers/errata.rb +15 -0
  602. data/lib/pubid/itu/identifiers/handbook.rb +38 -0
  603. data/lib/pubid/itu/identifiers/question.rb +63 -0
  604. data/lib/pubid/itu/identifiers/recommendation.rb +15 -0
  605. data/lib/pubid/itu/identifiers/report.rb +39 -0
  606. data/lib/pubid/itu/identifiers/special_publication.rb +31 -0
  607. data/lib/pubid/itu/identifiers/standard_serialization.rb +58 -0
  608. data/lib/pubid/itu/identifiers/supplement.rb +149 -0
  609. data/lib/pubid/itu/identifiers.rb +25 -0
  610. data/lib/pubid/itu/model.rb +111 -0
  611. data/lib/pubid/itu/parser.rb +679 -0
  612. data/lib/pubid/itu/urn_generator.rb +135 -0
  613. data/lib/pubid/itu/urn_parser.rb +31 -0
  614. data/lib/pubid/itu.rb +78 -0
  615. data/lib/pubid/jcgm/builder.rb +106 -0
  616. data/lib/pubid/jcgm/components/publisher.rb +20 -0
  617. data/lib/pubid/jcgm/components.rb +9 -0
  618. data/lib/pubid/jcgm/identifier.rb +21 -0
  619. data/lib/pubid/jcgm/identifiers/amendment.rb +24 -0
  620. data/lib/pubid/jcgm/identifiers/corrigendum.rb +34 -0
  621. data/lib/pubid/jcgm/identifiers/guide.rb +21 -0
  622. data/lib/pubid/jcgm/identifiers/gum_guide.rb +39 -0
  623. data/lib/pubid/jcgm/identifiers/meeting.rb +47 -0
  624. data/lib/pubid/jcgm/identifiers.rb +13 -0
  625. data/lib/pubid/jcgm/parser.rb +151 -0
  626. data/lib/pubid/jcgm/renderer.rb +89 -0
  627. data/lib/pubid/jcgm/single_identifier.rb +118 -0
  628. data/lib/pubid/jcgm/supplement_identifier.rb +51 -0
  629. data/lib/pubid/jcgm/urn_generator.rb +116 -0
  630. data/lib/pubid/jcgm/urn_parser.rb +177 -0
  631. data/lib/pubid/jcgm.rb +86 -0
  632. data/lib/pubid/jis/builder.rb +116 -0
  633. data/lib/pubid/jis/identifier.rb +194 -0
  634. data/lib/pubid/jis/identifiers/amendment.rb +16 -0
  635. data/lib/pubid/jis/identifiers/corrigendum.rb +16 -0
  636. data/lib/pubid/jis/identifiers/explanation.rb +22 -0
  637. data/lib/pubid/jis/identifiers/japanese_industrial_standard.rb +16 -0
  638. data/lib/pubid/jis/identifiers/standard.rb +28 -0
  639. data/lib/pubid/jis/identifiers/technical_report.rb +32 -0
  640. data/lib/pubid/jis/identifiers/technical_specification.rb +32 -0
  641. data/lib/pubid/jis/identifiers.rb +17 -0
  642. data/lib/pubid/jis/parser.rb +135 -0
  643. data/lib/pubid/jis/renderer.rb +69 -0
  644. data/lib/pubid/jis/single_identifier.rb +31 -0
  645. data/lib/pubid/jis/supplement_identifier.rb +50 -0
  646. data/lib/pubid/jis/urn_generator.rb +25 -0
  647. data/lib/pubid/jis/urn_parser.rb +23 -0
  648. data/lib/pubid/jis.rb +69 -0
  649. data/lib/pubid/lutaml/no_store_registration.rb +30 -0
  650. data/lib/pubid/nist/builder.rb +481 -0
  651. data/lib/pubid/nist/caster.rb +1234 -0
  652. data/lib/pubid/nist/circular_supplement_builder.rb +293 -0
  653. data/lib/pubid/nist/components/code.rb +27 -0
  654. data/lib/pubid/nist/components/edition.rb +134 -0
  655. data/lib/pubid/nist/components/issue_number.rb +28 -0
  656. data/lib/pubid/nist/components/part.rb +77 -0
  657. data/lib/pubid/nist/components/stage.rb +53 -0
  658. data/lib/pubid/nist/components/supplement.rb +191 -0
  659. data/lib/pubid/nist/components/translation.rb +42 -0
  660. data/lib/pubid/nist/components/update.rb +103 -0
  661. data/lib/pubid/nist/components/version.rb +35 -0
  662. data/lib/pubid/nist/components/volume.rb +32 -0
  663. data/lib/pubid/nist/components.rb +18 -0
  664. data/lib/pubid/nist/configuration.rb +87 -0
  665. data/lib/pubid/nist/identifier.rb +8 -0
  666. data/lib/pubid/nist/identifiers/base.rb +744 -0
  667. data/lib/pubid/nist/identifiers/circular.rb +80 -0
  668. data/lib/pubid/nist/identifiers/circular_supplement.rb +60 -0
  669. data/lib/pubid/nist/identifiers/commercial_standard.rb +42 -0
  670. data/lib/pubid/nist/identifiers/commercial_standard_emergency.rb +58 -0
  671. data/lib/pubid/nist/identifiers/commercial_standards_monthly.rb +69 -0
  672. data/lib/pubid/nist/identifiers/crpl_report.rb +138 -0
  673. data/lib/pubid/nist/identifiers/dated_document.rb +49 -0
  674. data/lib/pubid/nist/identifiers/federal_information_processing_standards.rb +95 -0
  675. data/lib/pubid/nist/identifiers/grant_contractor_report.rb +36 -0
  676. data/lib/pubid/nist/identifiers/handbook.rb +58 -0
  677. data/lib/pubid/nist/identifiers/internal_report.rb +64 -0
  678. data/lib/pubid/nist/identifiers/letter_circular.rb +46 -0
  679. data/lib/pubid/nist/identifiers/miscellaneous_publication.rb +74 -0
  680. data/lib/pubid/nist/identifiers/monograph.rb +80 -0
  681. data/lib/pubid/nist/identifiers/ncstar.rb +41 -0
  682. data/lib/pubid/nist/identifiers/nsrds.rb +41 -0
  683. data/lib/pubid/nist/identifiers/owmwp.rb +35 -0
  684. data/lib/pubid/nist/identifiers/report.rb +77 -0
  685. data/lib/pubid/nist/identifiers/special_publication.rb +46 -0
  686. data/lib/pubid/nist/identifiers/technical_note.rb +91 -0
  687. data/lib/pubid/nist/identifiers.rb +33 -0
  688. data/lib/pubid/nist/parser.rb +752 -0
  689. data/lib/pubid/nist/parser_output_normalizer.rb +233 -0
  690. data/lib/pubid/nist/preprocessor.rb +443 -0
  691. data/lib/pubid/nist/renderer.rb +43 -0
  692. data/lib/pubid/nist/router.rb +149 -0
  693. data/lib/pubid/nist/series/base.rb +58 -0
  694. data/lib/pubid/nist/series/crpl.rb +13 -0
  695. data/lib/pubid/nist/series/fips.rb +14 -0
  696. data/lib/pubid/nist/series/ir.rb +57 -0
  697. data/lib/pubid/nist/series/letter_preserving.rb +15 -0
  698. data/lib/pubid/nist/series/mono.rb +19 -0
  699. data/lib/pubid/nist/series/ncstar.rb +20 -0
  700. data/lib/pubid/nist/series.rb +49 -0
  701. data/lib/pubid/nist/supplement_identifier.rb +84 -0
  702. data/lib/pubid/nist/urn_generator.rb +133 -0
  703. data/lib/pubid/nist/urn_parser.rb +67 -0
  704. data/lib/pubid/nist.rb +138 -0
  705. data/lib/pubid/oasis/builder.rb +79 -0
  706. data/lib/pubid/oasis/identifier.rb +160 -0
  707. data/lib/pubid/oasis/identifiers/standard.rb +29 -0
  708. data/lib/pubid/oasis/identifiers.rb +9 -0
  709. data/lib/pubid/oasis/parser.rb +34 -0
  710. data/lib/pubid/oasis/renderer.rb +29 -0
  711. data/lib/pubid/oasis/urn_generator.rb +25 -0
  712. data/lib/pubid/oasis/urn_parser.rb +22 -0
  713. data/lib/pubid/oasis.rb +79 -0
  714. data/lib/pubid/ogc/builder.rb +33 -0
  715. data/lib/pubid/ogc/identifier.rb +81 -0
  716. data/lib/pubid/ogc/identifiers/document.rb +26 -0
  717. data/lib/pubid/ogc/identifiers.rb +10 -0
  718. data/lib/pubid/ogc/parser.rb +40 -0
  719. data/lib/pubid/ogc/renderer.rb +33 -0
  720. data/lib/pubid/ogc/urn_generator.rb +14 -0
  721. data/lib/pubid/ogc/urn_parser.rb +22 -0
  722. data/lib/pubid/ogc.rb +71 -0
  723. data/lib/pubid/oiml/builder.rb +270 -0
  724. data/lib/pubid/oiml/components/code.rb +37 -0
  725. data/lib/pubid/oiml/components.rb +9 -0
  726. data/lib/pubid/oiml/identifier.rb +64 -0
  727. data/lib/pubid/oiml/identifiers/amendment.rb +13 -0
  728. data/lib/pubid/oiml/identifiers/annex.rb +29 -0
  729. data/lib/pubid/oiml/identifiers/basic_publication.rb +15 -0
  730. data/lib/pubid/oiml/identifiers/bulletin.rb +137 -0
  731. data/lib/pubid/oiml/identifiers/code_number.rb +90 -0
  732. data/lib/pubid/oiml/identifiers/document.rb +15 -0
  733. data/lib/pubid/oiml/identifiers/errata.rb +15 -0
  734. data/lib/pubid/oiml/identifiers/expert_report.rb +15 -0
  735. data/lib/pubid/oiml/identifiers/guide.rb +15 -0
  736. data/lib/pubid/oiml/identifiers/recommendation.rb +15 -0
  737. data/lib/pubid/oiml/identifiers/seminar_report.rb +15 -0
  738. data/lib/pubid/oiml/identifiers/vocabulary.rb +15 -0
  739. data/lib/pubid/oiml/identifiers.rb +20 -0
  740. data/lib/pubid/oiml/parser.rb +274 -0
  741. data/lib/pubid/oiml/renderer.rb +232 -0
  742. data/lib/pubid/oiml/single_identifier.rb +113 -0
  743. data/lib/pubid/oiml/supplement_identifier.rb +88 -0
  744. data/lib/pubid/oiml/urn_generator.rb +78 -0
  745. data/lib/pubid/oiml/urn_parser.rb +39 -0
  746. data/lib/pubid/oiml.rb +81 -0
  747. data/lib/pubid/omg/builder.rb +19 -0
  748. data/lib/pubid/omg/identifier.rb +47 -0
  749. data/lib/pubid/omg/identifiers/specification.rb +32 -0
  750. data/lib/pubid/omg/identifiers.rb +9 -0
  751. data/lib/pubid/omg/parser.rb +40 -0
  752. data/lib/pubid/omg/renderer.rb +19 -0
  753. data/lib/pubid/omg.rb +51 -0
  754. data/lib/pubid/parser/common_parse_methods.rb +13 -0
  755. data/lib/pubid/parser/common_parse_rules.rb +56 -0
  756. data/lib/pubid/parser/grammar.rb +74 -0
  757. data/lib/pubid/parser.rb +9 -0
  758. data/lib/pubid/parsers/base.rb +11 -0
  759. data/lib/pubid/parsers/mr_string.rb +206 -0
  760. data/lib/pubid/plateau/builder.rb +54 -0
  761. data/lib/pubid/plateau/identifier.rb +4 -0
  762. data/lib/pubid/plateau/identifiers/annex.rb +16 -0
  763. data/lib/pubid/plateau/identifiers/base.rb +55 -0
  764. data/lib/pubid/plateau/identifiers/handbook.rb +28 -0
  765. data/lib/pubid/plateau/identifiers/technical_report.rb +16 -0
  766. data/lib/pubid/plateau/identifiers.rb +11 -0
  767. data/lib/pubid/plateau/parser.rb +73 -0
  768. data/lib/pubid/plateau/renderer.rb +51 -0
  769. data/lib/pubid/plateau/supplement_identifier.rb +68 -0
  770. data/lib/pubid/plateau/urn_generator.rb +29 -0
  771. data/lib/pubid/plateau/urn_parser.rb +43 -0
  772. data/lib/pubid/plateau.rb +82 -0
  773. data/lib/pubid/prefixes_support.rb +51 -0
  774. data/lib/pubid/renderers/annotator.rb +233 -0
  775. data/lib/pubid/renderers/base.rb +66 -0
  776. data/lib/pubid/renderers/directives_renderer.rb +75 -0
  777. data/lib/pubid/renderers/guide_renderer.rb +26 -0
  778. data/lib/pubid/renderers/human_readable.rb +83 -0
  779. data/lib/pubid/renderers/iwa_renderer.rb +20 -0
  780. data/lib/pubid/renderers/mr_string.rb +64 -0
  781. data/lib/pubid/renderers/supplement_renderer.rb +36 -0
  782. data/lib/pubid/renderers/urn.rb +11 -0
  783. data/lib/pubid/renderers.rb +15 -0
  784. data/lib/pubid/rendering/context.rb +168 -0
  785. data/lib/pubid/rendering/date.rb +27 -0
  786. data/lib/pubid/rendering/language.rb +21 -0
  787. data/lib/pubid/rendering/numbering.rb +42 -0
  788. data/lib/pubid/rendering/publisher.rb +25 -0
  789. data/lib/pubid/rendering/stage.rb +38 -0
  790. data/lib/pubid/rendering/supplement.rb +46 -0
  791. data/lib/pubid/rendering.rb +13 -0
  792. data/lib/pubid/sae/builder.rb +32 -0
  793. data/lib/pubid/sae/components/code.rb +9 -0
  794. data/lib/pubid/sae/components/date.rb +27 -0
  795. data/lib/pubid/sae/components/type.rb +23 -0
  796. data/lib/pubid/sae/components.rb +11 -0
  797. data/lib/pubid/sae/identifier.rb +4 -0
  798. data/lib/pubid/sae/identifiers/base.rb +47 -0
  799. data/lib/pubid/sae/identifiers.rb +8 -0
  800. data/lib/pubid/sae/parser.rb +55 -0
  801. data/lib/pubid/sae/renderer.rb +36 -0
  802. data/lib/pubid/sae/urn_generator.rb +30 -0
  803. data/lib/pubid/sae/urn_parser.rb +36 -0
  804. data/lib/pubid/sae.rb +66 -0
  805. data/lib/pubid/tgpp/builder.rb +41 -0
  806. data/lib/pubid/tgpp/identifier.rb +107 -0
  807. data/lib/pubid/tgpp/identifiers/technical_report.rb +32 -0
  808. data/lib/pubid/tgpp/identifiers/technical_specification.rb +33 -0
  809. data/lib/pubid/tgpp/identifiers.rb +11 -0
  810. data/lib/pubid/tgpp/parser.rb +78 -0
  811. data/lib/pubid/tgpp/renderer.rb +52 -0
  812. data/lib/pubid/tgpp/urn_generator.rb +39 -0
  813. data/lib/pubid/tgpp/urn_parser.rb +30 -0
  814. data/lib/pubid/tgpp.rb +83 -0
  815. data/lib/pubid/type_resolver.rb +59 -0
  816. data/lib/pubid/un/builder.rb +42 -0
  817. data/lib/pubid/un/identifier.rb +40 -0
  818. data/lib/pubid/un/identifiers/document.rb +24 -0
  819. data/lib/pubid/un/identifiers.rb +9 -0
  820. data/lib/pubid/un/parser.rb +25 -0
  821. data/lib/pubid/un/renderer.rb +11 -0
  822. data/lib/pubid/un.rb +46 -0
  823. data/lib/pubid/urn_generator/base.rb +110 -0
  824. data/lib/pubid/urn_parser/base.rb +81 -0
  825. data/lib/pubid/urn_parser/errors.rb +16 -0
  826. data/lib/pubid/urn_parser.rb +14 -0
  827. data/lib/pubid/utils/string_normalizer.rb +196 -0
  828. data/lib/pubid/utils.rb +7 -0
  829. data/lib/pubid/version.rb +3 -1
  830. data/lib/pubid/w3c/builder.rb +55 -0
  831. data/lib/pubid/w3c/identifier.rb +130 -0
  832. data/lib/pubid/w3c/identifiers/candidate_recommendation.rb +31 -0
  833. data/lib/pubid/w3c/identifiers/candidate_recommendation_draft.rb +31 -0
  834. data/lib/pubid/w3c/identifiers/draft_note.rb +31 -0
  835. data/lib/pubid/w3c/identifiers/note.rb +30 -0
  836. data/lib/pubid/w3c/identifiers/obsolete_recommendation.rb +31 -0
  837. data/lib/pubid/w3c/identifiers/proposed_edited_recommendation.rb +31 -0
  838. data/lib/pubid/w3c/identifiers/proposed_recommendation.rb +31 -0
  839. data/lib/pubid/w3c/identifiers/recommendation.rb +31 -0
  840. data/lib/pubid/w3c/identifiers/standard.rb +12 -0
  841. data/lib/pubid/w3c/identifiers/superseded_recommendation.rb +31 -0
  842. data/lib/pubid/w3c/identifiers/working_draft.rb +30 -0
  843. data/lib/pubid/w3c/identifiers.rb +25 -0
  844. data/lib/pubid/w3c/parser.rb +43 -0
  845. data/lib/pubid/w3c/renderer.rb +35 -0
  846. data/lib/pubid/w3c/urn_generator.rb +18 -0
  847. data/lib/pubid/w3c/urn_parser.rb +36 -0
  848. data/lib/pubid/w3c.rb +67 -0
  849. data/lib/pubid/xsf/builder.rb +17 -0
  850. data/lib/pubid/xsf/identifier.rb +66 -0
  851. data/lib/pubid/xsf/identifiers/xep.rb +31 -0
  852. data/lib/pubid/xsf/identifiers.rb +9 -0
  853. data/lib/pubid/xsf/parser.rb +25 -0
  854. data/lib/pubid/xsf/renderer.rb +23 -0
  855. data/lib/pubid/xsf/urn_generator.rb +13 -0
  856. data/lib/pubid/xsf/urn_parser.rb +18 -0
  857. data/lib/pubid/xsf.rb +81 -0
  858. data/lib/pubid.rb +551 -13
  859. data/lib/tasks/docs.rake +37 -0
  860. data/lib/tasks/export.rake +39 -0
  861. data/lib/tasks/website-data.json +7546 -0
  862. metadata +875 -171
  863. data/lib/pubid/registry.rb +0 -30
@@ -0,0 +1,1495 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Bsi
5
+ class Builder < Pubid::Builder::Base
6
+ # Default typed stage used when an abbreviation lookup yields no match.
7
+ # Empty/unknown abbreviations fall back to the published British Standard.
8
+ DEFAULT_TYPED_STAGE = Pubid::Components::TypedStage.new(
9
+ code: :pubbs,
10
+ stage_code: :published,
11
+ type_code: :bs,
12
+ abbr: ["BS"],
13
+ name: "British Standard",
14
+ harmonized_stages: %w[60.00 60.60],
15
+ ).freeze
16
+
17
+ def self.build(parsed_data)
18
+ new.build(parsed_data)
19
+ end
20
+
21
+ def build(data)
22
+ data = flatten_array(data) if data.is_a?(Array)
23
+
24
+ # Store original data to check if BSI prefix was present
25
+ @original_data = data.dup
26
+
27
+ # Check for StandaloneAmendment first (very specific pattern)
28
+ if data[:standalone_amendment] || data[:parenthesized_amd]
29
+ return build_standalone_amendment(data)
30
+ end
31
+
32
+ # Check for CommitteeDocument
33
+ if data[:committee_document]
34
+ return build_committee_document(data[:committee_document])
35
+ end
36
+
37
+ # Check for Index identifier first
38
+ if data[:index_identifier]
39
+ return build_index(data[:index_identifier])
40
+ end
41
+
42
+ # Check for Supplementary Index identifier
43
+ if data[:supplementary_index_identifier]
44
+ return build_supplementary_index(data[:supplementary_index_identifier])
45
+ end
46
+
47
+ # Check for Explanatory Supplement identifier
48
+ if data[:explanatory_supplement_identifier]
49
+ return build_explanatory_supplement(data[:explanatory_supplement_identifier])
50
+ end
51
+
52
+ # Check for Method identifier
53
+ if data[:method_identifier]
54
+ return build_method(data[:method_identifier])
55
+ end
56
+
57
+ # Check for Test Method identifier
58
+ if data[:test_method_identifier]
59
+ return build_test_method(data[:test_method_identifier])
60
+ end
61
+
62
+ # Check for Section identifier
63
+ if data[:section_identifier]
64
+ return build_section(data[:section_identifier])
65
+ end
66
+
67
+ # Check for Detailed Specification identifier
68
+ if data[:detailed_specification]
69
+ return build_detailed_specification(data[:detailed_specification])
70
+ end
71
+
72
+ # Check for DISC identifier
73
+ if data[:disc_identifier]
74
+ return build_disc(data[:disc_identifier])
75
+ end
76
+
77
+ # Check for Aerospace identifier with letter suffix edition
78
+ if data[:aerospace_identifier]
79
+ return build_aerospace_identifier(data[:aerospace_identifier])
80
+ end
81
+
82
+ # Check for SupplementDocument first
83
+ if data[:supplement_document]
84
+ return build_supplement_document(data[:supplement_document])
85
+ end
86
+
87
+ # Check for AddendumDocument
88
+ if data[:addendum_document]
89
+ return build_addendum_document(data[:addendum_document])
90
+ end
91
+
92
+ # Check for BundledIdentifier
93
+ if data[:bundled_parts] || data[:bundled_list]
94
+ return build_bundled_identifier(data)
95
+ end
96
+
97
+ # Check for Set identifier
98
+ if data[:set]
99
+ return build_set(data[:set])
100
+ end
101
+
102
+ # Extract supplements before processing
103
+ supplements_data = extract_supplements(data)
104
+
105
+ # Check for Value-Added Publication wrapper first
106
+ if data[:pdf_format] || data[:tc_format] || data[:book_format]
107
+ return build_value_added_publication(data, supplements_data)
108
+ end
109
+
110
+ # Check for National Annex first (most specific)
111
+ # NationalAnnex can have:
112
+ # - Own supplements: "NA+A1:2012 to BASE"
113
+ # - Adopted string: "NA to BS EN 1234"
114
+ if data[:na_prefix]
115
+ return build_national_annex(data, supplements_data)
116
+ elsif data[:adopted_string]
117
+ # Adopted identifiers carry their supplements and Expert-commentary
118
+ # suffix *inside* the adopted string, so build_adopted_identifier does
119
+ # all the wrapping. Wrapping again here would double-wrap (a second
120
+ # ExpertCommentary around the first) and break #base_document.
121
+ return build_adopted_identifier(data)
122
+ end
123
+
124
+ # Determine identifier class using the module's lookup helpers
125
+ identifier = locate_identifier_klass(data).new
126
+ assign_attributes(identifier, data)
127
+
128
+ # Wrap with consolidated if supplements present
129
+ if supplements_data.any?
130
+ identifier = wrap_with_consolidated(identifier,
131
+ supplements_data)
132
+ end
133
+
134
+ # Wrap with ExpertCommentary if needed (full "Expert Commentary" form
135
+ # arrives as the standalone :expert_commentary_full token)
136
+ if data[:expert_commentary] || data[:expert_commentary_full]
137
+ identifier = wrap_with_expert_commentary(identifier)
138
+ end
139
+
140
+ identifier
141
+ end
142
+
143
+ private
144
+
145
+ def build_index(data)
146
+ # Extract values from the parsed data
147
+ data[:publisher]&.to_s
148
+ number_val = data[:number][:number] if data[:number].is_a?(Hash)
149
+ number_val ||= data[:number].to_s if data[:number]
150
+ year_val = data[:year].to_i if data[:year]
151
+
152
+ # Extract index_suffix information
153
+ index_suffix_data = data[:index_suffix]
154
+ format = "space" # default
155
+ issue_number = nil
156
+
157
+ if index_suffix_data.is_a?(Hash)
158
+ if index_suffix_data[:colon_sep]
159
+ format = "colon"
160
+ elsif index_suffix_data[:issue_number]
161
+ issue_number = index_suffix_data[:issue_number].to_s
162
+ end
163
+ end
164
+
165
+ # Build attributes hash (conditional arguments must be handled separately)
166
+ attrs = {
167
+ number: number_val,
168
+ issue_number: issue_number,
169
+ index_format: format,
170
+ }
171
+ attrs[:date] = Components::Date.new(year: year_val) if year_val
172
+
173
+ Identifiers::Index.new(attrs)
174
+ end
175
+
176
+ def build_supplementary_index(data)
177
+ # Extract values from the parsed data
178
+ number_val = data[:number][:number] if data[:number].is_a?(Hash)
179
+ number_val ||= data[:number].to_s if data[:number]
180
+ year_val = data[:year].to_i if data[:year]
181
+
182
+ # Build attributes hash
183
+ attrs = {
184
+ number: number_val,
185
+ }
186
+ attrs[:date] = Components::Date.new(year: year_val) if year_val
187
+
188
+ Identifiers::SupplementaryIndex.new(attrs)
189
+ end
190
+
191
+ def build_explanatory_supplement(data)
192
+ # Extract values from the parsed data
193
+ number_val = data[:number][:number] if data[:number].is_a?(Hash)
194
+ number_val ||= data[:number].to_s if data[:number]
195
+ year_val = data[:year].to_i if data[:year]
196
+
197
+ # Extract part from parts array (e.g., [{part: "1"}] -> "1")
198
+ part_val = nil
199
+ if data[:parts].is_a?(Array) && !data[:parts].empty?
200
+ # Parts come as [{part: "1"}], so we need to extract the :part key
201
+ first_part = data[:parts][0]
202
+ if first_part.is_a?(Hash) && first_part[:part]
203
+ part_val = first_part[:part].to_s
204
+ end
205
+ end
206
+
207
+ # Build attributes hash
208
+ attrs = {
209
+ number: number_val,
210
+ }
211
+ attrs[:part] = part_val if part_val
212
+ attrs[:date] = Components::Date.new(year: year_val) if year_val
213
+
214
+ Identifiers::ExplanatorySupplement.new(attrs)
215
+ end
216
+
217
+ def build_method(data)
218
+ # Extract values from the parsed data
219
+ data[:publisher]&.to_s
220
+ number_val = data[:number][:number] if data[:number].is_a?(Hash)
221
+ number_val ||= data[:number].to_s if data[:number]
222
+ year_val = data[:year].to_i if data[:year]
223
+
224
+ # Extract part from parts array (e.g., [{part: "1"}] -> "1")
225
+ part_val = nil
226
+ if data[:parts].is_a?(Array) && !data[:parts].empty?
227
+ # Parts come as [{part: "1"}], so we need to extract the :part key
228
+ first_part = data[:parts][0]
229
+ if first_part.is_a?(Hash) && first_part[:part]
230
+ part_val = first_part[:part].to_s
231
+ end
232
+ end
233
+
234
+ # Extract method_suffix information
235
+ method_suffix_data = data[:method_suffix]
236
+ method_code = nil
237
+ method_to = nil
238
+ method_and = nil
239
+ is_plural = false
240
+
241
+ if method_suffix_data.is_a?(Hash)
242
+ method_code = method_suffix_data[:method_code].to_s if method_suffix_data[:method_code]
243
+ method_to = method_suffix_data[:method_to].to_s if method_suffix_data[:method_to]
244
+ method_and = method_suffix_data[:method_and].to_s if method_suffix_data[:method_and]
245
+ # is_plural is true when we have method_to or method_and
246
+ is_plural = !method_to.nil? || !method_and.nil?
247
+ end
248
+
249
+ # Build attributes hash
250
+ attrs = {
251
+ number: number_val,
252
+ method_code: method_code,
253
+ method_to: method_to,
254
+ method_and: method_and,
255
+ is_plural: is_plural,
256
+ }
257
+ attrs[:part] = part_val if part_val
258
+ attrs[:date] = Components::Date.new(year: year_val) if year_val
259
+
260
+ Identifiers::Method.new(attrs)
261
+ end
262
+
263
+ def build_test_method(data)
264
+ # Extract values from the parsed data
265
+ # Format: BS {number}:{test_series}:{test_id}:{year}
266
+ publisher_val = data[:publisher].to_s if data[:publisher]
267
+ number_val = data[:number][:number] if data[:number].is_a?(Hash)
268
+ number_val ||= data[:number].to_s if data[:number]
269
+ year_val = data[:year].to_i if data[:year]
270
+
271
+ # Extract test_method_suffix information
272
+ test_method_suffix_data = data[:test_method_suffix]
273
+ test_series = nil
274
+ test_id = nil
275
+
276
+ if test_method_suffix_data.is_a?(Hash)
277
+ test_series = test_method_suffix_data[:test_series].to_s if test_method_suffix_data[:test_series]
278
+ test_id = test_method_suffix_data[:test_id].to_s if test_method_suffix_data[:test_id]
279
+ end
280
+
281
+ # Build attributes hash
282
+ attrs = {
283
+ number: number_val,
284
+ test_series: test_series,
285
+ test_id: test_id,
286
+ }
287
+ if publisher_val
288
+ attrs[:publisher] =
289
+ Components::Publisher.new(body: publisher_val)
290
+ end
291
+ attrs[:date] = Components::Date.new(year: year_val) if year_val
292
+
293
+ Identifiers::TestMethod.new(attrs)
294
+ end
295
+
296
+ def build_section(data)
297
+ # Extract values from the parsed data
298
+ # Publisher can come from :publisher (BS) or :type (DD, PD, etc.)
299
+ publisher_val = data[:publisher]&.to_s || data[:type]&.to_s
300
+ number_val = data[:number][:number] if data[:number].is_a?(Hash)
301
+ number_val ||= data[:number].to_s if data[:number]
302
+ year_val = data[:year].to_i if data[:year]
303
+
304
+ # Extract section_suffix information
305
+ section_suffix_data = data[:section_suffix]
306
+ section_id = nil
307
+ format = "space" # default
308
+
309
+ if section_suffix_data.is_a?(Hash)
310
+ section_id = section_suffix_data[:section_id].to_s if section_suffix_data[:section_id]
311
+ format = "colon" if section_suffix_data[:colon_sep]
312
+ end
313
+
314
+ # Build attributes hash (conditional arguments must be handled separately)
315
+ attrs = {
316
+ number: number_val,
317
+ section_id: section_id,
318
+ section_format: format,
319
+ }
320
+ if publisher_val
321
+ attrs[:publisher] =
322
+ Components::Publisher.new(body: publisher_val)
323
+ end
324
+ attrs[:date] = Components::Date.new(year: year_val) if year_val
325
+
326
+ Identifiers::Section.new(attrs)
327
+ end
328
+
329
+ def build_detailed_specification(data)
330
+ # Extract values from the parsed data
331
+ # Format: "BS {number} N{code}:year" or "BS {number} C{range}:year"
332
+ publisher_val = data[:publisher].to_s if data[:publisher]
333
+ number_val = data[:number][:number] if data[:number].is_a?(Hash)
334
+ number_val ||= data[:number].to_s if data[:number]
335
+ year_val = data[:year].to_i if data[:year]
336
+
337
+ # Extract detailed_spec_suffix information
338
+ detailed_spec_data = data[:detailed_spec_suffix]
339
+ spec_code = nil
340
+ if detailed_spec_data.is_a?(Hash) && detailed_spec_data[:spec_code]
341
+ spec_code = detailed_spec_data[:spec_code].to_s
342
+ end
343
+
344
+ # Build attributes hash
345
+ attrs = {
346
+ number: number_val,
347
+ }
348
+ attrs[:spec_code] = Components::Code.new(value: spec_code) if spec_code
349
+ if publisher_val
350
+ attrs[:publisher] =
351
+ Components::Publisher.new(body: publisher_val)
352
+ end
353
+ attrs[:date] = Components::Date.new(year: year_val) if year_val
354
+
355
+ Identifiers::DetailedSpecification.new(attrs)
356
+ end
357
+
358
+ def build_disc(data)
359
+ # Extract values from the parsed data
360
+ # DISC format: "DISC PD {number}[-{part}]:{year}"
361
+ number_val = data[:number][:number] if data[:number].is_a?(Hash)
362
+ number_val ||= data[:number].to_s if data[:number]
363
+ year_val = data[:year].to_i if data[:year]
364
+
365
+ # Extract part from parts array (if present)
366
+ part_val = nil
367
+ if data[:parts]&.any?
368
+ parts_array = Array(data[:parts])
369
+ if parts_array.first && parts_array.first[:part]
370
+ part_val = parts_array.first[:part].to_s
371
+ end
372
+ end
373
+
374
+ # Build attributes hash
375
+ attrs = {
376
+ number: number_val,
377
+ }
378
+ attrs[:part] = part_val if part_val
379
+ attrs[:publisher] = Components::Publisher.new(body: "DISC")
380
+ attrs[:date] = Components::Date.new(year: year_val) if year_val
381
+
382
+ Identifiers::Disc.new(attrs)
383
+ end
384
+
385
+ def build_aerospace_identifier(data)
386
+ # Extract values from the parsed data
387
+ # Format: "BS AU {number}{letter_edition}[-{part}{letter_edition}]:{year}"
388
+ prefix_val = data[:prefix][:prefix] if data[:prefix].is_a?(Hash)
389
+ prefix_val ||= data[:prefix].to_s if data[:prefix]
390
+
391
+ # Extract number and letter edition
392
+ number_val = data[:number].to_s if data[:number]
393
+
394
+ # Letter edition is a sibling of number in the AST
395
+ edition_val = nil
396
+ if data[:letter_edition].is_a?(Hash)
397
+ edition_val = data[:letter_edition][:letter_edition].to_s
398
+ end
399
+
400
+ # Extract part directly (from :part key) or from :part_with_letter_edition
401
+ part_val = nil
402
+ part_edition_val = nil
403
+
404
+ # First check for direct :part key (when part_with_letter_edition is used in parser)
405
+ if data[:part]
406
+ if data[:part].is_a?(Hash)
407
+ part_val = data[:part][:part].to_s if data[:part][:part]
408
+ part_edition_val = data[:part][:letter_edition].to_s if data[:part][:letter_edition]
409
+ else
410
+ part_val = data[:part].to_s
411
+ end
412
+ end
413
+
414
+ # Also check for :part_with_letter_edition key (for alternative patterns)
415
+ if !part_val && data[:part_with_letter_edition].is_a?(Hash)
416
+ part_val = data[:part_with_letter_edition][:part].to_s if data[:part_with_letter_edition][:part]
417
+ part_edition_val = data[:part_with_letter_edition][:letter_edition].to_s if data[:part_with_letter_edition][:letter_edition]
418
+ end
419
+
420
+ # Extract iteration (optional, may be empty)
421
+ iteration_val = nil
422
+ if data[:iteration] && data[:iteration][:iteration] && !data[:iteration][:iteration].empty?
423
+ iteration_val = data[:iteration][:iteration].to_s
424
+ end
425
+
426
+ # Extract year
427
+ year_val = data[:year].to_i if data[:year]
428
+
429
+ # Use part edition if present, otherwise use number edition
430
+ final_edition = part_edition_val || edition_val
431
+
432
+ # Build attributes hash
433
+ attrs = {
434
+ prefix: prefix_val,
435
+ number: number_val,
436
+ }
437
+ attrs[:part] = part_val if part_val
438
+ attrs[:iteration] = iteration_val if iteration_val
439
+ attrs[:edition] = final_edition if final_edition
440
+ attrs[:publisher] = Components::Publisher.new(body: "BS")
441
+ attrs[:date] = Components::Date.new(year: year_val) if year_val
442
+
443
+ Identifiers::AerospaceStandard.new(attrs)
444
+ end
445
+
446
+ def build_bundled_identifier(data)
447
+ if data[:bundled_parts]
448
+ # Parts/Sections format: "BS 4048:Parts 1 and 2:1966"
449
+ parts_data = data[:bundled_parts]
450
+ base_number = parts_data[:number].to_s
451
+ bundle_type = parts_data[:bundle_type].to_s
452
+ part1 = parts_data[:part1].to_s
453
+ part2 = parts_data[:part2].to_s
454
+ year_val = parts_data[:year].to_i if parts_data[:year]
455
+
456
+ # Build base identifier
457
+ base_id = SingleIdentifier.new(
458
+ publisher: Components::Publisher.new(body: "BS"),
459
+ number: base_number,
460
+ )
461
+
462
+ # Build identifiers for each part
463
+ id1 = SingleIdentifier.new(
464
+ publisher: Components::Publisher.new(body: "BS"),
465
+ number: base_number,
466
+ part: part1,
467
+ )
468
+
469
+ id2 = SingleIdentifier.new(
470
+ publisher: Components::Publisher.new(body: "BS"),
471
+ number: base_number,
472
+ part: part2,
473
+ )
474
+
475
+ Identifiers::BundledIdentifier.new(
476
+ identifiers: [base_id, id1, id2],
477
+ bundle_type: bundle_type,
478
+ common_year: year_val ? Components::Date.new(year: year_val) : nil,
479
+ )
480
+
481
+ elsif data[:bundled_list]
482
+ # List format: "BS SP 10 & 11:1949" or "BS 2SP 68 to BS 2SP 71:1973"
483
+ # bundled_list is an ARRAY of hashes
484
+ list_array = data[:bundled_list]
485
+
486
+ # First element has publisher/prefix/bundle_item
487
+ first_elem = list_array[0]
488
+ publisher_val = first_elem[:publisher]&.to_s || "BS"
489
+ prefix_val = first_elem[:prefix]&.to_s
490
+
491
+ items = []
492
+ separators = []
493
+
494
+ # Process first item
495
+ if first_elem[:bundle_item]
496
+ items << build_bundle_item(first_elem[:bundle_item], publisher_val,
497
+ prefix_val)
498
+ end
499
+
500
+ # Process remaining elements (may be separator+item or just year)
501
+ list_array[1..].each do |elem|
502
+ if elem[:year]
503
+ # Year element - will be extracted separately
504
+ next
505
+ elsif elem[:bundle_item]
506
+ # This element has separator + bundle_item
507
+ # Preserve separator case for "TO" vs "to"
508
+ sep = if elem[:sep_and]
509
+ " and "
510
+ elsif elem[:sep_to]
511
+ # Check if separator includes uppercase TO
512
+ to_case = elem[:sep_to][:to_case]&.to_s
513
+ to_case == "TO" ? " TO " : " to "
514
+ elsif elem[:sep_ampersand]
515
+ " & "
516
+ elsif elem[:sep_semicolon]
517
+ "; "
518
+ elsif elem[:sep_comma]
519
+ ","
520
+ else
521
+ " and "
522
+ end
523
+ separators << sep
524
+ items << build_bundle_item(elem[:bundle_item], publisher_val,
525
+ prefix_val)
526
+ end
527
+ end
528
+
529
+ # Extract year from last element if present
530
+ year_elem = list_array.find { |e| e[:year] }
531
+ year_val = year_elem[:year].to_i if year_elem
532
+
533
+ Identifiers::BundledIdentifier.new(
534
+ identifiers: items,
535
+ separators: separators,
536
+ common_year: year_val ? Components::Date.new(year: year_val) : nil,
537
+ )
538
+ end
539
+ end
540
+
541
+ def build_set(data)
542
+ # data is a hash with :set key containing an array of set items
543
+ # The parser returns: {set: [{set_item: {...}}, {set_item: {...}}, ...]}
544
+ # Extract the array from the hash
545
+ items_array = data[:set] || data
546
+ items_array = [items_array] unless items_array.is_a?(Array)
547
+
548
+ identifiers = []
549
+
550
+ items_array.each_with_index do |item, _i|
551
+ # Extract the actual item data from :set_item key
552
+ item_data = item[:set_item] || item
553
+
554
+ # Build string identifier for recursive parsing
555
+ # Note: item_data values are Parslet::Slice objects, need to convert to string
556
+ id_str = ""
557
+
558
+ # Publisher (BS)
559
+ if item_data[:publisher]
560
+ id_str += "#{item_data[:publisher]} "
561
+ end
562
+
563
+ # Adopted org (ISO, IEC, etc.)
564
+ if item_data[:adopted_org]
565
+ id_str += "#{item_data[:adopted_org]} "
566
+ end
567
+
568
+ # Number
569
+ if item_data[:number]
570
+ number_val = if item_data[:number].is_a?(Hash)
571
+ item_data[:number][:number]&.to_s
572
+ else
573
+ item_data[:number].to_s
574
+ end
575
+ id_str += number_val.to_s if number_val
576
+ end
577
+
578
+ # Add part if present (parts is an array from the parser)
579
+ if item_data[:parts].is_a?(Hash) && item_data[:parts][:parts]
580
+ parts_array = item_data[:parts][:parts]
581
+ if parts_array.is_a?(Array) && parts_array.any?
582
+ part_val = parts_array.first[:part]
583
+ id_str += "-#{part_val}"
584
+ end
585
+ end
586
+
587
+ # Add year if present
588
+ if item_data[:year]
589
+ id_str += ":#{item_data[:year]}"
590
+ end
591
+
592
+ # Skip if empty string
593
+ next if id_str.strip.empty?
594
+
595
+ # Recursively parse this identifier
596
+ parsed_id = Pubid::Bsi.parse(id_str)
597
+ identifiers << parsed_id if parsed_id
598
+ end
599
+
600
+ Identifiers::Set.new(
601
+ identifiers: identifiers,
602
+ separators: [" + "] * [0, identifiers.length - 1].max,
603
+ )
604
+ end
605
+
606
+ def build_bundle_item(item_data, default_publisher, default_prefix)
607
+ # Extract values based on what's present
608
+ if item_data.is_a?(Hash)
609
+ # Check if publisher/prefix were EXPLICITLY present in parse
610
+ has_explicit_publisher = item_data.key?(:publisher)
611
+ has_explicit_prefix = item_data.key?(:prefix)
612
+
613
+ publisher_val = item_data[:publisher]&.to_s || default_publisher
614
+ prefix_val = item_data[:prefix]&.to_s || default_prefix
615
+ number_val = if item_data[:number].is_a?(Hash)
616
+ item_data[:number][:number]&.to_s
617
+ else
618
+ item_data[:number]&.to_s
619
+ end
620
+
621
+ # Handle both dash-separated parts and space-separated parts
622
+ parts_val = item_data[:parts]
623
+ space_separated_part_val = item_data[:part]
624
+ else
625
+ # Simple string (alphanumeric like "N001" or just number)
626
+ has_explicit_publisher = false
627
+ has_explicit_prefix = false
628
+ publisher_val = default_publisher
629
+ prefix_val = default_prefix
630
+ number_val = item_data.to_s
631
+ parts_val = nil
632
+ space_separated_part_val = nil
633
+ end
634
+
635
+ id = SingleIdentifier.new(
636
+ publisher: Components::Publisher.new(body: publisher_val),
637
+ )
638
+ id.prefix = prefix_val if prefix_val && !prefix_val.empty?
639
+ id.number = number_val if number_val
640
+
641
+ id.explicit_prefix = has_explicit_publisher || has_explicit_prefix
642
+ id.explicit_publisher = has_explicit_publisher
643
+
644
+ # Handle dash-separated parts (from parts array)
645
+ if parts_val.is_a?(Hash) && parts_val[:parts].is_a?(Array)
646
+ parts_array = parts_val[:parts]
647
+ if parts_array.any?
648
+ part_str = parts_array.first[:part].to_s
649
+ id.part = part_str
650
+ end
651
+ # Handle space-separated part (direct part attribute)
652
+ elsif space_separated_part_val
653
+ id.part = space_separated_part_val.to_s
654
+ # Mark this part as space-separated for rendering
655
+ id.space_separated_part = true
656
+ end
657
+
658
+ id
659
+ end
660
+
661
+ def wrap_with_expert_commentary(base_id)
662
+ # Determine format from original data
663
+ # Check for expert_commentary_full first (highest priority)
664
+ format = if @original_data[:expert_commentary_full]
665
+ "full"
666
+ elsif @original_data[:expert_commentary_topic]
667
+ "abbr_with_topic"
668
+ elsif @original_data[:expert_commentary]
669
+ "abbr"
670
+ else
671
+ "abbr" # Default
672
+ end
673
+
674
+ topic = @original_data[:expert_commentary_topic]&.to_s
675
+
676
+ Identifiers::ExpertCommentary.new(
677
+ base: base_id,
678
+ format: format,
679
+ topic: topic,
680
+ )
681
+ end
682
+
683
+ def build_supplement_document(data)
684
+ # Handle nested hash from parser (when using .as with alternatives)
685
+ data = data[:supplement_document] if data[:supplement_document].is_a?(Hash)
686
+
687
+ # Extract values from nested hashes (parser returns {:number => {:number => "1000"}})
688
+ number_val = data[:number][:number] if data[:number].is_a?(Hash)
689
+ iteration_val = data[:iteration][:iteration][0][:iteration] if data[:iteration].is_a?(Hash) && data[:iteration][:iteration].is_a?(Array) && data[:iteration][:iteration].any?
690
+ iteration_val = nil if iteration_val.is_a?(String) && iteration_val.empty?
691
+ parts_val = data[:parts][:parts] if data[:parts].is_a?(Hash)
692
+ flex_prefix_val = data[:flex_prefix].to_s if data[:flex_prefix]
693
+
694
+ # Check if this is reverse format: "Supplement No. N (YEAR) to BS NUMBER:YEAR"
695
+ if data[:supplement_number] && data[:supplement_year] && data[:publisher] && number_val && data[:base_year]
696
+ # Reverse format
697
+ reverse_format = true
698
+ base_year = data[:base_year]
699
+ else
700
+ reverse_format = false
701
+ base_year = data[:year]
702
+ end
703
+
704
+ # Build base identifier
705
+ base_data = {
706
+ publisher: data[:publisher],
707
+ number: number_val,
708
+ iteration: iteration_val,
709
+ parts: parts_val,
710
+ flex_prefix: flex_prefix_val,
711
+ year: base_year,
712
+ }
713
+
714
+ base_id = build(base_data)
715
+
716
+ # Determine supplement type (with or without "No.")
717
+ # Check if supp_no_prefix is "No." string or if it's nil/absent
718
+ supplement_type = if data[:supp_no_prefix] == "No."
719
+ "No."
720
+ elsif data[:supp_no_prefix]
721
+ data[:supp_no_prefix].to_s
722
+ else
723
+ ""
724
+ end
725
+
726
+ Identifiers::SupplementDocument.new(
727
+ base: base_id,
728
+ supplement_number: data[:supplement_number].to_s,
729
+ supplement_year: data[:supplement_year].to_i,
730
+ supplement_type: supplement_type,
731
+ reverse_format: reverse_format,
732
+ separator: data[:supp_sep].to_s,
733
+ )
734
+ end
735
+
736
+ def build_addendum_document(data)
737
+ # Extract values from nested hashes (parser returns {:number => {:number => "1000"}})
738
+ number_val = data[:number][:number] if data[:number].is_a?(Hash)
739
+ iteration_val = data[:iteration][:iteration][0][:iteration] if data[:iteration].is_a?(Hash) && data[:iteration][:iteration].is_a?(Array) && data[:iteration][:iteration].any?
740
+ iteration_val = nil if iteration_val.is_a?(String) && iteration_val.empty?
741
+ parts_val = data[:parts][:parts] if data[:parts].is_a?(Hash)
742
+ flex_prefix_val = data[:flex_prefix].to_s if data[:flex_prefix]
743
+
744
+ # Build base identifier
745
+ base_data = {
746
+ publisher: data[:publisher],
747
+ number: number_val,
748
+ iteration: iteration_val,
749
+ parts: parts_val,
750
+ flex_prefix: flex_prefix_val,
751
+ year: data[:base_year],
752
+ }
753
+
754
+ base_id = build(base_data)
755
+
756
+ # Determine addendum type (with or without "No.")
757
+ addendum_type = if data[:add_no_prefix] == "No."
758
+ "No."
759
+ elsif data[:add_no_prefix]
760
+ data[:add_no_prefix].to_s
761
+ else
762
+ ""
763
+ end
764
+
765
+ # Determine separator - use colon when add_sep is nil and base_year is present
766
+ add_sep = data[:add_sep]
767
+ if add_sep.nil? && data[:base_year]
768
+ add_sep = ":"
769
+ elsif add_sep.nil?
770
+ add_sep = ":"
771
+ end
772
+
773
+ Identifiers::AddendumDocument.new(
774
+ base: base_id,
775
+ addendum_number: data[:addendum_number].to_s,
776
+ addendum_year: data[:addendum_year].to_i,
777
+ addendum_type: addendum_type,
778
+ separator: add_sep.to_s,
779
+ )
780
+ end
781
+
782
+ def build_standalone_amendment(data)
783
+ # Extract from either standalone_amendment or parenthesized_amd key
784
+ amd_data = data[:standalone_amendment] || data[:parenthesized_amd]
785
+
786
+ # Handle nested hash if needed
787
+ amd_data = amd_data[:standalone_amendment] || amd_data[:parenthesized_amd] if amd_data.is_a?(Hash) && (amd_data[:standalone_amendment] || amd_data[:parenthesized_amd])
788
+
789
+ amendment_number = amd_data[:amendment_number].to_s if amd_data[:amendment_number]
790
+ corrigendum = !amd_data[:corrigendum].nil?
791
+ parenthesized = !data[:parenthesized_amd].nil?
792
+
793
+ # NOTE `amd_data[:amendment_number]` above is a PARSE-TREE key and keeps
794
+ # its name; only the attribute moved to `number`.
795
+ attrs = {
796
+ number: Components::Code.new(value: amendment_number),
797
+ corrigendum: corrigendum,
798
+ parenthesized: parenthesized,
799
+ }
800
+
801
+ Identifiers::StandaloneAmendment.new(attrs)
802
+ end
803
+
804
+ def build_committee_document(data)
805
+ # Extract values from the parsed data
806
+ # Format: "YY/NNNNNNNN DC"
807
+ year_val = data[:year].to_s if data[:year]
808
+ document_number = data[:document_number].to_s if data[:document_number]
809
+
810
+ # Convert 2-digit year to 4-digit year (assuming 2000s for 00-99)
811
+ # Could also use a more sophisticated algorithm for year conversion
812
+ full_year = year_val ? "20#{year_val}" : nil
813
+
814
+ # NOTE `data[:document_number]` above is a PARSE-TREE key and keeps its
815
+ # name; only the attribute moved to `number`.
816
+ attrs = {
817
+ number: Components::Code.new(value: document_number),
818
+ }
819
+ attrs[:date] = Components::Date.new(year: full_year.to_i) if full_year
820
+
821
+ Identifiers::CommitteeDocument.new(attrs)
822
+ end
823
+
824
+ def build_value_added_publication(data, _supplements_data)
825
+ # Determine format type
826
+ format = if data[:pdf_format]
827
+ "PDF"
828
+ elsif data[:tc_format]
829
+ "TC"
830
+ elsif data[:book_format]
831
+ "BOOK"
832
+ end
833
+
834
+ # Remove VAP format flags from data before building base
835
+ base_data = data.dup
836
+ base_data.delete(:pdf_format)
837
+ base_data.delete(:tc_format)
838
+ base_data.delete(:book_format)
839
+
840
+ # Build base identifier (it will handle supplements via supplements_data)
841
+ base_id = build(base_data)
842
+
843
+ Identifiers::ValueAddedPublication.new(
844
+ base: base_id,
845
+ format: format,
846
+ )
847
+ end
848
+
849
+ def build_national_annex(data, _supplements_data)
850
+ # Build base identifier (what comes after "NA to")
851
+ base_data = data.dup
852
+ base_data.delete(:na_prefix)
853
+ base_data.delete(:na_supplements)
854
+ # DON'T delete base supplements! The base identifier can have its own supplements
855
+ # base_data.delete(:supplements) # REMOVED - base needs its supplements
856
+
857
+ # Recursively parse the base identifier (it will handle its own supplements)
858
+ base_id = build(base_data)
859
+
860
+ # Extract NA supplements from the CORRECT path: data[:na_prefix][:na_supplements]
861
+ # Parser nests them under na_prefix, not at top level
862
+ na_supp_raw = if data[:na_prefix].is_a?(Hash) && data[:na_prefix][:na_supplements]
863
+ data[:na_prefix][:na_supplements]
864
+ elsif data[:na_supplements]
865
+ # Fallback to top level if structure differs
866
+ data[:na_supplements]
867
+ else
868
+ nil
869
+ end
870
+
871
+ na_supp_data = if na_supp_raw
872
+ extract_supplements_from_array(Array(na_supp_raw))
873
+ else
874
+ []
875
+ end
876
+
877
+ # Convert NA supplements to Amendment/Corrigendum objects with short year expansion
878
+ na_supps = na_supp_data.map do |supp|
879
+ # Expand short years to full years (15 -> 2015, 18 -> 2018)
880
+ year_val = supp[:year]&.to_s
881
+ if year_val && year_val.length == 2
882
+ year_int = year_val.to_i
883
+ # Assume 20XX for years 00-99
884
+ year_val = (2000 + year_int).to_s
885
+ end
886
+
887
+ if supp[:type] == :amendment
888
+ Identifiers::Amendment.new(
889
+ base: nil, # NA supplements don't wrap base
890
+ amendment_number: supp[:number],
891
+ amendment_year: year_val&.to_i,
892
+ separator: supp[:separator] || "+",
893
+ )
894
+ else
895
+ Identifiers::Corrigendum.new(
896
+ base: nil,
897
+ corrigendum_number: supp[:number],
898
+ corrigendum_year: year_val&.to_i,
899
+ separator: supp[:separator] || "+",
900
+ )
901
+ end
902
+ end
903
+
904
+ Identifiers::NationalAnnex.new(
905
+ na_supplements: na_supps,
906
+ base_doc: base_id,
907
+ )
908
+ end
909
+
910
+ def extract_supplements_from_array(supps_array)
911
+ return [] if supps_array.empty?
912
+
913
+ supps_array.map do |s|
914
+ supp_data = s.is_a?(Hash) && s[:supplement] ? s[:supplement] : s
915
+
916
+ # Determine separator - slash vs plus
917
+ separator = supp_data[:amd_sep_slash] ? "/" : "+"
918
+
919
+ {
920
+ type: supp_data[:amd_number] ? :amendment : :corrigendum,
921
+ number: (supp_data[:amd_number] || supp_data[:cor_number])&.to_s,
922
+ year: (supp_data[:amd_year] || supp_data[:cor_year])&.to_s,
923
+ separator: separator,
924
+ amd_suffix_form: !(supp_data[:amd_sep_plus] || supp_data[:amd_sep_slash]),
925
+ }
926
+ end
927
+ end
928
+
929
+ def locate_identifier_klass(parsed_hash)
930
+ # Special case: Flex documents
931
+ return Identifiers::Flex if parsed_hash[:flex_type]
932
+
933
+ # Special case: National Annex prefix
934
+ return Identifiers::NationalAnnex if parsed_hash[:na_prefix]
935
+
936
+ # Special case: Aerospace/Specialized prefix (BS A, BS AU, BS 2A, etc.)
937
+ return Identifiers::AerospaceStandard if parsed_hash[:prefix]
938
+
939
+ # Special case: Handle adopted identifiers
940
+ # Match "EN " followed by digit (not "EN ISO" or "EN IEC")
941
+ return Identifiers::AdoptedEuropeanNorm if parsed_hash[:adopted_string]&.match?(/^EN\s+\d/)
942
+ return Identifiers::AdoptedInternationalStandard if parsed_hash[:adopted_string]
943
+
944
+ # Use type to determine class via Pubid::Bsi.locate_stage / locate_type.
945
+ # Unknown abbreviations fall back to DEFAULT_TYPED_STAGE (published BS).
946
+ type_str = parsed_hash[:type] || parsed_hash[:stage] || ""
947
+ typed_stage = Bsi.locate_stage(type_str.to_s.upcase) || DEFAULT_TYPED_STAGE
948
+ Bsi.locate_type(typed_stage.type_code) || Identifiers::BritishStandard
949
+ end
950
+
951
+ def cast(type, value)
952
+ case type
953
+ when :type
954
+ # Lookup from register via Pubid::Bsi.locate_stage.
955
+ # Unknown abbreviations fall back to DEFAULT_TYPED_STAGE (published BS).
956
+ typed_stage = Bsi.locate_stage(value.to_s.upcase) || DEFAULT_TYPED_STAGE
957
+ {
958
+ stage: typed_stage.to_stage,
959
+ type: typed_stage.to_type,
960
+ typed_stage: typed_stage,
961
+ original_abbr: value.to_s, # Preserve original abbreviation
962
+ }
963
+
964
+ when :publisher
965
+ Components::Publisher.new(body: value.to_s)
966
+
967
+ when :prefix
968
+ # Specialized prefix (A, AU, C, M, 2A, etc.)
969
+ value&.to_s
970
+
971
+ when :flex_prefix
972
+ # Flex type prefix (CECC, E9111, M, etc.)
973
+ value&.to_s
974
+
975
+ when :number
976
+ value.to_s
977
+
978
+ when :parts
979
+ # Extract parts - handle both formats:
980
+ # 1. Multi-level with separate keys: {:part=>"2", :subpart=>"1"}
981
+ # 2. Combined format: {:part=>"2-1"} (split by dash)
982
+ parts_array = Array(value)
983
+ if parts_array.any?
984
+ first_part = parts_array.first
985
+ part_str = first_part[:part].to_s
986
+
987
+ # Check if subpart is already separated (new format from part_with_subpart rule)
988
+ if first_part.key?(:subpart)
989
+ {
990
+ part: part_str,
991
+ subpart: first_part[:subpart].to_s,
992
+ }
993
+ else
994
+ # Old format - split on dash to get part and subpart
995
+ part_components = part_str.split("-")
996
+ result = { part: part_components.first }
997
+ if part_components.length > 1
998
+ result[:subpart] = part_components[1]
999
+ end
1000
+ result
1001
+ end
1002
+ end
1003
+
1004
+ when :part
1005
+ value[:part].to_s if value.is_a?(Hash)
1006
+
1007
+ when :subpart
1008
+ value.to_s
1009
+
1010
+ when :year, :date
1011
+ # Only create date if value is present
1012
+ if value.nil?
1013
+ nil
1014
+ else
1015
+ { date: Components::Date.new(year: value.to_i) }
1016
+ end
1017
+
1018
+ when :month
1019
+ value.to_i
1020
+
1021
+ when :edition
1022
+ value.to_s
1023
+
1024
+ when :flex_type, :na_prefix
1025
+ # Don't cast, used for class selection only
1026
+ nil
1027
+
1028
+ when :adopted_string
1029
+ # Handle both string and nested hash formats
1030
+ # Parser may return: "EN ISO 13485 Expert Commentary" (string)
1031
+ # or: {:adopted_string_content=>" EN ISO 13485 Expert Commentary"} (hash)
1032
+ if value.is_a?(Hash) && value[:adopted_string_content]
1033
+ value[:adopted_string_content].to_s.strip
1034
+ else
1035
+ value.to_s.strip
1036
+ end
1037
+
1038
+ when :supplements
1039
+ # Handled separately by extract_supplements
1040
+ nil
1041
+
1042
+ when :expert_commentary
1043
+ # Handle nested hash from parser
1044
+ # Parser returns: {:expert_commentary_topic=>"Fire"} or {:expert_commentary_full=>"Expert Commentary"}
1045
+ if value.is_a?(Hash)
1046
+ if value[:expert_commentary_topic]
1047
+ @original_data[:expert_commentary_topic] =
1048
+ value[:expert_commentary_topic].to_s
1049
+ end
1050
+ if value[:expert_commentary_full]
1051
+ @original_data[:expert_commentary_full] =
1052
+ value[:expert_commentary_full].to_s
1053
+ end
1054
+ end
1055
+ true
1056
+
1057
+ when :expert_commentary_full
1058
+ # Don't cast, used for format detection
1059
+ nil
1060
+
1061
+ when :expert_commentary_topic
1062
+ # Don't cast, used for format detection
1063
+ nil
1064
+
1065
+ when :pdf_format, :tc_format, :book_format
1066
+ # Don't cast, used for VAP wrapper construction
1067
+ nil
1068
+
1069
+ when :translation_lang
1070
+ # Extract just the language name (e.g., "German", "Italian")
1071
+ value.to_s.capitalize
1072
+
1073
+ when :translation_upper
1074
+ # Uppercase translation like "SPANISH" -> "Spanish"
1075
+ value.to_s.capitalize
1076
+
1077
+ when :second_number
1078
+ # For collections like PAS 2035/2030
1079
+ Components::Code.new(value: value.to_s)
1080
+
1081
+ when :iteration
1082
+ # For bracket notation like 1000[9]
1083
+ value.to_s
1084
+
1085
+ when :index_identifier
1086
+ # Don't cast, handled by build_index
1087
+ nil
1088
+
1089
+ when :index_suffix
1090
+ # Don't cast, handled by build_index
1091
+ nil
1092
+
1093
+ when :method_identifier
1094
+ # Don't cast, handled by build_method
1095
+ nil
1096
+
1097
+ when :method_suffix
1098
+ # Don't cast, handled by build_method
1099
+ nil
1100
+
1101
+ when :section_identifier
1102
+ # Don't cast, handled by build_section
1103
+ nil
1104
+
1105
+ when :section_suffix
1106
+ # Don't cast, handled by build_section
1107
+ nil
1108
+
1109
+ when :detailed_specification
1110
+ # Don't cast, handled by build_detailed_specification
1111
+ nil
1112
+
1113
+ when :detailed_spec_suffix
1114
+ # Don't cast, handled by build_detailed_specification
1115
+ nil
1116
+
1117
+ when :spec_code
1118
+ Components::Code.new(value: value.to_s)
1119
+
1120
+ when :amendment_number
1121
+ Components::Code.new(value: value.to_s)
1122
+
1123
+ when :corrigendum
1124
+ !value.nil?
1125
+
1126
+ when :parenthesized_amd
1127
+ # Don't cast, used for format detection
1128
+ nil
1129
+
1130
+ when :standalone_amendment
1131
+ # Don't cast, handled by build_standalone_amendment
1132
+ nil
1133
+
1134
+ else
1135
+ value
1136
+ end
1137
+ end
1138
+
1139
+ def build_adopted_identifier(data)
1140
+ # Extract the actual adopted string value from the parsed data
1141
+ # The parser may produce nested hash: {:adopted_string=>{:adopted_string=>"ISO 37101:2016"}}
1142
+ adopted_str_value = data[:adopted_string]
1143
+ adopted_str = if adopted_str_value.is_a?(Hash)
1144
+ adopted_str_value[:adopted_string] || adopted_str_value[:adopted_string_no_expert]
1145
+ else
1146
+ adopted_str_value
1147
+ end
1148
+ adopted_str = adopted_str.to_s.strip if adopted_str
1149
+
1150
+ return nil unless adopted_str && !adopted_str.empty?
1151
+
1152
+ # Check if this is a bare adopted identifier (no BSI/BS/PD prefix)
1153
+ # If data has no publisher/type/na_prefix/flex_type, it's a bare adopted identifier
1154
+ is_bare = !data[:publisher] && !data[:type] && !data[:na_prefix] && !data[:flex_type] && !data[:stage]
1155
+
1156
+ # Extract ExComm suffix before parsing (it should be preserved in data[:expert_commentary])
1157
+ # Remove it from adopted_string so ISO/IEC parsers don't choke on it
1158
+ # Handle all three formats: "Expert Commentary", "ExComm", "ExComm (Fire)"
1159
+ # Expert-commentary suffix matching is case-insensitive; output is always
1160
+ # canonicalised to "Expert Commentary" / "ExComm".
1161
+ if adopted_str.match?(/expert commentary$/i)
1162
+ adopted_str = adopted_str.sub(/expert commentary$/i, "")
1163
+ data[:expert_commentary_full] = "Expert Commentary"
1164
+ data[:expert_commentary] = true unless data.key?(:expert_commentary)
1165
+ # Update @original_data so wrap_with_expert_commentary can access it
1166
+ @original_data[:expert_commentary_full] = "Expert Commentary"
1167
+ unless @original_data.key?(:expert_commentary)
1168
+ @original_data[:expert_commentary] =
1169
+ true
1170
+ end
1171
+ elsif adopted_str.match?(/excomm \($/i)
1172
+ adopted_str = adopted_str.sub(/excomm \(.*\)$/i, "")
1173
+ data[:expert_commentary_full] = "Expert Commentary"
1174
+ data[:expert_commentary] = true unless data.key?(:expert_commentary)
1175
+ @original_data[:expert_commentary_full] = "Expert Commentary"
1176
+ unless @original_data.key?(:expert_commentary)
1177
+ @original_data[:expert_commentary] =
1178
+ true
1179
+ end
1180
+ elsif adopted_str.match?(/excomm \(/i)
1181
+ # Extract topic from "ExComm (Fire)"
1182
+ topic_match = adopted_str.match(/excomm\s*\(([^)]+)\)/i)
1183
+ adopted_str = adopted_str.sub(/excomm\s*\(.*\)$/i, "")
1184
+ data[:expert_commentary_topic] = topic_match[1] if topic_match
1185
+ data[:expert_commentary] = true unless data.key?(:expert_commentary)
1186
+ if topic_match
1187
+ @original_data[:expert_commentary_topic] =
1188
+ topic_match[1]
1189
+ end
1190
+ unless @original_data.key?(:expert_commentary)
1191
+ @original_data[:expert_commentary] =
1192
+ true
1193
+ end
1194
+ elsif adopted_str.match?(/excomm\s*$/i)
1195
+ # Abbreviated form "ExComm" at the end
1196
+ adopted_str = adopted_str.sub(/excomm\s*$/i, "")
1197
+ data[:expert_commentary] = true unless data.key?(:expert_commentary)
1198
+ unless @original_data.key?(:expert_commentary)
1199
+ @original_data[:expert_commentary] =
1200
+ true
1201
+ end
1202
+ end
1203
+
1204
+ # NEW: Extract translation suffix before parsing
1205
+ # Handle parenthetical format: "(French version)", "(Spanish Translation)"
1206
+ # We need to track whether "version" or "Translation" was present
1207
+ if adopted_str.match?(/\s*\([A-Za-z]+(?:\s+(?:Translation|version))?\)\s*$/)
1208
+ translation_match = adopted_str.match(/\s*\(([A-Za-z]+)(?:\s+(Translation|version))?\)\s*$/)
1209
+ if translation_match
1210
+ data[:translation_lang] = translation_match[1]
1211
+ # Track if "version" or "Translation" suffix was present
1212
+ if translation_match[2]
1213
+ data[:translation_suffix_type] =
1214
+ translation_match[2]
1215
+ end
1216
+ adopted_str = adopted_str.sub(
1217
+ /\s*\([A-Za-z]+(?:\s+(?:Translation|version))?\)\s*$/, ""
1218
+ )
1219
+ end
1220
+ # Handle all-caps format: "FRENCH TRANSLATION"
1221
+ elsif adopted_str.match?(/\s+(SPANISH|FRENCH|GERMAN|ITALIAN)\s+TRANSLATION\s*$/)
1222
+ translation_upper_match = adopted_str.match(/\s+(SPANISH|FRENCH|GERMAN|ITALIAN)\s+TRANSLATION\s*$/)
1223
+ if translation_upper_match
1224
+ data[:translation_upper] = translation_upper_match[1]
1225
+ # Track that "Translation" suffix was present
1226
+ data[:translation_suffix_type] = "Translation"
1227
+ adopted_str = adopted_str.sub(
1228
+ /\s+(SPANISH|FRENCH|GERMAN|ITALIAN)\s+TRANSLATION\s*$/, ""
1229
+ )
1230
+ end
1231
+ end
1232
+
1233
+ # Only extract edition if NOT bare - bare identifiers should preserve edition internally
1234
+ edition_match = is_bare ? nil : adopted_str.match(/\s+ED(\d+)\s*$/)
1235
+ extracted_edition = edition_match ? edition_match[1] : nil
1236
+ # Remove edition from adopted string for recursive parsing (only if extracted)
1237
+ adopted_str_clean = if edition_match
1238
+ adopted_str.sub(/\s+ED\d+\s*$/,
1239
+ "")
1240
+ else
1241
+ adopted_str
1242
+ end
1243
+
1244
+ # Use extracted edition if no edition in data
1245
+ final_edition = data[:edition] || extracted_edition
1246
+
1247
+ # NEW: Extract reaffirmation notation like (R2004) before parsing
1248
+ # This is used for documents that have been reaffirmed
1249
+ # Example: "DD ISO/IEC 11177-1:1995 (R2004)"
1250
+ if adopted_str_clean.match?(/\s+\(R(\d{4})\)\s*$/)
1251
+ reaffirmation_match = adopted_str_clean.match(/\s+\(R(\d{4})\)\s*$/)
1252
+ if reaffirmation_match
1253
+ data[:reaffirmation_year] = reaffirmation_match[1]
1254
+ adopted_str_clean = adopted_str_clean.sub(/\s+\(R\d{4}\)\s*$/, "")
1255
+ end
1256
+ end
1257
+
1258
+ # NEW: Extract BSI-style supplements from adopted_string before delegating to ISO/IEC
1259
+ # BSI uses +A1:2020 format, but ISO/IEC parsers can't handle this
1260
+ # Patterns: +A1:2020, +A11:2021, +AMD1:2001, +C1:2020
1261
+ extracted_supplements = []
1262
+ # Match patterns like: +A1:2020, +A11:2021, +AMD1:2001, +C1:2020, +COR1:2020,
1263
+ # and the year-less compact forms +A2 / +C1 (the ":YYYY" is optional).
1264
+ # Must be at end of string or followed by space (not colon, which is part of date)
1265
+ adopted_str_clean = adopted_str_clean.gsub(/([+])(A(\d+)|AMD(\d+)|C(\d+)|COR(\d+))(?::(\d{4}))?(?:\s|$)/) do |_match|
1266
+ separator = $1 # "+" or "/"
1267
+ type_code = $2 || $3 || $4 || $5 || $6
1268
+ $2 || $3 || $4 || $5 || $6
1269
+ year = $7
1270
+
1271
+ # Determine supplement type
1272
+ supp_type = if type_code.start_with?("A", "AMD")
1273
+ :amendment
1274
+ elsif type_code.start_with?("C", "COR")
1275
+ :corrigendum
1276
+ end
1277
+
1278
+ # Extract number from type code (A1 -> 1, AMD1 -> 1, C1 -> 1)
1279
+ supp_number = if type_code.start_with?("AMD")
1280
+ type_code.sub("AMD", "")
1281
+ else
1282
+ type_code.sub(/^[AC]/, "")
1283
+ end
1284
+
1285
+ extracted_supplements << {
1286
+ type: supp_type,
1287
+ number: supp_number,
1288
+ year: year,
1289
+ separator: separator,
1290
+ # compact "+A"/"/A" join form (not the trailing " AMD5" suffix form)
1291
+ amd_suffix_form: false,
1292
+ }
1293
+
1294
+ "" # Remove from adopted_string
1295
+ end.strip
1296
+
1297
+ # Extract AMD without year - "AMD5" or "AMD AA"
1298
+ # These don't have + or / prefix and don't have a year
1299
+ # Note: \s* (optional whitespace) after AMD to handle "AMD5" vs "AMD 5"
1300
+ adopted_str_clean = adopted_str_clean.gsub(/\s+AMD\s*(AA|\d+)(?:\s|$)/i) do |match|
1301
+ amd_number = match.strip.sub(/^AMD\s*/i, "")
1302
+ extracted_supplements << {
1303
+ type: :amendment,
1304
+ number: amd_number,
1305
+ year: nil,
1306
+ separator: nil, # No separator for AMD without year
1307
+ amd_suffix_form: true, # trailing " AMD5" suffix form
1308
+ }
1309
+ "" # Remove from adopted_string
1310
+ end.strip
1311
+
1312
+ # Determine the BSI prefix to use (BS, PD, DD)
1313
+ bsi_prefix = if data[:type]
1314
+ data[:type].to_s # PD, DD, etc.
1315
+ elsif data[:publisher]
1316
+ data[:publisher].to_s # BS
1317
+ else
1318
+ "BS" # Default
1319
+ end
1320
+
1321
+ # Multi-level adoption hierarchy (check most specific first):
1322
+ # 1. BS EN ISO/IEC (triple-level)
1323
+ # 2. BS ISO/IEC (double-level)
1324
+ # 3. BS EN (double-level)
1325
+ # 4. Bare ISO/IEC/EN (return as-is)
1326
+
1327
+ adopted_id = nil
1328
+
1329
+ # Check for EN ISO or EN IEC patterns (triple-level).
1330
+ # The test must be ANCHORED, because the repair below is: an unanchored
1331
+ # match accepts "CEN ISO/TS 12180-1:2007" (the "EN ISO" is inside
1332
+ # "CEN ISO"), but `sub(/^EN\s+/, "")` then changes nothing, so
1333
+ # adopted_id stayed nil and the whole parse returned nil for 49
1334
+ # "DD CEN ISO/..." identifiers in BSI's own pass fixtures. Anchored,
1335
+ # a "CEN ..." string falls through to the CEN branch below, which is
1336
+ # where it belongs.
1337
+ if adopted_str_clean.match?(/\AEN\s+(ISO\/IEC|IEC|ISO)/)
1338
+ # Parse the ISO/IEC part
1339
+ iso_iec_str = adopted_str_clean.sub(/^EN\s+/, "")
1340
+
1341
+ if iso_iec_str.start_with?("ISO/IEC") || iso_iec_str.include?("ISO/IEC")
1342
+ adopted_id = Pubid::Iso.parse(iso_iec_str)
1343
+ elsif iso_iec_str.start_with?("ISO")
1344
+ adopted_id = Pubid::Iso.parse(iso_iec_str)
1345
+ elsif iso_iec_str.start_with?("IEC")
1346
+ adopted_id = Pubid::Iec.parse(iso_iec_str)
1347
+ end
1348
+
1349
+ # Wrap ISO/IEC identifier in EN adoption
1350
+ if adopted_id
1351
+ adopted_id = Pubid::CenCenelec::Identifiers::AdoptedEuropeanNorm.new(
1352
+ publisher: ["EN"],
1353
+ adopted_identifier: adopted_id,
1354
+ )
1355
+ end
1356
+
1357
+ # Check for direct ISO/IEC patterns (double-level: BS ISO, BS IEC or bare ISO/IEC)
1358
+ elsif adopted_str_clean.start_with?("ISO/IEC") || adopted_str_clean.include?("ISO/IEC")
1359
+ adopted_id = Pubid::Iso.parse(adopted_str_clean)
1360
+ elsif adopted_str_clean.start_with?("ISO")
1361
+ adopted_id = Pubid::Iso.parse(adopted_str_clean)
1362
+ elsif adopted_str_clean.start_with?("IEC")
1363
+ adopted_id = Pubid::Iec.parse(adopted_str_clean)
1364
+
1365
+ # Check for EN patterns (double-level: BS EN or DD/PD CEN) or CEN types
1366
+ elsif adopted_str_clean.start_with?("EN", "CEN", "CLC", "CR", "ES",
1367
+ "ENV", "HD", "CWA")
1368
+ adopted_id = Pubid::CenCenelec.parse(adopted_str_clean)
1369
+ # Check for CISPR
1370
+ elsif adopted_str_clean.start_with?("CISPR")
1371
+ adopted_id = Pubid::Iec.parse(adopted_str_clean)
1372
+ end
1373
+
1374
+ # If this is a bare adopted identifier (no BSI prefix), return it as-is
1375
+ return adopted_id if is_bare && adopted_id
1376
+
1377
+ # Return appropriate wrapper based on adoption type
1378
+ if adopted_id
1379
+ # If adopted_id is a CEN identifier (in Cen module), use AdoptedEuropeanNorm
1380
+ # The Expert-commentary suffix is represented uniformly by an outer
1381
+ # ExpertCommentary wrapper (below), never a boolean on the inner norm,
1382
+ # so that #base_document peels cleanly to the bare standard.
1383
+ identifier = if adopted_id.class.name.start_with?("Pubid::CenCenelec::")
1384
+ Identifiers::AdoptedEuropeanNorm.new(
1385
+ publisher: Components::Publisher.new(body: bsi_prefix),
1386
+ adopted_identifier: adopted_id,
1387
+ edition: final_edition&.to_s,
1388
+ translation_lang: data[:translation_lang]&.to_s,
1389
+ translation_upper: data[:translation_upper]&.to_s,
1390
+ # Pass translation_suffix_type for rendering
1391
+ translation_suffix_type: data[:translation_suffix_type]&.to_s,
1392
+ # Pass reaffirmation_year for rendering
1393
+ reaffirmation_year: data[:reaffirmation_year]&.to_s,
1394
+ )
1395
+ else
1396
+ # Otherwise it's ISO/IEC, use AdoptedInternationalStandard
1397
+ Identifiers::AdoptedInternationalStandard.new(
1398
+ publisher: Components::Publisher.new(body: bsi_prefix),
1399
+ adopted_identifier: adopted_id,
1400
+ edition: final_edition&.to_s,
1401
+ translation_lang: data[:translation_lang]&.to_s,
1402
+ translation_upper: data[:translation_upper]&.to_s,
1403
+ # Pass translation_suffix_type for rendering
1404
+ translation_suffix_type: data[:translation_suffix_type]&.to_s,
1405
+ # Pass reaffirmation_year for rendering
1406
+ reaffirmation_year: data[:reaffirmation_year]&.to_s,
1407
+ )
1408
+ end
1409
+
1410
+ # Wrap supplements first (they render before the ExComm suffix), then
1411
+ # wrap the whole thing in a single ExpertCommentary — same shape as the
1412
+ # pure-BSI path, keeping #base_document uniform.
1413
+ if extracted_supplements.any?
1414
+ identifier = wrap_with_consolidated(identifier, extracted_supplements)
1415
+ end
1416
+ if data[:expert_commentary] || data[:expert_commentary_full]
1417
+ identifier = wrap_with_expert_commentary(identifier)
1418
+ end
1419
+
1420
+ identifier
1421
+ end
1422
+ end
1423
+
1424
+ def wrap_with_consolidated(base, supplements_data,
1425
+ expert_commentary: nil, expert_commentary_topic: nil)
1426
+ # If expert_commentary data is provided, set it on the base
1427
+ # This allows ConsolidatedIdentifier to render the ExComm suffix correctly
1428
+ if expert_commentary
1429
+ base_attrs = base.class.attributes
1430
+ if base_attrs.key?(:expert_commentary)
1431
+ base.expert_commentary = expert_commentary
1432
+ end
1433
+ if expert_commentary_topic && base_attrs.key?(:expert_commentary_topic)
1434
+ base.expert_commentary_topic = expert_commentary_topic
1435
+ end
1436
+ end
1437
+
1438
+ supplement_ids = supplements_data.map do |supp|
1439
+ # Expand short years to full years (15 -> 2015, 18 -> 2018)
1440
+ year_val = supp[:year]&.to_s
1441
+ if year_val && year_val.length == 2
1442
+ year_int = year_val.to_i
1443
+ # Assume 20XX for years 00-99
1444
+ year_val = (2000 + year_int).to_s
1445
+ end
1446
+
1447
+ if supp[:type] == :amendment
1448
+ Identifiers::Amendment.new(
1449
+ base: base,
1450
+ amendment_number: supp[:number],
1451
+ amendment_year: year_val&.to_i,
1452
+ separator: supp[:separator] || "+",
1453
+ amd_suffix_form: supp[:amd_suffix_form] ? true : false,
1454
+ )
1455
+ else
1456
+ Identifiers::Corrigendum.new(
1457
+ base: base,
1458
+ corrigendum_number: supp[:number],
1459
+ corrigendum_year: year_val&.to_i,
1460
+ separator: supp[:separator] || "+",
1461
+ )
1462
+ end
1463
+ end
1464
+
1465
+ Identifiers::ConsolidatedIdentifier.new(
1466
+ identifiers: [base] + supplement_ids,
1467
+ )
1468
+ end
1469
+
1470
+ def extract_supplements(data)
1471
+ return [] unless data[:supplements]
1472
+
1473
+ supps_array = data[:supplements]
1474
+ return [] if supps_array.empty?
1475
+
1476
+ supps_array.map do |s|
1477
+ supp_data = s.is_a?(Hash) && s[:supplement] ? s[:supplement] : s
1478
+
1479
+ # Determine separator - slash vs plus
1480
+ separator = supp_data[:amd_sep_slash] ? "/" : "+"
1481
+
1482
+ {
1483
+ type: supp_data[:amd_number] ? :amendment : :corrigendum,
1484
+ number: (supp_data[:amd_number] || supp_data[:cor_number])&.to_s,
1485
+ year: (supp_data[:amd_year] || supp_data[:cor_year])&.to_s,
1486
+ separator: separator,
1487
+ # The compact "+A"/"/A" amendment rule captures an explicit separator;
1488
+ # the trailing " AMD5" suffix form does not.
1489
+ amd_suffix_form: !(supp_data[:amd_sep_plus] || supp_data[:amd_sep_slash]),
1490
+ }
1491
+ end
1492
+ end
1493
+ end
1494
+ end
1495
+ end