pubid 1.15.19 → 2.0.0.pre.alpha.2

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