pubid 1.15.19 → 2.0.0.pre.alpha.1

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 (601) 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 +87 -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 +18 -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 +13 -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 +21 -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 +18 -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 +15 -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 +15 -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 +27 -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 +16 -0
  160. data/lib/pubid/ccsds/identifiers/base.rb +78 -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 +74 -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 +15 -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 +18 -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/language.rb +37 -0
  219. data/lib/pubid/components/locality.rb +10 -0
  220. data/lib/pubid/components/publisher.rb +36 -0
  221. data/lib/pubid/components/stage.rb +54 -0
  222. data/lib/pubid/components/type.rb +58 -0
  223. data/lib/pubid/components/typed_stage.rb +55 -0
  224. data/lib/pubid/components.rb +15 -0
  225. data/lib/pubid/core/pattern_doc_generator.rb +272 -0
  226. data/lib/pubid/core/update_codes.rb +77 -0
  227. data/lib/pubid/core.rb +8 -0
  228. data/lib/pubid/csa/builder.rb +671 -0
  229. data/lib/pubid/csa/components/code.rb +9 -0
  230. data/lib/pubid/csa/components.rb +9 -0
  231. data/lib/pubid/csa/composite_identifier.rb +27 -0
  232. data/lib/pubid/csa/identifier.rb +457 -0
  233. data/lib/pubid/csa/identifiers/base.rb +133 -0
  234. data/lib/pubid/csa/identifiers/bundled.rb +125 -0
  235. data/lib/pubid/csa/identifiers/canadian_adopted.rb +82 -0
  236. data/lib/pubid/csa/identifiers/cec.rb +129 -0
  237. data/lib/pubid/csa/identifiers/combined.rb +130 -0
  238. data/lib/pubid/csa/identifiers/csa_adopted.rb +78 -0
  239. data/lib/pubid/csa/identifiers/package.rb +65 -0
  240. data/lib/pubid/csa/identifiers/series.rb +127 -0
  241. data/lib/pubid/csa/identifiers/standard.rb +10 -0
  242. data/lib/pubid/csa/identifiers.rb +17 -0
  243. data/lib/pubid/csa/parser.rb +445 -0
  244. data/lib/pubid/csa/scheme.rb +44 -0
  245. data/lib/pubid/csa/single_identifier.rb +30 -0
  246. data/lib/pubid/csa/urn_generator.rb +80 -0
  247. data/lib/pubid/csa/wrapper_identifier.rb +31 -0
  248. data/lib/pubid/csa.rb +25 -0
  249. data/lib/pubid/etsi/builder.rb +133 -0
  250. data/lib/pubid/etsi/components/code.rb +42 -0
  251. data/lib/pubid/etsi/components/version.rb +37 -0
  252. data/lib/pubid/etsi/components.rb +10 -0
  253. data/lib/pubid/etsi/identifier.rb +14 -0
  254. data/lib/pubid/etsi/identifiers/amendment.rb +15 -0
  255. data/lib/pubid/etsi/identifiers/base.rb +38 -0
  256. data/lib/pubid/etsi/identifiers/corrigendum.rb +15 -0
  257. data/lib/pubid/etsi/identifiers/etsi_standard.rb +19 -0
  258. data/lib/pubid/etsi/identifiers/supplement_identifier.rb +91 -0
  259. data/lib/pubid/etsi/identifiers.rb +14 -0
  260. data/lib/pubid/etsi/parser.rb +133 -0
  261. data/lib/pubid/etsi/scheme.rb +42 -0
  262. data/lib/pubid/etsi/urn_generator.rb +76 -0
  263. data/lib/pubid/etsi.rb +21 -0
  264. data/lib/pubid/export/auditor.rb +89 -0
  265. data/lib/pubid/export/data_class_exporter.rb +59 -0
  266. data/lib/pubid/export/exporter.rb +74 -0
  267. data/lib/pubid/export/flavor_exporter.rb +402 -0
  268. data/lib/pubid/export/ieee_exporter.rb +78 -0
  269. data/lib/pubid/export/itu_exporter.rb +66 -0
  270. data/lib/pubid/export/nist_exporter.rb +64 -0
  271. data/lib/pubid/export/registry_exporter.rb +90 -0
  272. data/lib/pubid/export/result.rb +97 -0
  273. data/lib/pubid/export/scheme_exporter.rb +70 -0
  274. data/lib/pubid/export.rb +18 -0
  275. data/lib/pubid/format_detector.rb +16 -0
  276. data/lib/pubid/format_registry.rb +42 -0
  277. data/lib/pubid/identifier.rb +235 -0
  278. data/lib/pubid/identifier_metadata.rb +148 -0
  279. data/lib/pubid/identifier_registry.rb +198 -0
  280. data/lib/pubid/idf/builder.rb +82 -0
  281. data/lib/pubid/idf/identifier.rb +69 -0
  282. data/lib/pubid/idf/identifiers/amendment.rb +27 -0
  283. data/lib/pubid/idf/identifiers/corrigendum.rb +27 -0
  284. data/lib/pubid/idf/identifiers/international_standard.rb +123 -0
  285. data/lib/pubid/idf/identifiers/reviewed_method.rb +100 -0
  286. data/lib/pubid/idf/identifiers.rb +13 -0
  287. data/lib/pubid/idf/parser.rb +143 -0
  288. data/lib/pubid/idf/scheme.rb +61 -0
  289. data/lib/pubid/idf/single_identifier.rb +19 -0
  290. data/lib/pubid/idf/supplement_identifier.rb +43 -0
  291. data/lib/pubid/idf/urn_generator.rb +84 -0
  292. data/lib/pubid/idf.rb +25 -0
  293. data/lib/pubid/iec/builder.rb +457 -0
  294. data/lib/pubid/iec/components/code.rb +59 -0
  295. data/lib/pubid/iec/components/consolidated_amendment.rb +59 -0
  296. data/lib/pubid/iec/components/publisher.rb +35 -0
  297. data/lib/pubid/iec/components/sheet.rb +32 -0
  298. data/lib/pubid/iec/components/trf_info.rb +38 -0
  299. data/lib/pubid/iec/components/vap_suffix.rb +41 -0
  300. data/lib/pubid/iec/identifier.rb +21 -0
  301. data/lib/pubid/iec/identifiers/amendment.rb +94 -0
  302. data/lib/pubid/iec/identifiers/base.rb +78 -0
  303. data/lib/pubid/iec/identifiers/component_specification.rb +39 -0
  304. data/lib/pubid/iec/identifiers/conformity_assessment.rb +39 -0
  305. data/lib/pubid/iec/identifiers/consolidated_identifier.rb +86 -0
  306. data/lib/pubid/iec/identifiers/corrigendum.rb +94 -0
  307. data/lib/pubid/iec/identifiers/fragment_identifier.rb +141 -0
  308. data/lib/pubid/iec/identifiers/guide.rb +104 -0
  309. data/lib/pubid/iec/identifiers/international_standard.rb +147 -0
  310. data/lib/pubid/iec/identifiers/interpretation_sheet.rb +104 -0
  311. data/lib/pubid/iec/identifiers/operational_document.rb +39 -0
  312. data/lib/pubid/iec/identifiers/publicly_available_specification.rb +101 -0
  313. data/lib/pubid/iec/identifiers/sheet_identifier.rb +66 -0
  314. data/lib/pubid/iec/identifiers/societal_technology_trend_report.rb +40 -0
  315. data/lib/pubid/iec/identifiers/systems_reference_document.rb +40 -0
  316. data/lib/pubid/iec/identifiers/technical_report.rb +132 -0
  317. data/lib/pubid/iec/identifiers/technical_specification.rb +132 -0
  318. data/lib/pubid/iec/identifiers/technology_report.rb +39 -0
  319. data/lib/pubid/iec/identifiers/test_report_form.rb +78 -0
  320. data/lib/pubid/iec/identifiers/vap_identifier.rb +77 -0
  321. data/lib/pubid/iec/identifiers/white_paper.rb +39 -0
  322. data/lib/pubid/iec/identifiers/working_document.rb +96 -0
  323. data/lib/pubid/iec/parser.rb +412 -0
  324. data/lib/pubid/iec/rendering_style.rb +113 -0
  325. data/lib/pubid/iec/scheme.rb +71 -0
  326. data/lib/pubid/iec/single_identifier.rb +80 -0
  327. data/lib/pubid/iec/supplement_identifier.rb +161 -0
  328. data/lib/pubid/iec/urn_generator.rb +193 -0
  329. data/lib/pubid/iec/urn_parser.rb +289 -0
  330. data/lib/pubid/iec.rb +85 -0
  331. data/lib/pubid/ieee/aiee/builder.rb +71 -0
  332. data/lib/pubid/ieee/aiee/identifier.rb +105 -0
  333. data/lib/pubid/ieee/aiee/parser.rb +130 -0
  334. data/lib/pubid/ieee/aiee.rb +11 -0
  335. data/lib/pubid/ieee/builder.rb +1237 -0
  336. data/lib/pubid/ieee/components/code.rb +102 -0
  337. data/lib/pubid/ieee/components/draft.rb +93 -0
  338. data/lib/pubid/ieee/components/relationship.rb +157 -0
  339. data/lib/pubid/ieee/components/typed_stage.rb +100 -0
  340. data/lib/pubid/ieee/identifier.rb +13 -0
  341. data/lib/pubid/ieee/identifiers/adopted_standard.rb +33 -0
  342. data/lib/pubid/ieee/identifiers/base.rb +591 -0
  343. data/lib/pubid/ieee/identifiers/conformance_identifier.rb +35 -0
  344. data/lib/pubid/ieee/identifiers/corrigendum.rb +37 -0
  345. data/lib/pubid/ieee/identifiers/csa_dual_published.rb +51 -0
  346. data/lib/pubid/ieee/identifiers/dual_identifier.rb +18 -0
  347. data/lib/pubid/ieee/identifiers/dual_published.rb +28 -0
  348. data/lib/pubid/ieee/identifiers/iec_ieee_copublished.rb +27 -0
  349. data/lib/pubid/ieee/identifiers/interpretation_identifier.rb +34 -0
  350. data/lib/pubid/ieee/identifiers/joint_development.rb +172 -0
  351. data/lib/pubid/ieee/identifiers/multi_numbered_identifier.rb +51 -0
  352. data/lib/pubid/ieee/identifiers/nesc/base.rb +56 -0
  353. data/lib/pubid/ieee/identifiers/nesc/draft.rb +28 -0
  354. data/lib/pubid/ieee/identifiers/nesc/handbook.rb +32 -0
  355. data/lib/pubid/ieee/identifiers/nesc/redline.rb +26 -0
  356. data/lib/pubid/ieee/identifiers/nesc/standard.rb +26 -0
  357. data/lib/pubid/ieee/identifiers/nesc.rb +15 -0
  358. data/lib/pubid/ieee/identifiers/parenthetical_identifier.rb +20 -0
  359. data/lib/pubid/ieee/identifiers/project_draft_identifier.rb +26 -0
  360. data/lib/pubid/ieee/identifiers/redlined_standard.rb +33 -0
  361. data/lib/pubid/ieee/identifiers/si_standard.rb +73 -0
  362. data/lib/pubid/ieee/identifiers/standard.rb +41 -0
  363. data/lib/pubid/ieee/identifiers/supplement_identifier.rb +23 -0
  364. data/lib/pubid/ieee/identifiers.rb +33 -0
  365. data/lib/pubid/ieee/ire/builder.rb +61 -0
  366. data/lib/pubid/ieee/ire/identifier.rb +58 -0
  367. data/lib/pubid/ieee/ire/parser.rb +91 -0
  368. data/lib/pubid/ieee/ire.rb +11 -0
  369. data/lib/pubid/ieee/nesc/builder.rb +101 -0
  370. data/lib/pubid/ieee/nesc/parser.rb +154 -0
  371. data/lib/pubid/ieee/nesc.rb +10 -0
  372. data/lib/pubid/ieee/parser.rb +1226 -0
  373. data/lib/pubid/ieee/scheme.rb +90 -0
  374. data/lib/pubid/ieee/typed_stages.rb +172 -0
  375. data/lib/pubid/ieee/urn_generator.rb +188 -0
  376. data/lib/pubid/ieee.rb +32 -0
  377. data/lib/pubid/ieee_debug.rb +31 -0
  378. data/lib/pubid/iho/builder.rb +37 -0
  379. data/lib/pubid/iho/identifier.rb +19 -0
  380. data/lib/pubid/iho/identifiers/base.rb +41 -0
  381. data/lib/pubid/iho/identifiers/bibliographic.rb +20 -0
  382. data/lib/pubid/iho/identifiers/circular_letter.rb +19 -0
  383. data/lib/pubid/iho/identifiers/miscellaneous.rb +20 -0
  384. data/lib/pubid/iho/identifiers/publication.rb +19 -0
  385. data/lib/pubid/iho/identifiers/standard.rb +19 -0
  386. data/lib/pubid/iho/identifiers.rb +14 -0
  387. data/lib/pubid/iho/parser.rb +68 -0
  388. data/lib/pubid/iho/scheme.rb +29 -0
  389. data/lib/pubid/iho/urn_generator.rb +29 -0
  390. data/lib/pubid/iho.rb +21 -0
  391. data/lib/pubid/iso/builder.rb +305 -0
  392. data/lib/pubid/iso/bundled_identifier.rb +85 -0
  393. data/lib/pubid/iso/combined_identifier.rb +22 -0
  394. data/lib/pubid/iso/components/code.rb +36 -0
  395. data/lib/pubid/iso/components/publisher.rb +60 -0
  396. data/lib/pubid/iso/components.rb +12 -0
  397. data/lib/pubid/iso/format_resolver.rb +45 -0
  398. data/lib/pubid/iso/identifier.rb +69 -0
  399. data/lib/pubid/iso/identifiers/addendum.rb +104 -0
  400. data/lib/pubid/iso/identifiers/amendment.rb +128 -0
  401. data/lib/pubid/iso/identifiers/base.rb +115 -0
  402. data/lib/pubid/iso/identifiers/corrigendum.rb +108 -0
  403. data/lib/pubid/iso/identifiers/data.rb +76 -0
  404. data/lib/pubid/iso/identifiers/directives.rb +59 -0
  405. data/lib/pubid/iso/identifiers/directives_supplement.rb +119 -0
  406. data/lib/pubid/iso/identifiers/extract.rb +30 -0
  407. data/lib/pubid/iso/identifiers/guide.rb +100 -0
  408. data/lib/pubid/iso/identifiers/international_standard.rb +168 -0
  409. data/lib/pubid/iso/identifiers/international_standardized_profile.rb +94 -0
  410. data/lib/pubid/iso/identifiers/international_workshop_agreement.rb +89 -0
  411. data/lib/pubid/iso/identifiers/pas.rb +93 -0
  412. data/lib/pubid/iso/identifiers/recommendation.rb +45 -0
  413. data/lib/pubid/iso/identifiers/supplement.rb +87 -0
  414. data/lib/pubid/iso/identifiers/tc_document.rb +108 -0
  415. data/lib/pubid/iso/identifiers/technical_report.rb +103 -0
  416. data/lib/pubid/iso/identifiers/technical_specification.rb +102 -0
  417. data/lib/pubid/iso/identifiers/technology_trends_assessments.rb +95 -0
  418. data/lib/pubid/iso/identifiers.rb +33 -0
  419. data/lib/pubid/iso/parser.rb +510 -0
  420. data/lib/pubid/iso/rendering_style.rb +120 -0
  421. data/lib/pubid/iso/scheme.rb +187 -0
  422. data/lib/pubid/iso/single_identifier.rb +61 -0
  423. data/lib/pubid/iso/supplement_identifier.rb +27 -0
  424. data/lib/pubid/iso/urn_generator.rb +412 -0
  425. data/lib/pubid/iso/urn_parser.rb +423 -0
  426. data/lib/pubid/iso/utilities.rb +86 -0
  427. data/lib/pubid/iso.rb +50 -0
  428. data/lib/pubid/itu/builder.rb +171 -0
  429. data/lib/pubid/itu/components/code.rb +39 -0
  430. data/lib/pubid/itu/components/sector.rb +35 -0
  431. data/lib/pubid/itu/components/series.rb +29 -0
  432. data/lib/pubid/itu/i18n.rb +9 -0
  433. data/lib/pubid/itu/i18n.yaml +30 -0
  434. data/lib/pubid/itu/identifier.rb +53 -0
  435. data/lib/pubid/itu/identifiers/amendment.rb +43 -0
  436. data/lib/pubid/itu/identifiers/annex.rb +74 -0
  437. data/lib/pubid/itu/identifiers/base.rb +154 -0
  438. data/lib/pubid/itu/identifiers/combined_identifier.rb +47 -0
  439. data/lib/pubid/itu/identifiers/corrigendum.rb +44 -0
  440. data/lib/pubid/itu/identifiers/recommendation.rb +16 -0
  441. data/lib/pubid/itu/identifiers/special_publication.rb +31 -0
  442. data/lib/pubid/itu/identifiers/supplement.rb +46 -0
  443. data/lib/pubid/itu/identifiers.rb +16 -0
  444. data/lib/pubid/itu/model.rb +111 -0
  445. data/lib/pubid/itu/parser.rb +225 -0
  446. data/lib/pubid/itu/scheme.rb +174 -0
  447. data/lib/pubid/itu/urn_generator.rb +105 -0
  448. data/lib/pubid/itu.rb +22 -0
  449. data/lib/pubid/jcgm/builder.rb +88 -0
  450. data/lib/pubid/jcgm/components/publisher.rb +20 -0
  451. data/lib/pubid/jcgm/components.rb +9 -0
  452. data/lib/pubid/jcgm/identifier.rb +11 -0
  453. data/lib/pubid/jcgm/identifiers/amendment.rb +35 -0
  454. data/lib/pubid/jcgm/identifiers/guide.rb +21 -0
  455. data/lib/pubid/jcgm/identifiers/gum_guide.rb +51 -0
  456. data/lib/pubid/jcgm/identifiers.rb +11 -0
  457. data/lib/pubid/jcgm/parser.rb +84 -0
  458. data/lib/pubid/jcgm/scheme.rb +60 -0
  459. data/lib/pubid/jcgm/single_identifier.rb +48 -0
  460. data/lib/pubid/jcgm/supplement_identifier.rb +16 -0
  461. data/lib/pubid/jcgm/urn_generator.rb +110 -0
  462. data/lib/pubid/jcgm.rb +31 -0
  463. data/lib/pubid/jis/builder.rb +124 -0
  464. data/lib/pubid/jis/components/code.rb +59 -0
  465. data/lib/pubid/jis/components.rb +9 -0
  466. data/lib/pubid/jis/identifier.rb +18 -0
  467. data/lib/pubid/jis/identifiers/amendment.rb +16 -0
  468. data/lib/pubid/jis/identifiers/base.rb +72 -0
  469. data/lib/pubid/jis/identifiers/explanation.rb +22 -0
  470. data/lib/pubid/jis/identifiers/japanese_industrial_standard.rb +16 -0
  471. data/lib/pubid/jis/identifiers/standard.rb +27 -0
  472. data/lib/pubid/jis/identifiers/technical_report.rb +31 -0
  473. data/lib/pubid/jis/identifiers/technical_specification.rb +31 -0
  474. data/lib/pubid/jis/identifiers.rb +17 -0
  475. data/lib/pubid/jis/parser.rb +109 -0
  476. data/lib/pubid/jis/scheme.rb +49 -0
  477. data/lib/pubid/jis/single_identifier.rb +37 -0
  478. data/lib/pubid/jis/supplement_identifier.rb +47 -0
  479. data/lib/pubid/jis/urn_generator.rb +25 -0
  480. data/lib/pubid/jis.rb +23 -0
  481. data/lib/pubid/lutaml/no_store_registration.rb +30 -0
  482. data/lib/pubid/nist/builder.rb +2100 -0
  483. data/lib/pubid/nist/components/code.rb +38 -0
  484. data/lib/pubid/nist/components/edition.rb +118 -0
  485. data/lib/pubid/nist/components/issue_number.rb +28 -0
  486. data/lib/pubid/nist/components/part.rb +77 -0
  487. data/lib/pubid/nist/components/publisher.rb +24 -0
  488. data/lib/pubid/nist/components/stage.rb +53 -0
  489. data/lib/pubid/nist/components/supplement.rb +121 -0
  490. data/lib/pubid/nist/components/translation.rb +42 -0
  491. data/lib/pubid/nist/components/update.rb +103 -0
  492. data/lib/pubid/nist/components/version.rb +35 -0
  493. data/lib/pubid/nist/components/volume.rb +32 -0
  494. data/lib/pubid/nist/components.rb +19 -0
  495. data/lib/pubid/nist/configuration.rb +77 -0
  496. data/lib/pubid/nist/identifiers/base.rb +499 -0
  497. data/lib/pubid/nist/identifiers/circular.rb +68 -0
  498. data/lib/pubid/nist/identifiers/circular_supplement.rb +50 -0
  499. data/lib/pubid/nist/identifiers/commercial_standard.rb +41 -0
  500. data/lib/pubid/nist/identifiers/commercial_standard_emergency.rb +56 -0
  501. data/lib/pubid/nist/identifiers/commercial_standards_monthly.rb +56 -0
  502. data/lib/pubid/nist/identifiers/crpl_report.rb +135 -0
  503. data/lib/pubid/nist/identifiers/federal_information_processing_standards.rb +94 -0
  504. data/lib/pubid/nist/identifiers/grant_contractor_report.rb +35 -0
  505. data/lib/pubid/nist/identifiers/handbook.rb +50 -0
  506. data/lib/pubid/nist/identifiers/internal_report.rb +56 -0
  507. data/lib/pubid/nist/identifiers/letter_circular.rb +45 -0
  508. data/lib/pubid/nist/identifiers/miscellaneous_publication.rb +65 -0
  509. data/lib/pubid/nist/identifiers/monograph.rb +69 -0
  510. data/lib/pubid/nist/identifiers/ncstar.rb +41 -0
  511. data/lib/pubid/nist/identifiers/nsrds.rb +41 -0
  512. data/lib/pubid/nist/identifiers/owmwp.rb +35 -0
  513. data/lib/pubid/nist/identifiers/report.rb +68 -0
  514. data/lib/pubid/nist/identifiers/special_publication.rb +36 -0
  515. data/lib/pubid/nist/identifiers/technical_note.rb +90 -0
  516. data/lib/pubid/nist/identifiers.rb +33 -0
  517. data/lib/pubid/nist/parser.rb +1084 -0
  518. data/lib/pubid/nist/scheme.rb +199 -0
  519. data/lib/pubid/nist/supplement_identifier.rb +83 -0
  520. data/lib/pubid/nist/urn_generator.rb +127 -0
  521. data/lib/pubid/nist.rb +36 -0
  522. data/lib/pubid/oiml/builder.rb +189 -0
  523. data/lib/pubid/oiml/components/code.rb +20 -0
  524. data/lib/pubid/oiml/components.rb +9 -0
  525. data/lib/pubid/oiml/identifier.rb +11 -0
  526. data/lib/pubid/oiml/identifiers/amendment.rb +13 -0
  527. data/lib/pubid/oiml/identifiers/annex.rb +62 -0
  528. data/lib/pubid/oiml/identifiers/base.rb +36 -0
  529. data/lib/pubid/oiml/identifiers/basic_publication.rb +13 -0
  530. data/lib/pubid/oiml/identifiers/document.rb +13 -0
  531. data/lib/pubid/oiml/identifiers/expert_report.rb +13 -0
  532. data/lib/pubid/oiml/identifiers/guide.rb +13 -0
  533. data/lib/pubid/oiml/identifiers/recommendation.rb +13 -0
  534. data/lib/pubid/oiml/identifiers/seminar_report.rb +13 -0
  535. data/lib/pubid/oiml/identifiers/vocabulary.rb +13 -0
  536. data/lib/pubid/oiml/identifiers.rb +18 -0
  537. data/lib/pubid/oiml/parser.rb +173 -0
  538. data/lib/pubid/oiml/scheme.rb +46 -0
  539. data/lib/pubid/oiml/single_identifier.rb +90 -0
  540. data/lib/pubid/oiml/supplement_identifier.rb +43 -0
  541. data/lib/pubid/oiml/urn_generator.rb +64 -0
  542. data/lib/pubid/oiml.rb +26 -0
  543. data/lib/pubid/parser/common_parse_methods.rb +13 -0
  544. data/lib/pubid/parser/common_parse_rules.rb +56 -0
  545. data/lib/pubid/parser.rb +8 -0
  546. data/lib/pubid/parsers/base.rb +11 -0
  547. data/lib/pubid/parsers/mr_string.rb +93 -0
  548. data/lib/pubid/plateau/builder.rb +50 -0
  549. data/lib/pubid/plateau/identifiers/annex.rb +16 -0
  550. data/lib/pubid/plateau/identifiers/base.rb +51 -0
  551. data/lib/pubid/plateau/identifiers/handbook.rb +34 -0
  552. data/lib/pubid/plateau/identifiers/technical_report.rb +20 -0
  553. data/lib/pubid/plateau/identifiers.rb +12 -0
  554. data/lib/pubid/plateau/parser.rb +63 -0
  555. data/lib/pubid/plateau/scheme.rb +45 -0
  556. data/lib/pubid/plateau/supplement_identifier.rb +72 -0
  557. data/lib/pubid/plateau/urn_generator.rb +29 -0
  558. data/lib/pubid/plateau.rb +25 -0
  559. data/lib/pubid/renderers/base.rb +19 -0
  560. data/lib/pubid/renderers/directives_renderer.rb +62 -0
  561. data/lib/pubid/renderers/guide_renderer.rb +20 -0
  562. data/lib/pubid/renderers/human_readable.rb +58 -0
  563. data/lib/pubid/renderers/iwa_renderer.rb +16 -0
  564. data/lib/pubid/renderers/mr_string.rb +16 -0
  565. data/lib/pubid/renderers/supplement_renderer.rb +33 -0
  566. data/lib/pubid/renderers/urn.rb +11 -0
  567. data/lib/pubid/renderers.rb +14 -0
  568. data/lib/pubid/rendering/base.rb +73 -0
  569. data/lib/pubid/rendering/common.rb +211 -0
  570. data/lib/pubid/rendering/context.rb +156 -0
  571. data/lib/pubid/rendering/date.rb +27 -0
  572. data/lib/pubid/rendering/format.rb +25 -0
  573. data/lib/pubid/rendering/language.rb +21 -0
  574. data/lib/pubid/rendering/numbering.rb +24 -0
  575. data/lib/pubid/rendering/publisher.rb +25 -0
  576. data/lib/pubid/rendering/stage.rb +38 -0
  577. data/lib/pubid/rendering/supplement.rb +46 -0
  578. data/lib/pubid/rendering.rb +16 -0
  579. data/lib/pubid/sae/builder.rb +32 -0
  580. data/lib/pubid/sae/components/code.rb +9 -0
  581. data/lib/pubid/sae/components/date.rb +19 -0
  582. data/lib/pubid/sae/components/type.rb +19 -0
  583. data/lib/pubid/sae/components.rb +11 -0
  584. data/lib/pubid/sae/identifier.rb +14 -0
  585. data/lib/pubid/sae/identifiers/base.rb +42 -0
  586. data/lib/pubid/sae/identifiers.rb +9 -0
  587. data/lib/pubid/sae/parser.rb +55 -0
  588. data/lib/pubid/sae/scheme.rb +47 -0
  589. data/lib/pubid/sae/urn_generator.rb +38 -0
  590. data/lib/pubid/sae.rb +19 -0
  591. data/lib/pubid/scheme.rb +207 -0
  592. data/lib/pubid/urn_generator/base.rb +110 -0
  593. data/lib/pubid/utils/string_normalizer.rb +196 -0
  594. data/lib/pubid/utils.rb +7 -0
  595. data/lib/pubid/version.rb +3 -1
  596. data/lib/pubid.rb +137 -13
  597. data/lib/tasks/docs.rake +37 -0
  598. data/lib/tasks/export.rake +38 -0
  599. data/lib/tasks/website-data.json +7488 -0
  600. metadata +613 -171
  601. data/lib/pubid/registry.rb +0 -30
@@ -0,0 +1,2100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Nist
5
+ # Builder class for constructing NIST identifier objects from parsed data
6
+ # Single Responsibility: Transform parsed data into identifier objects
7
+ #
8
+ # CRITICAL ARCHITECTURE PRINCIPLE:
9
+ # Builder NEVER makes business logic decisions.
10
+ # Builder ONLY casts parsed data to domain objects.
11
+ class Builder < Pubid::Builder::Base
12
+ # Translation normalization map (V1 compatibility)
13
+ TRANSLATION_MAP = {
14
+ "es" => "spa",
15
+ "sp" => "spa",
16
+ "pt" => "por",
17
+ "id" => "ind",
18
+ "chi" => "zho",
19
+ "viet" => "vie",
20
+ "port" => "por",
21
+ "esp" => "spa",
22
+ }.freeze
23
+
24
+ def initialize(scheme)
25
+ @scheme = scheme
26
+ end
27
+
28
+ # Build an identifier object from parsed data
29
+ # @param parsed [Hash, Array] the parsed identifier data
30
+ # @return [Identifiers::Base] the constructed identifier object
31
+ def build(parsed)
32
+ # Parslet can return array of hashes - merge them
33
+ parsed_hash = parsed.is_a?(Array) ? flatten_array(parsed) : parsed
34
+
35
+ # NEW: Fix for update year captured as edition_e
36
+ # Pattern: "800-53r4/Upd3-2015" where parser captures "-2015" as :edition_e
37
+ # but it should be :update_year as part of the update component
38
+ if parsed_hash[:update_prefix] && parsed_hash[:update] && parsed_hash[:edition_e]
39
+ # Move edition_e to update_year
40
+ edition_id = parsed_hash[:edition_e][:edition_id]
41
+ parsed_hash[:update] =
42
+ parsed_hash[:update].merge(update_year: edition_id)
43
+ parsed_hash.delete(:edition_e) # Remove the edition_e hash
44
+ end
45
+
46
+ # NEW: Fix for letter suffix in number with edition_dash_year
47
+ # Pattern: "304a-2017" where parser returns first_number="304a" and edition_dash_year="2017"
48
+ # Expected: number="304", part="A", edition with type="e" and id="2017"
49
+ # We'll handle this by extracting the info and NOT adding :part to parsed_hash
50
+ # to avoid it being processed by cast(:part, ...) which would set type="pt"
51
+ letter_suffix_part = nil
52
+ edition_from_dash_year = nil
53
+ if parsed_hash[:first_number]&.to_s&.match?(/^[0-9]+[a-zA-Z]$/) && parsed_hash[:edition_dash_year]
54
+ number_str = parsed_hash[:first_number].to_s
55
+ # Extract letter suffix from number
56
+ if match_data = number_str.match(/^([0-9]+)([a-zA-Z])$/)
57
+ base_number = match_data[1]
58
+ letter_suffix = match_data[2].upcase
59
+
60
+ # Update first_number to exclude letter suffix
61
+ parsed_hash[:first_number] =
62
+ Components::Code.new(number: base_number)
63
+
64
+ # Store Part component for later (after identifier is initialized)
65
+ letter_suffix_part = Components::Part.new(type: "",
66
+ value: letter_suffix)
67
+
68
+ # Convert edition_dash_year to Edition component with type="e"
69
+ dash_year = parsed_hash[:edition_dash_year][:dash_year]
70
+ edition_from_dash_year = Components::Edition.new(type: "e",
71
+ id: dash_year)
72
+ parsed_hash.delete(:edition_dash_year) # Remove the old key
73
+ end
74
+ end
75
+
76
+ # NEW: Fix for edition_dash_year with embedded edition in first_number
77
+ # Pattern: "44e2-1955" where first_number="44e2" and edition_dash_year="1955"
78
+ # Expected: edition extracted from "44e2" (type: "e", id: "2") with additional_text="1955"
79
+ #
80
+ # We need to check for this pattern BEFORE the simple year-as-edition pattern below
81
+ if parsed_hash[:first_number]&.to_s&.match?(/^[0-9]+[a-zA-Z]\d+$/) && parsed_hash[:edition_dash_year]
82
+ # Extract edition from embedded pattern (e.g., "44e2" -> type="e", id="2")
83
+ number_str = parsed_hash[:first_number].to_s
84
+ if match_data = number_str.match(/^(\d+)([a-zA-Z])(\d+)$/)
85
+ base_number = match_data[1]
86
+ edition_type = match_data[2].downcase
87
+ edition_id = match_data[3]
88
+
89
+ # Update first_number to base number only
90
+ parsed_hash[:first_number] =
91
+ Components::Code.new(number: base_number)
92
+
93
+ # Create Edition with additional_text from edition_dash_year
94
+ dash_year = parsed_hash[:edition_dash_year][:dash_year]
95
+ edition_obj = Components::Edition.new(
96
+ type: edition_type,
97
+ id: edition_id,
98
+ additional_text: dash_year,
99
+ )
100
+ parsed_hash[:edition_with_year] = edition_obj
101
+ parsed_hash.delete(:edition_dash_year)
102
+ end
103
+ end
104
+
105
+ # NEW: Fix for edition embedded in first_number WITHOUT edition_dash_year
106
+ # Pattern: "8115r1" where parser returns first_number="8115r1" with NO edition_dash_year
107
+ # Expected: first_number="8115", edition with type="r", id="1"
108
+ # This handles patterns like IR "8115r1/upd" where r1 is the edition
109
+ # CRITICAL: Only process if NO second_number is present!
110
+ # Otherwise, the compound number logic (lines 418-437) will handle edition+year patterns
111
+ if !parsed_hash[:second_number] &&
112
+ parsed_hash[:first_number]&.to_s&.match?(/^[0-9]+[a-zA-Z]\d+$/) &&
113
+ !parsed_hash[:edition_dash_year]
114
+ number_str = parsed_hash[:first_number].to_s
115
+ if match_data = number_str.match(/^(\d+)([a-zA-Z])(\d+)$/)
116
+ base_number = match_data[1]
117
+ edition_type = match_data[2].downcase
118
+ edition_id = match_data[3]
119
+
120
+ # Update first_number to base number only
121
+ parsed_hash[:first_number] =
122
+ Components::Code.new(number: base_number)
123
+
124
+ # Create Edition without additional_text (no year in this pattern)
125
+ edition_obj = Components::Edition.new(
126
+ type: edition_type,
127
+ id: edition_id,
128
+ )
129
+ parsed_hash[:edition_with_year] = edition_obj
130
+ end
131
+ end
132
+
133
+ # REMOVED: edition_r_with_space pre-processing
134
+ # Now handled by cast_edition method with original_prefix preservation
135
+
136
+ # NEW: Fix for second_number_edition_year pattern
137
+ # Pattern: "105-1-1990" where parser returns second_number_edition_year={second_number="1", dash_year="1990"}
138
+ # Expected: number="105-1", edition with type="e", id="1990", original_prefix="-"
139
+ if parsed_hash[:second_number_edition_year]
140
+ second_num_value = parsed_hash[:second_number_edition_year][:second_number]
141
+ dash_year = parsed_hash[:second_number_edition_year][:dash_year]
142
+
143
+ # Extract second_number and edition_dash_year from the combined hash
144
+ parsed_hash[:second_number] = second_num_value
145
+
146
+ # For HB (Handbook) series, create year-only edition with dash rendering
147
+ is_handbook = begin
148
+ parsed_hash[:series].to_s == "HB"
149
+ rescue StandardError
150
+ false
151
+ end
152
+
153
+ if is_handbook && dash_year.to_s.match?(/^\d{4}$/)
154
+ # Create Edition with type="e" and id=dash_year for HB series
155
+ edition_obj = Components::Edition.new(type: "e", id: dash_year)
156
+ parsed_hash[:edition_from_year] = edition_obj
157
+ else
158
+ # For other series, treat dash_year as edition_dash_year for further processing
159
+ parsed_hash[:edition_dash_year] = { dash_year: dash_year }
160
+ end
161
+
162
+ parsed_hash.delete(:second_number_edition_year)
163
+ end
164
+
165
+ # NEW: Fix for fips_month_year_after_part pattern
166
+ # Pattern: "11-1-Sep1977" where parser returns fips_month_year_after_part={second_number="1", edition_month="Sep", edition_year="1977"}
167
+ # Expected: number="11-1", edition with type="e", id="197709" (year + month number)
168
+ if parsed_hash[:fips_month_year_after_part]
169
+ second_num_value = parsed_hash[:fips_month_year_after_part][:second_number]
170
+ month_str = parsed_hash[:fips_month_year_after_part][:edition_month]
171
+ year_str = parsed_hash[:fips_month_year_after_part][:edition_year]
172
+
173
+ # Extract second_number
174
+ parsed_hash[:second_number] = second_num_value
175
+
176
+ # Convert month abbreviation to month number and combine with year
177
+ month_num = Date::ABBR_MONTHNAMES.index(month_str) ||
178
+ Date::MONTHNAMES.index(month_str) ||
179
+ month_str.to_i
180
+
181
+ edition_id = if month_num&.positive?
182
+ "#{year_str}#{format('%02d', month_num)}"
183
+ else
184
+ year_str
185
+ end
186
+
187
+ # Create Edition with type="e" and combined ID
188
+ edition_obj = Components::Edition.new(type: "e", id: edition_id)
189
+ parsed_hash[:edition_from_year] = edition_obj
190
+
191
+ parsed_hash.delete(:fips_month_year_after_part)
192
+ end
193
+
194
+ # NEW: Fix for IR compound number vs edition pattern
195
+ # Pattern: "84-2946" where parser returns first_number="84", edition_dash_year={dash_year="2946"}
196
+ # For IR (InteragencyReport), 4-digit second numbers >= 1000 are typically editions
197
+ # EXCEPT for:
198
+ # - Numbers > 2699 which are clearly not valid years (like 2946)
199
+ # - Patterns with embedded edition_e (like "76-1094e2") which should be compound
200
+ if parsed_hash[:series]&.to_s == "IR" && parsed_hash[:first_number] && parsed_hash[:edition_dash_year]
201
+ first_num = parsed_hash[:first_number].to_s
202
+ dash_year = parsed_hash[:edition_dash_year][:dash_year].to_s
203
+
204
+ # Check if first_number looks like a 2-digit year (00-99)
205
+ if first_num.match?(/^\d{2}$/) && dash_year.match?(/^\d{4}$/)
206
+ dash_year_num = dash_year.to_i
207
+ # Valid year range for IR: 1901-2026 (NBS establishment to present)
208
+ is_valid_year = dash_year_num.between?(1901, 2026)
209
+ # If there's an embedded edition (e2, e3, etc.), treat as compound, not edition
210
+ has_embedded_edition = parsed_hash[:edition_e]
211
+ if is_valid_year && !has_embedded_edition
212
+ # Edition format: "76e1000", "76e2013", "76e1100", "80e2100", "81e2300", "81e2400", "82e2500", "82e2600"
213
+ parsed_hash[:first_number] =
214
+ Components::Code.new(number: first_num)
215
+ parsed_hash[:edition] =
216
+ Components::Edition.new(type: "e", id: dash_year)
217
+ else
218
+ # Compound number: "84-2946" or "76-1094e2"
219
+ parsed_hash[:first_number] =
220
+ Components::Code.new(number: "#{first_num}-#{dash_year}")
221
+ end
222
+ parsed_hash.delete(:edition_dash_year)
223
+ end
224
+ end
225
+
226
+ # NEW: Fix for edition_dash_year that's actually a second_number
227
+ # Pattern: "250-1039" where parser returns edition_dash_year="1039"
228
+ # Also handles RPT date ranges: "1946-1947" where edition_dash_year="1947"
229
+ # but it should be second_number="1039" (not a year - years are 1900+ or 2000+)
230
+ if parsed_hash[:first_number] && parsed_hash[:edition_dash_year] && !parsed_hash[:first_number].to_s.match?(/^[0-9]+[a-zA-Z]\d+$/)
231
+ dash_year = parsed_hash[:edition_dash_year][:dash_year].to_s
232
+ series = begin
233
+ parsed_hash[:series].to_s
234
+ rescue StandardError
235
+ ""
236
+ end
237
+
238
+ # Check if dash_year is a valid year (1901-2026)
239
+ dash_year_num = dash_year.to_i
240
+ is_valid_year = dash_year_num.between?(1901, 2026)
241
+
242
+ # Check if series uses dash-year as edition (HB, CS, FIPS)
243
+ # These series convert dash-year to edition ONLY for valid years
244
+ uses_dash_year_as_edition = ["HB", "CS", "FIPS"].include?(series)
245
+
246
+ # GCR always converts dash-year to edition (e.g., "15-1000" → "15e1000")
247
+ is_gcr = series == "GCR"
248
+
249
+ # IR only converts dash-year to edition for valid years (e.g., "76-1100" → "76e1100")
250
+ # but NOT for non-years like 2946 (e.g., "84-2946" stays as "84-2946")
251
+ is_ir = series == "IR"
252
+
253
+ # Check if series uses compound numbers with dash-year (RPT date ranges)
254
+ is_rpt = series == "RPT"
255
+
256
+ if is_rpt
257
+ # For RPT (Report) series with date ranges, create compound number: "1946-1947"
258
+ parsed_hash[:first_number] =
259
+ Components::Code.new(number: "#{parsed_hash[:first_number]}-#{dash_year}")
260
+ parsed_hash.delete(:edition_dash_year)
261
+ elsif is_gcr
262
+ # GCR always converts dash_year to edition regardless of whether it's a valid year
263
+ # e.g., "15-1000" → "15e1000", "15-1001" → "15e1001"
264
+ edition_obj = Components::Edition.new(type: "e", id: dash_year)
265
+ parsed_hash[:edition_from_year] = edition_obj
266
+ parsed_hash.delete(:edition_dash_year)
267
+ elsif is_ir && is_valid_year
268
+ # IR only converts valid years to edition
269
+ # e.g., "76-1100" → "76e1100", but "84-2946" stays as "84-2946"
270
+ edition_obj = Components::Edition.new(type: "e", id: dash_year)
271
+ parsed_hash[:edition_from_year] = edition_obj
272
+ parsed_hash.delete(:edition_dash_year)
273
+ elsif uses_dash_year_as_edition && is_valid_year
274
+ # For HB, CS, FIPS: convert dash_year to edition ONLY if it's a valid year
275
+ edition_obj = Components::Edition.new(type: "e", id: dash_year)
276
+ parsed_hash[:edition_from_year] = edition_obj
277
+ parsed_hash.delete(:edition_dash_year)
278
+ elsif dash_year.to_i < 1900
279
+ # For other series with dash_year < 1900, treat as second_number
280
+ parsed_hash[:second_number] = dash_year
281
+ parsed_hash.delete(:edition_dash_year)
282
+ elsif is_valid_year
283
+ # For remaining cases with valid year (>= 1901), check if series uses it as edition
284
+ is_handbook = series == "HB"
285
+ is_commercial_standard = series == "CS"
286
+ is_fips = series == "FIPS"
287
+
288
+ if is_handbook || is_commercial_standard || is_fips
289
+ edition_obj = Components::Edition.new(type: "e", id: dash_year)
290
+ parsed_hash[:edition_from_year] = edition_obj
291
+ parsed_hash.delete(:edition_dash_year)
292
+ end
293
+ end
294
+ end
295
+
296
+ # NEW: Fix for edition embedded in second_number
297
+ # Pattern: "53e5" where second_number="53e5" with edition "e5" embedded
298
+ # Expected: second_number="53", edition with type="e" and id="5"
299
+ if parsed_hash[:second_number]&.to_s&.match?(/^\d+[a-zA-Z]\d+$/)
300
+ second_str = parsed_hash[:second_number].to_s
301
+ # Extract edition from second_number (e.g., "53e5" → "53" + edition "e5")
302
+ if match_data = second_str.match(/^(\d+)([a-zA-Z])(\d+)$/)
303
+ base_number = match_data[1]
304
+ edition_letter = match_data[2]
305
+ edition_id = match_data[3]
306
+
307
+ # Update second_number and create Edition component
308
+ parsed_hash[:second_number] =
309
+ Components::Code.new(number: base_number)
310
+ # Store Edition component for later (after identifier is initialized)
311
+ edition_from_embedded = Components::Edition.new(
312
+ type: edition_letter, id: edition_id,
313
+ )
314
+ end
315
+ end
316
+
317
+ # NEW: Check for CIRC supplement pattern
318
+ # Note: :base_portion is lost during parser merge, so check for supplement indicators
319
+ if parsed_hash[:supplement_date_range] || parsed_hash[:supplement_slash_year] ||
320
+ parsed_hash[:supplement_month_year] || parsed_hash[:supplement_year] ||
321
+ parsed_hash[:supplement] || parsed_hash[:base_portion]
322
+ return build_circular_supplement(parsed_hash)
323
+ end
324
+
325
+ # Locate the appropriate identifier class via Scheme
326
+ identifier = @scheme.locate_identifier_klass(parsed_hash).new
327
+
328
+ # NEW: If we extracted a letter suffix Part, assign it now (after identifier initialization)
329
+ if letter_suffix_part
330
+ identifier.part = letter_suffix_part
331
+ end
332
+
333
+ # NEW: If we extracted an Edition from edition_dash_year, assign it now
334
+ if edition_from_dash_year
335
+ identifier.edition = edition_from_dash_year
336
+ end
337
+
338
+ # NEW: If we extracted an Edition from embedded second_number, assign it now
339
+ if edition_from_embedded
340
+ identifier.edition = edition_from_embedded
341
+ end
342
+
343
+ # NEW: If we extracted an Edition from edition_dash_year with embedded edition in first_number, assign it now
344
+ if parsed_hash[:edition_embedded_with_year]
345
+ identifier.edition = parsed_hash[:edition_embedded_with_year]
346
+ end
347
+
348
+ # NEW: If we extracted an Edition from edition_dash_year as year-only edition, assign it now
349
+ if parsed_hash[:edition_from_year]
350
+ identifier.edition = parsed_hash[:edition_from_year]
351
+ end
352
+
353
+ # NEW: If we extracted an Edition from edition_dash_year with embedded edition, assign it now
354
+ if parsed_hash[:edition_with_year]
355
+ identifier.edition = parsed_hash[:edition_with_year]
356
+ end
357
+
358
+ # NEW: If we have a direct Edition from parsed_hash, assign it now
359
+ # (Used for IR patterns where large dash_year is treated as edition)
360
+ if parsed_hash[:edition]
361
+ identifier.edition = parsed_hash[:edition]
362
+ end
363
+
364
+ # Track first_number, second_number, decimal_number, and letter_number for building compound number
365
+ first_num = nil
366
+ second_num = nil
367
+ decimal_num = nil
368
+ letter_num = nil
369
+ part_num = nil
370
+ extracted_revision = nil
371
+
372
+ # Cast and assign all attributes
373
+ parsed_hash.each_pair do |key, value|
374
+ realized_components = cast(key.to_sym, value, parsed_hash) # Pass parsed_hash for context
375
+ next if realized_components.nil?
376
+
377
+ # Track number components
378
+ if key == :first_number && realized_components.is_a?(Components::Code)
379
+ first_num = realized_components
380
+ elsif key == :second_number && realized_components.is_a?(Components::Code)
381
+ second_num = realized_components
382
+ elsif key == :crpl_range && realized_components.is_a?(Components::Code)
383
+ # crpl_range is treated as second_number for compound number construction
384
+ second_num = realized_components
385
+ elsif key == :part_number
386
+ part_num = value.to_s
387
+ # NEW: Track decimal_number for IR identifiers (e.g., 80-2073.3)
388
+ # decimal_number is stored as hash with :decimal_base and :decimal_suffix
389
+ elsif key == :decimal_number && realized_components.is_a?(Hash)
390
+ # Store the raw hash for processing during compound number construction
391
+ decimal_num = realized_components
392
+ # NEW: Track letter_number for NCSTAR identifiers (e.g., 1-1A, 1-3B)
393
+ # letter_number is stored as hash with :letter_base and :letter_suffix
394
+ # For SpecialPublication (e.g., 800-56A), we need to:
395
+ # 1. Store the original hash for compound number construction (letter_base)
396
+ # 2. Create a Part component from letter_suffix
397
+ elsif key == :letter_number
398
+ # Store the original hash for compound number construction
399
+ letter_num = value
400
+ # If cast returned a hash with a part component, it will be assigned below
401
+ end
402
+
403
+ # Handle composite hash returns (multiple related values)
404
+ case realized_components
405
+ when Hash
406
+ realized_components.each_pair do |sub_key, sub_value|
407
+ # Track first_number from hash returns
408
+ if sub_key == :first_number && sub_value.is_a?(Components::Code)
409
+ first_num = sub_value
410
+ # Track second_number from hash returns
411
+ elsif sub_key == :second_number && sub_value.is_a?(Components::Code)
412
+ second_num = sub_value
413
+ # NEW: Handle second_number with edition (hash with :number_only and :edition_id)
414
+ # For "126r2013": parser returns {:number_only=>"126", :edition_id=>"2013"}
415
+ # We DON'T convert to Components::Code here; we process it during compound number construction
416
+ elsif sub_key == :second_number && sub_value.is_a?(Hash)
417
+ if sub_value[:number_only] && sub_value[:edition_id]
418
+ # Store the raw hash for processing during compound number construction
419
+ # This prevents the hash from being assigned directly to identifier.number
420
+ second_num = sub_value
421
+ extracted_revision = "r" # Mark as revision format
422
+ end
423
+ # Track revision extraction
424
+ elsif sub_key == :revision
425
+ extracted_revision = sub_value
426
+ end
427
+ # Skip assignment for second_number hashes - they'll be processed during compound number construction
428
+ next if sub_key == :second_number && sub_value.is_a?(Hash) && sub_value[:number_only]
429
+
430
+ attrs = identifier.class.attributes
431
+ setter = "#{sub_key}="
432
+ if attrs.key?(sub_key.to_sym)
433
+ identifier.public_send(setter,
434
+ sub_value)
435
+ end
436
+ end
437
+ else
438
+ attrs = identifier.class.attributes
439
+ setter = "#{key}="
440
+ if attrs.key?(key.to_sym)
441
+ identifier.public_send(setter,
442
+ realized_components)
443
+ end
444
+ end
445
+ end
446
+
447
+ # Build compound number from first_number and second_number
448
+ if first_num && !identifier.number
449
+ # Skip if this is a v#n# pattern - now handled as Part component
450
+ if identifier.volume && identifier.issue_number
451
+ # V#n# pattern handled as Part in first_number cast
452
+ # NEW: Handle decimal number pattern (e.g., 80-2073.3 for IR identifiers)
453
+ # decimal_num is {:decimal_base => "2073", :decimal_suffix => "3"}
454
+ elsif decimal_num
455
+ decimal_base = decimal_num[:decimal_base].to_s
456
+ decimal_suffix = decimal_num[:decimal_suffix].to_s
457
+ identifier.number = Components::Code.new(number: "#{first_num.value}-#{decimal_base}.#{decimal_suffix}")
458
+ # NEW: Handle letter number pattern (e.g., 1-1A, 1-3B for NCSTAR identifiers)
459
+ # letter_num is {:letter_base => "1", :letter_suffix => "A"}
460
+ # Also handles IR series "R" suffix: "79-1786R" → "79-1786r1"
461
+ elsif letter_num
462
+ letter_base = letter_num[:letter_base].to_s
463
+ letter_suffix = letter_num[:letter_suffix].to_s
464
+
465
+ # SPECIAL CASE: IR series with "R" suffix means "r1" (revision 1)
466
+ # "79-1786R" → number="79-1786", edition="r1"
467
+ is_ir = parsed_hash[:series]&.to_s == "IR"
468
+ if is_ir && letter_suffix == "R"
469
+ # IR "R" suffix converts to revision format "r1"
470
+ identifier.number = Components::Code.new(number: "#{first_num.value}-#{letter_base}")
471
+ edition_obj = Components::Edition.new(type: "r", id: "1")
472
+ identifier.edition = edition_obj
473
+ identifier.edition_component = edition_obj
474
+ identifier.revision = "r1"
475
+ # If a Part component was already set (from cast handler), the letter_suffix
476
+ # is a separate Part component (e.g., SpecialPublication "800-56A" → number="800-56", part="A")
477
+ # Otherwise, letter_suffix is part of the number (e.g., NCSTAR "1-1A" → number="1-1A")
478
+ elsif identifier.part
479
+ # SpecialPublication pattern: letter_suffix is separate Part component
480
+ identifier.number = Components::Code.new(number: "#{first_num.value}-#{letter_base}")
481
+ else
482
+ # NCSTAR pattern: letter_suffix is part of the number
483
+ identifier.number = Components::Code.new(number: "#{first_num.value}-#{letter_base}#{letter_suffix}")
484
+ end
485
+ elsif second_num
486
+ # Check for special patterns first
487
+ # NEW: Handle second_number with edition (hash from parser pattern)
488
+ # For "126r2013": second_num is {:number_only=>"126", :edition_id=>"2013"}
489
+ if second_num.is_a?(Hash) && second_num[:number_only] && second_num[:edition_id]
490
+ # Extract components from hash
491
+ number_part = second_num[:number_only].to_s
492
+ edition_id = second_num[:edition_id].to_s
493
+
494
+ # Create Edition component
495
+ edition_obj = Components::Edition.new(type: "r", id: edition_id)
496
+
497
+ identifier.number = Components::Code.new(number: "#{first_num.value}-#{number_part}")
498
+ identifier.edition = edition_obj
499
+ identifier.edition_component = edition_obj
500
+ identifier.revision = "r#{edition_id}"
501
+ # CS Emergency pattern: e104-43 → number=104, edition_year=1943
502
+ # Logic: e104-43 means "emergency 104 from 1943" (43 = 1943)
503
+ elsif first_num.value.to_s.match?(/^e(\d{3})$/) &&
504
+ second_num.value.to_s.match?(/^\d{2}$/)
505
+ match_data = first_num.value.to_s.match(/^e(\d{3})$/)
506
+ number_part = match_data[1] # 104
507
+ year_suffix = second_num.value.to_s # 43
508
+ # Edition year: 19 + 43 = 1943 (1900s + year suffix)
509
+ edition_year = "19#{year_suffix}"
510
+
511
+ # Create Edition component
512
+ edition_obj = Components::Edition.new(type: "e", id: edition_year)
513
+
514
+ identifier.number = Components::Code.new(number: number_part)
515
+ identifier.edition = edition_obj
516
+ identifier.edition_component = edition_obj
517
+ elsif first_num.value.to_s.match?(/^(\d+)e(\d+)$/) &&
518
+ second_num.value.to_s.match?(/^\d{2,4}$/)
519
+ # Pattern: "11e2-1915" OR "123e2-50" parsed as first="11e2"|"123e2", second="1915"|"50"
520
+ # Extract number and edition from first_num
521
+ match_data = first_num.value.to_s.match(/^(\d+)e(\d+)$/)
522
+ number_part = match_data[1]
523
+ edition_id = match_data[2]
524
+ year_part = second_num.value.to_s
525
+
526
+ # Expand 2-digit year to 4-digit (50 → 1950)
527
+ year_part = "19#{year_part}" if year_part.length == 2
528
+
529
+ identifier.number = Components::Code.new(number: number_part)
530
+
531
+ # For edition+year patterns, handling depends on identifier type:
532
+ # - CIRC: edition number + year as additional_text, rendered with dot ("11e2-1915" → "11e2.1915")
533
+ # - HB, others: edition number + year as additional_text, rendered with dash ("44e2-1955")
534
+ # Both use the same Edition component structure, only rendering differs
535
+ edition_obj = Components::Edition.new(type: "e",
536
+ id: edition_id, additional_text: year_part)
537
+ identifier.edition = edition_obj
538
+ identifier.edition_component = edition_obj
539
+ elsif first_num.value.to_s.match?(/^(\d+)supp?$/) &&
540
+ second_num.value.to_s.match?(/^\d{4}$/)
541
+ # Pattern: "25supp-1924" parsed as first="25supp", second="1924"
542
+ number_part = first_num.value.to_s.match(/^(\d+)supp?$/)[1]
543
+ year_part = second_num.value.to_s
544
+
545
+ identifier.number = Components::Code.new(number: number_part)
546
+ identifier.supplement = year_part
547
+ elsif identifier.is_a?(Identifiers::TechnicalNote) &&
548
+ second_num.value.to_s.match?(/^(19|20)\d{2}$/)
549
+ # SPECIAL CASE FOR TN: second_num is edition year
550
+ # Following "date IS edition" rule: -1993 becomes Edition(type: "e", id: "1993")
551
+ identifier.number = first_num
552
+ edition_obj = Components::Edition.new(type: "e",
553
+ id: second_num.value.to_s)
554
+ identifier.edition_component = edition_obj
555
+ identifier.edition = edition_obj
556
+ identifier.edition_year = second_num.value.to_s
557
+ elsif part_num && parsed_hash[:series].to_s.include?("IR")
558
+ # Normal compound number
559
+ # For IR identifiers, part_number should be a Part component (type="pt"), not in compound number
560
+ identifier.part = Components::Part.new(type: "pt",
561
+ value: part_num)
562
+ identifier.number = Components::Code.new(number: "#{first_num.value}-#{second_num.value}")
563
+ # For IR, create Part component with type="pt"
564
+ else
565
+ # For GCR and others, include part number in compound number
566
+ compound_value = "#{first_num.value}-#{second_num.value}"
567
+ compound_value += "-#{part_num}" if part_num
568
+ identifier.number = Components::Code.new(number: compound_value)
569
+ end
570
+ else
571
+ # No second_num, use first_num directly
572
+ identifier.number = first_num
573
+ end
574
+ end
575
+
576
+ # Apply extracted revision if not already set
577
+ if extracted_revision && !identifier.edition
578
+ # Convert extracted revision to Edition component
579
+ identifier.edition = Components::Edition.new(type: "r",
580
+ id: extracted_revision.to_s)
581
+ end
582
+
583
+ # IR-SPECIFIC: Handle compound numbers that were converted to edition+year format
584
+ # For IR identifiers, "84-2946" should remain as compound number, not become "84e2946"
585
+ # The preprocessing converts "84-2946" to "84e2946", so we need to convert it back for IR
586
+ is_ir = begin
587
+ parsed_hash[:series].to_s.include?("IR")
588
+ rescue StandardError
589
+ false
590
+ end
591
+ if is_ir && identifier.number && identifier.number.value.to_s.match?(/^(\d+)e(\d{4})$/)
592
+ # Extract the compound number parts from the edition+year format
593
+ match_data = identifier.number.value.to_s.match(/^(\d+)e(\d{4})$/)
594
+ number_part = match_data[1] # "84"
595
+ year_part = match_data[2] # "2946"
596
+
597
+ # Convert to compound number format
598
+ identifier.number = Components::Code.new(number: "#{number_part}-#{year_part}")
599
+
600
+ # Clear the edition that was incorrectly set from the year
601
+ identifier.edition = nil
602
+ identifier.edition_component = nil
603
+ end
604
+
605
+ # Set publisher_was_parsed flag if publisher was set
606
+ # This includes cases where publisher was explicitly parsed or extracted from series prefix
607
+ identifier.publisher_was_parsed = true if identifier.publisher
608
+
609
+ # NEW: Convert revision with month+year to update component (V1 compatibility)
610
+ # Patterns like "NIST IR 4743rJun1992" should be rendered as "NIST IR 4743/Upd1-199206"
611
+ if parsed_hash[:revision_month] && parsed_hash[:revision_year]
612
+ # rJun1992 pattern: revision_month is "Jun", revision_year is "1992"
613
+ month_str = parsed_hash[:revision_month].to_s
614
+ year_str = parsed_hash[:revision_year].to_s
615
+
616
+ # Convert month name to number (Jun → 06, Nov → 11, etc.)
617
+ month_num = month_name_to_number(month_str)
618
+
619
+ # Create update component with default number=1, converted year and month
620
+ update_obj = Components::Update.new(
621
+ number: "1",
622
+ year: year_str,
623
+ month: sprintf("%02d", month_num),
624
+ prefix: "slash", # V1 uses /Upd format
625
+ )
626
+
627
+ # Set both V2 component and legacy attribute for backward compatibility
628
+ identifier.update_component = update_obj
629
+ identifier.update = update_obj
630
+
631
+ # Clear the legacy revision_year/revision_month attributes
632
+ identifier.revision_year = nil
633
+ identifier.revision_month = nil
634
+ end
635
+
636
+ identifier
637
+ end
638
+
639
+ # Convert month name to month number
640
+ # @param month_name [String] month abbreviation (Jan, Feb, Mar, etc.)
641
+ # @return [Integer] month number (1-12)
642
+ def month_name_to_number(month_name)
643
+ month_map = {
644
+ "Jan" => 1, "January" => 1,
645
+ "Feb" => 2, "February" => 2,
646
+ "Mar" => 3, "March" => 3,
647
+ "Apr" => 4, "April" => 4,
648
+ "May" => 5,
649
+ "Jun" => 6, "June" => 6,
650
+ "Jul" => 7, "July" => 7,
651
+ "Aug" => 8, "August" => 8,
652
+ "Sep" => 9, "Sept" => 9, "September" => 9,
653
+ "Oct" => 10, "October" => 10,
654
+ "Nov" => 11, "November" => 11,
655
+ "Dec" => 12, "December" => 12
656
+ }
657
+ month_map[month_name] || 1 # Default to January if not found
658
+ end
659
+
660
+ # Build CircularSupplement with base_identifier wrapping
661
+ # @param parsed_hash [Hash] the parsed supplement data
662
+ # @return [Identifiers::CircularSupplement] the supplement identifier
663
+ def build_circular_supplement(parsed_hash)
664
+ supplement = Identifiers::CircularSupplement.new
665
+
666
+ # Extract series from circ_series if present (nested structure from parser)
667
+ series_value = nil
668
+ if parsed_hash[:circ_series].is_a?(Hash)
669
+ series_value = parsed_hash[:circ_series][:series]
670
+ elsif parsed_hash[:series]
671
+ series_value = parsed_hash[:series]
672
+ end
673
+
674
+ # Handle date range supplement (no base)
675
+ if parsed_hash[:supplement_date_range].is_a?(Hash)
676
+ range = parsed_hash[:supplement_date_range]
677
+ month_start = range[:supp_month_start]&.to_s
678
+ year_start = range[:supp_year_start]&.to_s
679
+ month_end = range[:supp_month_end]&.to_s
680
+ year_end = range[:supp_year_end]&.to_s
681
+
682
+ supplement.supplement_date_range_start = "#{month_start}#{year_start}" if month_start && year_start
683
+ supplement.supplement_date_range_end = "#{month_end}#{year_end}" if month_end && year_end
684
+
685
+ return supplement
686
+ end
687
+
688
+ # Build base identifier from base_portion (if present)
689
+ # If not present (because it was merged during parsing), use first_number
690
+ if parsed_hash[:base_portion]
691
+ # Extract the actual number value from base_portion hash
692
+ # base_portion can be: {:simple_number=>"118"}, {:base_number=>"145", :revision_letter=>"r", :revision_number=>"11"}, etc.
693
+ base_portion = parsed_hash[:base_portion]
694
+ base_number = if base_portion.is_a?(Hash)
695
+ # Extract the value from whichever key is present
696
+ base_portion[:simple_number] || base_portion[:base_number]
697
+ else
698
+ base_portion
699
+ end
700
+
701
+ # Check for letter suffix in base_portion (e.g., "378G")
702
+ letter_suffix = nil
703
+ if base_portion.is_a?(Hash) && base_portion[:letter_suffix]
704
+ letter_suffix = base_portion[:letter_suffix].to_s.upcase
705
+ end
706
+
707
+ # Extract publisher from circ_series if present
708
+ publisher_value = nil
709
+ if parsed_hash[:circ_series].is_a?(Hash) && parsed_hash[:circ_series][:series]
710
+ series_str = parsed_hash[:circ_series][:series].to_s
711
+ # Extract publisher from series (e.g., "NBS LCIRC" -> "NBS")
712
+ publisher_value = series_str.split.first if series_str.include?(" ")
713
+ end
714
+
715
+ # Check if base_portion has revision (for patterns like "145r11/1925")
716
+ has_revision = base_portion.is_a?(Hash) && base_portion[:revision_letter] && base_portion[:revision_number]
717
+
718
+ # NEW: Check if base_portion has edition_number (for patterns like "101e2")
719
+ has_edition = base_portion.is_a?(Hash) && base_portion[:edition_number]
720
+
721
+ # Include letter suffix in base_number if present
722
+ # Also include edition_number if present (for "101e2" pattern)
723
+ base_number_with_suffix = base_number.to_s
724
+ if letter_suffix
725
+ base_number_with_suffix += letter_suffix
726
+ end
727
+ if has_edition
728
+ base_number_with_suffix += "e#{base_portion[:edition_number]}"
729
+ end
730
+
731
+ # Reconstruct parse hash for base identifier
732
+ base_hash = {
733
+ series: series_value,
734
+ first_number: base_number_with_suffix,
735
+ parsed_format: parsed_hash[:parsed_format],
736
+ }
737
+ base_hash[:publisher] = publisher_value if publisher_value
738
+
739
+ # NEW: Add edition_number to base_hash for patterns like "101e2"
740
+ # This will be processed by the normal build() logic to create Edition component
741
+ if has_edition
742
+ # Create edition_e hash that will be converted to Edition with type="e"
743
+ base_hash[:edition_e] =
744
+ { edition_id: base_portion[:edition_number] }
745
+ end
746
+
747
+ # Recursively build base identifier
748
+ # This will go through normal build() process which extracts edition from "101e2"
749
+ supplement.base_identifier = build(base_hash)
750
+
751
+ # NEW: Handle revision + implicit supplement pattern (e.g., "145r11/1925")
752
+ # Create update format: "Upd1-{year}{revision_number}"
753
+ if has_revision && parsed_hash[:implicit_supplement].is_a?(Hash)
754
+ revision_number = base_portion[:revision_number].to_s
755
+ supplement_year = parsed_hash[:implicit_supplement][:implicit_supplement_year].to_s
756
+
757
+ # Create Update component for revision+supplement pattern
758
+ # Format: Upd1-{year}{revision_number} (always use "1" and concatenate year+revision)
759
+ update_value = "Upd1-#{supplement_year}#{revision_number}"
760
+ supplement.update = update_value
761
+ supplement.implicit_supplement = true # Mark as implicit supplement for rendering
762
+ end
763
+ elsif parsed_hash[:first_number]
764
+ # base_portion was lost during merge, use first_number to build base identifier
765
+ base_hash = {
766
+ publisher: parsed_hash[:publisher],
767
+ series: series_value || parsed_hash[:series],
768
+ first_number: parsed_hash[:first_number],
769
+ parsed_format: parsed_hash[:parsed_format],
770
+ }
771
+
772
+ # Recursively build base identifier
773
+ supplement.base_identifier = build(base_hash)
774
+ end
775
+
776
+ # Build supplement edition from captured data
777
+ if parsed_hash[:supplement_month_year]
778
+ # Parse month+year format like "Jan1924"
779
+ month_year = parsed_hash[:supplement_month_year].to_s
780
+ supplement.edition = Components::Edition.new(type: "s",
781
+ id: month_year)
782
+ elsif parsed_hash[:supplement_year]
783
+ # Just year: 1924
784
+ supplement.edition = Components::Edition.new(type: "s",
785
+ id: parsed_hash[:supplement_year].to_s)
786
+ elsif parsed_hash[:supplement_slash_year].is_a?(Hash)
787
+ # NEW: Handle supplement_slash_year pattern (e.g., "sup12/1926", "sup1/1927")
788
+ # V1 format: "Upd1-192612" where "1" is fixed and "192612" is year+number concatenated
789
+ # Single digit numbers are zero-padded: "sup1/1927" → "Upd1-192701"
790
+ supp_hash = parsed_hash[:supplement_slash_year]
791
+ supp_number = supp_hash[:supp_number]&.to_s
792
+ supp_year = supp_hash[:supp_year]&.to_s
793
+
794
+ # Pad supplement number to 2 digits for single-digit numbers
795
+ supp_number_padded = supp_number.rjust(2, "0")
796
+
797
+ # Create Update component for supplement (V1 compatibility uses Update for supplements)
798
+ # Format: Upd1-{year}{padded_number} (always use "1" and concatenate year+padded_number)
799
+ update_value = "Upd1-#{supp_year}#{supp_number_padded}"
800
+ supplement.update = update_value
801
+ elsif parsed_hash[:supplement_empty]
802
+ # Empty supplement - no edition
803
+ # supplement.edition remains nil
804
+ end
805
+
806
+ supplement
807
+ end
808
+
809
+ private
810
+
811
+ # Cast parsed value to appropriate component type
812
+ # ALL conversions happen in this single method
813
+ # @param type [Symbol] the parameter type
814
+ # @param value [Object] the parsed value
815
+ # @param parsed_hash [Hash] the full parsed hash for context
816
+ # @return [Object, Hash, nil] the cast component(s)
817
+ def cast(type, value, parsed_hash = {})
818
+ case type
819
+ when :publisher
820
+ return nil if value.nil? || value.to_s.strip.empty?
821
+
822
+ Components::Publisher.new(publisher: value.to_s)
823
+
824
+ when :series
825
+ return nil if value.nil? || value.to_s.strip.empty?
826
+
827
+ str_value = value.to_s
828
+ publisher_extracted = nil
829
+
830
+ # For compound series like "NBS CIRC", extract publisher and series separately
831
+ if str_value.start_with?("NBS ")
832
+ publisher_extracted = "NBS"
833
+ str_value = str_value.sub("NBS ", "")
834
+ end
835
+
836
+ # Return composite hash with both publisher and series if extracted
837
+ if publisher_extracted
838
+ {
839
+ publisher: Components::Publisher.new(publisher: publisher_extracted),
840
+ series: Components::Code.new(number: str_value),
841
+ }
842
+ else
843
+ Components::Code.new(number: str_value)
844
+ end
845
+
846
+ when :volume_number
847
+ # Volume from v#n# pattern - return Volume component
848
+ return nil if value.nil? || value.to_s.strip.empty?
849
+
850
+ { volume: Components::Volume.new(value: value.to_s) }
851
+
852
+ when :issue_number
853
+ # Issue number from v#n# pattern - return Part component
854
+ return nil if value.nil? || value.to_s.strip.empty?
855
+
856
+ { part: Components::Part.new(type: "n", value: value.to_s) }
857
+
858
+ when :part_number
859
+ # Part number from GCR pattern (e.g., 85-3273-37)
860
+ # Return raw value for inclusion in compound number
861
+ return nil if value.nil? || value.to_s.strip.empty?
862
+
863
+ value # Return raw value to be tracked in builder
864
+
865
+ when :letter_number
866
+ # Letter number pattern (e.g., 800-56A, 1-1A for NCSTAR, 73-197Ur for IR)
867
+ # Parser returns: {:letter_base=>"56", :letter_suffix=>"A"} or
868
+ # {:letter_base=>"197", :letter_suffix=>"U", :letter_suffix_extra=>"r"}
869
+ # For SpecialPublication, create Part component with letter suffix as value
870
+ # For MONO and NCSTAR, preserve letter suffix as part of the number (return raw value)
871
+ return nil if value.nil? || !value.is_a?(Hash)
872
+
873
+ letter_suffix = value[:letter_suffix]&.to_s&.strip
874
+ letter_suffix_extra = value[:letter_suffix_extra]&.to_s&.strip
875
+
876
+ # Combine letter_suffix and letter_suffix_extra (e.g., "U" + "r" = "Ur")
877
+ full_suffix = if letter_suffix_extra && !letter_suffix_extra.empty?
878
+ letter_suffix + letter_suffix_extra
879
+ else
880
+ letter_suffix
881
+ end
882
+
883
+ return nil if full_suffix.nil? || full_suffix.empty?
884
+
885
+ # Check if this is a MONO or NCSTAR series
886
+ # For these series, the letter suffix should be part of the number, not a separate Part component
887
+ # For IR with "R" or "Ur" suffix, also return raw value so builder can convert to edition "r1"
888
+ is_mono = begin
889
+ parsed_hash[:series].to_s.include?("MONO")
890
+ rescue StandardError
891
+ false
892
+ end
893
+ is_ncstar = begin
894
+ parsed_hash[:series].to_s.include?("NCSTAR")
895
+ rescue StandardError
896
+ false
897
+ end
898
+ # IR with "R" suffix needs special handling (convert to edition "r1")
899
+ # Also handle "Ur" which combines uppercase U with lowercase r
900
+ is_ir_with_r = begin
901
+ parsed_hash[:series].to_s.include?("IR") && (letter_suffix == "R" || full_suffix == "Ur")
902
+ rescue StandardError
903
+ false
904
+ end
905
+
906
+ if is_mono || is_ncstar || is_ir_with_r
907
+ # For MONO and NCSTAR, preserve letter suffix as part of the number
908
+ # For IR with "R" or "Ur", return raw value so builder can convert "79-1786R" to "79-1786r1"
909
+ # Return raw value so builder can construct proper format
910
+ value[:letter_suffix] = full_suffix
911
+ value
912
+ else
913
+ # For SpecialPublication and others, create Part component
914
+ { part: Components::Part.new(type: "", value: full_suffix.upcase) }
915
+ end
916
+
917
+ when :fips_part
918
+ # Part number from FIPS date pattern (e.g., 11-1-Sep30/1977)
919
+ # Return Part component with pt type
920
+ return nil if value.nil? || value.to_s.strip.empty?
921
+
922
+ { part: Components::Part.new(type: "pt", value: value.to_s) }
923
+
924
+ when :owmwp_date_number
925
+ # OWMWP date-based number format (MM-DD-YYYY)
926
+ # Parser returns: {:owmwp_month=>"06", :owmwp_day=>"13", :owmwp_year=>"2018"}
927
+ # Convert to number + edition: "06-13" + edition "e2018"
928
+ return nil if value.nil?
929
+
930
+ number_part = "#{value[:owmwp_month]}-#{value[:owmwp_day]}"
931
+ edition_part = Components::Edition.new(type: "e",
932
+ id: value[:owmwp_year])
933
+ { first_number: Components::Code.new(number: number_part), edition: edition_part }
934
+
935
+ when :first_number, :second_number
936
+ return nil if value.nil? || value.to_s.strip.empty?
937
+
938
+ # NEW: Handle OWMWP date-based number (nested hash structure)
939
+ # Parser returns: {:owmwp_date_number=>{:owmwp_month=>"06", :owmwp_day=>"13", :owmwp_year=>"2018"}}
940
+ # Convert to number + edition: "06-13" + edition "e2018"
941
+ if value.is_a?(Hash) && value[:owmwp_date_number]
942
+ owmwp_hash = value[:owmwp_date_number]
943
+ number_part = "#{owmwp_hash[:owmwp_month]}-#{owmwp_hash[:owmwp_day]}"
944
+ edition_part = Components::Edition.new(type: "e",
945
+ id: owmwp_hash[:owmwp_year])
946
+ return { type => Components::Code.new(number: number_part), edition: edition_part }
947
+ end
948
+
949
+ # NEW: Handle second_number with edition (hash with :number_only and :edition_id)
950
+ # This handles "126r2013" pattern where parser returns {:number_only=>"126", :edition_id=>"2013"}
951
+ # CRITICAL: Wrap in a structure that builder loop can recognize
952
+ # The builder loop expects keys like :second_number to be present in the hash
953
+ if type == :second_number && value.is_a?(Hash) && value[:number_only] && value[:edition_id]
954
+ # Return wrapped hash so builder loop finds :second_number key
955
+ return { second_number: value }
956
+ end
957
+
958
+ # NEW: Handle second_number with revision_letter (hash with :revision_letter containing :number_only and :letter)
959
+ # This handles "27ra" pattern where parser returns {revision_letter: {number_only: "27", letter: "a"}}
960
+ # Should be combined to "27rA" format
961
+ if type == :second_number && value.is_a?(Hash) && value[:revision_letter]
962
+ revision_data = value[:revision_letter]
963
+ number_only = revision_data[:number_only].to_s
964
+ letter = revision_data[:letter].to_s.upcase
965
+ # Return as second_number with combined format "27rA"
966
+ return { second_number: Components::Code.new(number: "#{number_only}r#{letter}") }
967
+ end
968
+
969
+ # Handle v#n# pattern (CSM series) - comes as hash from parser
970
+ # Return Volume and Part components separately
971
+ if value.is_a?(Hash) && value[:volume_number] && value[:issue_number]
972
+ volume_num = value[:volume_number].to_s
973
+ issue_num = value[:issue_number].to_s
974
+ return {
975
+ volume: Components::Volume.new(value: volume_num),
976
+ part: Components::Part.new(type: "n", value: issue_num),
977
+ }
978
+ end
979
+
980
+ str_value = value.to_s
981
+
982
+ # Handle special patterns embedded in first_number
983
+ if type == :first_number
984
+
985
+ # NEW: Handle first_number hash with number_with_rev_year (e.g., "1013rv1953")
986
+ # Parser returns: {:number_with_rev_year=>{:number=>"1013", :revision_year=>"1953"}}
987
+ if value.is_a?(Hash) && value[:number_with_rev_year]
988
+ number_part = value[:number_with_rev_year][:number].to_s
989
+ revision_year = value[:number_with_rev_year][:revision_year].to_s
990
+ return {
991
+ first_number: Components::Code.new(number: number_part),
992
+ edition: Components::Edition.new(type: "rv", id: revision_year),
993
+ }
994
+ end
995
+
996
+ # NEW: Handle first_number hash with language_code (e.g., "1262es")
997
+ # Parser returns: {:number=>"1262", :language_code=>"es"}
998
+ if value.is_a?(Hash) && value[:number] && value[:language_code]
999
+ number_part = value[:number].to_s
1000
+ language_code = value[:language_code].to_s.strip.downcase
1001
+ # Apply normalization map (es → spa, pt → por, etc.)
1002
+ normalized_code = TRANSLATION_MAP[language_code] || language_code
1003
+ return {
1004
+ first_number: Components::Code.new(number: number_part),
1005
+ translation_component: Components::Translation.new(code: normalized_code),
1006
+ }
1007
+ end
1008
+
1009
+ # NEW: Handle first_number hash with number, part_number, and edition_year (MR format)
1010
+ # Parser returns: {:number=>"28", :part_number=>"1", :edition_year=>"1969"}
1011
+ # For "NBS.HB.28pt1e1969" MR format input
1012
+ if value.is_a?(Hash) && value[:number] && value[:part_number] && value[:edition_year]
1013
+ number_part = value[:number].to_s
1014
+ part_number = value[:part_number].to_s
1015
+ edition_year = value[:edition_year].to_s
1016
+ return {
1017
+ first_number: Components::Code.new(number: number_part),
1018
+ part: Components::Part.new(type: "pt", value: part_number),
1019
+ edition: Components::Edition.new(type: "e", id: edition_year),
1020
+ }
1021
+ end
1022
+
1023
+ # NEW: Check for edition_year_separate in parsed_hash context
1024
+ # This handles "11e2-1915" where first_number="11e2" and edition_year_separate="1915"
1025
+ if parsed_hash[:edition_year_separate] && str_value =~ /^(\d+)e(\d+)$/
1026
+ number_part = $1
1027
+ edition_id = $2
1028
+ year_part = parsed_hash[:edition_year_separate].to_s
1029
+ return {
1030
+ first_number: Components::Code.new(number: number_part),
1031
+ edition: Components::Edition.new(type: "e", id: edition_id,
1032
+ additional_text: year_part),
1033
+ }
1034
+ end
1035
+
1036
+ # NEW: Check for number_with_volume in value hash (for first_number)
1037
+ # This handles "539v10" where parser captures :number and :volume_suffix separately
1038
+ # Parse tree: value = {:number_with_volume => {:number => "539", :volume_suffix => "10"}}
1039
+ if value.is_a?(Hash) && value[:number_with_volume] && value[:number_with_volume][:volume_suffix]
1040
+ number_part = value[:number_with_volume][:number].to_s
1041
+ volume_value = value[:number_with_volume][:volume_suffix].to_s
1042
+ return {
1043
+ first_number: Components::Code.new(number: number_part),
1044
+ volume: Components::Volume.new(value: volume_value),
1045
+ }
1046
+ end
1047
+
1048
+ # NEW: Check for historical_month and historical_year in parsed_hash context
1049
+ # This handles "-April1909" where it's captured as separate month/year
1050
+ if parsed_hash[:historical_month] && parsed_hash[:historical_year]
1051
+ month_part = parsed_hash[:historical_month].to_s
1052
+ year_part = parsed_hash[:historical_year].to_s
1053
+ # Check if str_value is just a number (the part before dash)
1054
+ if /^\d+$/.match?(str_value)
1055
+ return {
1056
+ first_number: Components::Code.new(number: str_value),
1057
+ edition: Components::Edition.new(type: "-",
1058
+ additional_text: "#{month_part}#{year_part}"),
1059
+ }
1060
+ else
1061
+ # No number, just historical edition
1062
+ return {
1063
+ edition: Components::Edition.new(type: "-",
1064
+ additional_text: "#{month_part}#{year_part}"),
1065
+ }
1066
+ end
1067
+ end
1068
+
1069
+ # NEW: Pattern "9350sup" - number with "sup" suffix (no year)
1070
+ # This handles RPT supplements like "NBS RPT 9350sup"
1071
+ if str_value =~ /^(\d+)sup$/
1072
+ return {
1073
+ first_number: Components::Code.new(number: $1),
1074
+ supplement: "",
1075
+ }
1076
+ end
1077
+
1078
+ # NEW: Check for supplement_year in parsed_hash context
1079
+ # This handles "25supp-1924" where first_number="25supp" and supplement_year="1924"
1080
+ if parsed_hash[:supplement_year] && str_value =~ /^(\d+)supp?$/
1081
+ number_part = $1
1082
+ year_part = parsed_hash[:supplement_year].to_s
1083
+ return {
1084
+ first_number: Components::Code.new(number: number_part),
1085
+ supplement: year_part,
1086
+ }
1087
+ end
1088
+
1089
+ # Pattern: "154supprev" - supplement with revision
1090
+ if str_value =~ /^(\d+)supprev$/
1091
+ return {
1092
+ first_number: Components::Code.new(number: $1),
1093
+ supplement: "",
1094
+ supplement_has_revision: true,
1095
+ }
1096
+ # NEW: Pattern "11e2-1915" - edition with separate year (inline match)
1097
+ # Creates: number="11", Edition(type: "e", id: "2", additional_text: "1915")
1098
+ # Renders: "NBS CIRC 11e2.1915"
1099
+ elsif str_value =~ /^(\d+)e(\d+)-(\d{4})$/
1100
+ number_part = $1
1101
+ edition_id = $2
1102
+ year_part = $3
1103
+ return {
1104
+ first_number: Components::Code.new(number: number_part),
1105
+ edition: Components::Edition.new(type: "e", id: edition_id,
1106
+ additional_text: year_part),
1107
+ }
1108
+ # NEW: Pattern "-April1909" - historical edition with month+year (inline match)
1109
+ # Creates: Edition(type: "-", additional_text: "April1909")
1110
+ # Renders: "NBS CIRC -April1909"
1111
+ elsif str_value =~ /^-([A-Za-z]{3,9})(\d{4})$/
1112
+ month_part = $1
1113
+ year_part = $2
1114
+ return {
1115
+ edition: Components::Edition.new(type: "-",
1116
+ additional_text: "#{month_part}#{year_part}"),
1117
+ }
1118
+ # NEW: CS Emergency pattern "e104" or "e104-43" → extract number
1119
+ # This must come BEFORE bare edition check to avoid conflict
1120
+ # CS emergency always has 3+ digit number (e104, not e2)
1121
+ # NOTE: If second_number exists (e104-43 pattern), defer to compound number logic
1122
+ elsif /^e(\d{3,})$/.match?(str_value) && !parsed_hash[:second_number]
1123
+ # Extract emergency number: e104 → 104 (only when no second_number)
1124
+ emergency_num = str_value.sub(/^e/, "")
1125
+ return {
1126
+ first_number: Components::Code.new(number: emergency_num),
1127
+ }
1128
+ # If e104-43 pattern (with second_number), keep e prefix for compound number logic
1129
+ elsif /^e(\d{3,})$/.match?(str_value) && parsed_hash[:second_number]
1130
+ # Keep e104 as-is, let compound number logic handle it
1131
+ return {
1132
+ first_number: Components::Code.new(number: str_value),
1133
+ }
1134
+ # NEW: Bare edition pattern like "100e1" (CS series without year)
1135
+ # ONLY when NO second_number present (to avoid conflict with "123e2-50")
1136
+ # Creates: number="100", Edition(type: "e", id: "1")
1137
+ # Renders: "NBS CS 100e1"
1138
+ # CRITICAL: Skip if edition_dash_year is present - let that handler create Edition with additional_text
1139
+ elsif str_value =~ /^(\d+)e(\d+)$/ && !parsed_hash[:second_number] && !parsed_hash[:edition_dash_year]
1140
+ number_part = $1
1141
+ edition_id = $2
1142
+
1143
+ return {
1144
+ first_number: Components::Code.new(number: number_part),
1145
+ edition: Components::Edition.new(type: "e", id: edition_id),
1146
+ }
1147
+ # NEW: Bare edition pattern "e2" - just edition without number prefix
1148
+ # Creates: Edition(type: "e", id: "2")
1149
+ # Renders: "NBS CIRC e2"
1150
+ # Only matches single or double digit (e1, e2, not e104 which is emergency)
1151
+ elsif str_value =~ /^e(\d{1,2})$/
1152
+ edition_id = $1
1153
+ return {
1154
+ edition: Components::Edition.new(type: "e", id: edition_id),
1155
+ }
1156
+ # Pattern: "13e2rev1908" - edition with revision year-only (NO month)
1157
+ # Creates: Edition(type: "e", id: "2", additional_text: "1908")
1158
+ # Renders: "e2.1908" (DOT separator)
1159
+ elsif str_value =~ /^(\d+)e(\d+)rev(\d{4})$/
1160
+ # CRITICAL: Capture BEFORE any regex method calls!
1161
+ number_part = $1
1162
+ edition_id_part = $2
1163
+ year_part = $3
1164
+ return {
1165
+ first_number: Components::Code.new(number: number_part),
1166
+ edition: Components::Edition.new(type: "e",
1167
+ id: edition_id_part, additional_text: year_part),
1168
+ }
1169
+ # Pattern: "13e2revJune1908" - edition with revision month+year
1170
+ # Creates: Edition(type: "e", id: "2", additional_text: "June1908")
1171
+ # Renders: "e2.June1908" (DOT separator)
1172
+ elsif str_value =~ /^(\d+)e(\d+)(rev.+)$/
1173
+ # CRITICAL: Capture $1, $2, $3 BEFORE calling .sub() which resets them!
1174
+ number_part = $1
1175
+ edition_id_part = $2
1176
+ rev_part = $3
1177
+ # Strip "rev" prefix from additional_text - store only "June1908" or "1908"
1178
+ additional_text = rev_part.sub(/^rev/, "")
1179
+ return {
1180
+ first_number: Components::Code.new(number: number_part),
1181
+ edition: Components::Edition.new(type: "e",
1182
+ id: edition_id_part, additional_text: additional_text),
1183
+ }
1184
+ # NEW: Pattern "24suppJan1924" - supplement with month and year in first_number
1185
+ # Creates: number="24", supplement="Jan1924"
1186
+ elsif str_value =~ /^(\d+)supp([A-Za-z]{3,9})(\d{4})$/
1187
+ number_part = $1
1188
+ month_part = $2
1189
+ year_part = $3
1190
+ return {
1191
+ first_number: Components::Code.new(number: number_part),
1192
+ supplement: "#{month_part}#{year_part}",
1193
+ }
1194
+ # NEW: Pattern "25supp1924" - supplement with year (no dash, no month)
1195
+ # Creates: number="25", supplement="1924"
1196
+ # Renders: "NBS SP 25supp1924"
1197
+ elsif str_value =~ /^(\d+)supp(\d{4})$/
1198
+ number_part = $1
1199
+ year_part = $2
1200
+ return {
1201
+ first_number: Components::Code.new(number: number_part),
1202
+ supplement: year_part,
1203
+ }
1204
+ # NEW: Pattern "25supp-1924" - supplement with dash-year (inline match)
1205
+ # Creates: number="25", supplement="1924"
1206
+ # Renders: "NBS CIRC 25supp-1924"
1207
+ elsif str_value =~ /^(\d+)supp-(\d{4})$/
1208
+ number_part = $1
1209
+ year_part = $2
1210
+ return {
1211
+ first_number: Components::Code.new(number: number_part),
1212
+ supplement: year_part,
1213
+ }
1214
+ # NEW: Pattern "101e2supp" - edition + supplement
1215
+ # Creates: number="101", Edition(type: "e", id: "2"), supplement=""
1216
+ # Renders: "NBS CIRC 101e2supp"
1217
+ elsif str_value =~ /^(\d+)e(\d+)supp$/
1218
+ number_part = $1
1219
+ edition_id = $2
1220
+ return {
1221
+ first_number: Components::Code.new(number: number_part),
1222
+ edition: Components::Edition.new(type: "e", id: edition_id),
1223
+ supplement: "",
1224
+ }
1225
+ end
1226
+ elsif type == :second_number && value.is_a?(Hash) && value[:first_number]
1227
+ # Handle second_number as a hash with first_number context
1228
+ # e.g., for pattern 800-57pt1r4
1229
+ number_part = value[:first_number].to_s
1230
+ part_value = value[:part_value]&.to_s
1231
+ revision_value = value[:revision_value]&.to_s
1232
+ return {
1233
+ first_number: Components::Code.new(number: number_part),
1234
+ part: Components::Part.new(value: part_value),
1235
+ edition: Components::Edition.new(type: "r", id: revision_value),
1236
+ }
1237
+ end
1238
+
1239
+ # Extract revision suffix from number (e.g., "53r5" → "53" + Edition(r, 5))
1240
+ # ENHANCED: Also extract revision with slash-year (e.g., "53r5/1917" → "53" + Edition)
1241
+ # ENHANCED: Also extract revision with 4-digit year (e.g., "1019r1963" → "1019" + Edition)
1242
+ # ENHANCED: Also extract revision with month+year (e.g., "4743rJun1992" → "4743" + Edition)
1243
+
1244
+ # NEW: Extract part suffix from number (e.g., "800-57pt1" → "800-57" + Part(1))
1245
+ # This handles SP series part notation
1246
+ # IMPORTANT: Handle combined part+revision first (e.g., "800-57pt1r4")
1247
+ if str_value =~ /^(.+?)pt(\d+)r(\d+[a-z]?)$/
1248
+ number_part = $1
1249
+ part_value = $2
1250
+ revision_value = $3
1251
+ return {
1252
+ type => Components::Code.new(number: number_part),
1253
+ part: Components::Part.new(type: "pt", value: part_value),
1254
+ edition: Components::Edition.new(type: "r", id: revision_value),
1255
+ }
1256
+ elsif str_value =~ /^(.+?)pt(\d+)$/
1257
+ number_part = $1
1258
+ part_value = $2
1259
+ return {
1260
+ type => Components::Code.new(number: number_part),
1261
+ part: Components::Part.new(type: "pt", value: part_value),
1262
+ }
1263
+ end
1264
+
1265
+ # NEW: Extract volume suffix from number (e.g., "539v10" → "539" + volume="10")
1266
+ # This handles CIRC volume notation
1267
+ if str_value =~ /^(\d+)v(\d+)$/
1268
+ number_part = $1
1269
+ volume_part = $2
1270
+ return {
1271
+ type => Components::Code.new(number: number_part),
1272
+ volume: volume_part,
1273
+ }
1274
+ end
1275
+
1276
+ # REVISION PATTERNS - These must come BEFORE letter suffix to avoid conflicts
1277
+ case str_value
1278
+ when /^(.+?)(r\d+\/\d{4})$/i
1279
+ # Pattern: r6/1925 (revision with slash-year)
1280
+ number_part = $1
1281
+ revision_with_year = $2 # e.g., "r6/1925"
1282
+ # Extract revision and year
1283
+ if revision_with_year =~ /^r(\d+)\/(\d{4})$/
1284
+ revision_id = $1
1285
+ year_part = $2
1286
+ return {
1287
+ type => Components::Code.new(number: number_part),
1288
+ edition: Components::Edition.new(type: "r", id: revision_id,
1289
+ additional_text: year_part),
1290
+ }
1291
+ end
1292
+ when /^(.+?)(r\d{4})$/i
1293
+ # Pattern: r1963 (revision as 4-digit year)
1294
+ number_part = $1
1295
+ year_value = $2.sub(/^r/, "") # Strip 'r' prefix
1296
+ return {
1297
+ type => Components::Code.new(number: number_part),
1298
+ edition: Components::Edition.new(type: "r", id: year_value),
1299
+ }
1300
+ when /^(.+?)(r[A-Za-z]{3,9}\d{4})$/i
1301
+ # Pattern: rJun1992 (revision with month and year)
1302
+ number_part = $1
1303
+ revision_with_date = $2 # e.g., "rJun1992"
1304
+ # Extract month and year
1305
+ if revision_with_date =~ /^r([A-Za-z]{3,9})(\d{4})$/
1306
+ month_part = $1
1307
+ year_part = $2
1308
+ return {
1309
+ type => Components::Code.new(number: number_part),
1310
+ edition: Components::Edition.new(type: "r",
1311
+ id: "#{month_part}#{year_part}"),
1312
+ }
1313
+ end
1314
+ when /^(.+?)(r\d+[a-z]?)$/i
1315
+ # Pattern: r5, r1a (simple revision)
1316
+ number_part = $1
1317
+ revision_value = $2.sub(/^r/, "") # Strip 'r' prefix
1318
+ return {
1319
+ type => Components::Code.new(number: number_part),
1320
+ edition: Components::Edition.new(type: "r", id: revision_value),
1321
+ }
1322
+ when /^(.+?)(?<![a-zA-Z])(r)$/i
1323
+ # Pattern: bare r with no digits (e.g., "800-90r")
1324
+ # Negative lookbehind ensures r is NOT preceded by a letter (avoids matching Ur, Ua, etc.)
1325
+ number_part = $1
1326
+ return {
1327
+ type => Components::Code.new(number: number_part),
1328
+ edition: Components::Edition.new(type: "r", id: "1"),
1329
+ }
1330
+ end
1331
+
1332
+ # NEW: Extract UPPERCASE letter suffix as Part component (e.g., "800-56A" → "800-56" + Part)
1333
+ # IMPORTANT: These patterns come AFTER revision patterns to avoid conflicts
1334
+ # Letter suffixes are UPPERCASE letters A-Z only (no lowercase to avoid revision markers)
1335
+
1336
+ # Pattern: UPPERCASE letter + revision (e.g., "800-56Ar2" → number + Part("", "A") + Edition(r, 2))
1337
+ # NO /i flag - only match uppercase letters!
1338
+ if str_value =~ /^(.+?)([A-Z])(r\d+[a-z]?)$/
1339
+ number_part = $1
1340
+ letter_part = $2
1341
+ revision_part = $3.sub(/^r/, "")
1342
+ return {
1343
+ type => Components::Code.new(number: number_part),
1344
+ part: Components::Part.new(type: "", value: letter_part),
1345
+ edition: Components::Edition.new(type: "r", id: revision_part),
1346
+ }
1347
+ # Pattern: bare UPPERCASE letter suffix (e.g., "800-56A" → number + Part("", "A"))
1348
+ # Only matches uppercase letters - won't match revision markers
1349
+ # IMPORTANT: For MR format preservation, keep letter suffix as part of number
1350
+ # IMPORTANT: For Report, FIPS, IR, and LC series, preserve letter suffix as part of number
1351
+ elsif str_value =~ /^(.+?)([A-Z])$/
1352
+ number_part = $1
1353
+ letter_part = $2
1354
+ # Check if we should preserve letter suffix in number
1355
+ # Check for specific series that need letter suffix preserved
1356
+ is_report = begin
1357
+ parsed_hash[:series].to_s.include?("RPT")
1358
+ rescue StandardError
1359
+ false
1360
+ end
1361
+ is_fips = begin
1362
+ parsed_hash[:series].to_s.include?("FIPS")
1363
+ rescue StandardError
1364
+ false
1365
+ end
1366
+ is_ir = begin
1367
+ parsed_hash[:series].to_s.include?("IR")
1368
+ rescue StandardError
1369
+ false
1370
+ end
1371
+ is_crpl = begin
1372
+ parsed_hash[:series].to_s.include?("CRPL")
1373
+ rescue StandardError
1374
+ false
1375
+ end
1376
+ is_mono = begin
1377
+ parsed_hash[:series].to_s.include?("MONO")
1378
+ rescue StandardError
1379
+ false
1380
+ end
1381
+ is_mp = begin
1382
+ parsed_hash[:series].to_s.include?("MP")
1383
+ rescue StandardError
1384
+ false
1385
+ end
1386
+ # Check for LC but exclude LCIRC (Letter Circular uses LC, not LCIRC)
1387
+ is_lc = begin
1388
+ parsed_hash[:series].to_s.include?("LC") && !parsed_hash[:series].to_s.include?("LCIRC")
1389
+ rescue StandardError
1390
+ false
1391
+ end
1392
+
1393
+ if parsed_hash[:parsed_format] == :mr || is_report || is_fips || is_ir || is_crpl || is_lc || is_mono || is_mp
1394
+ # For MR format, Report, FIPS, IR, CRPL, LC, MONO, and MP, preserve letter suffix as part of number
1395
+ return { type => Components::Code.new(number: str_value) }
1396
+ else
1397
+ # For other formats, extract letter suffix as separate Part component
1398
+ return {
1399
+ type => Components::Code.new(number: number_part),
1400
+ part: Components::Part.new(type: "", value: letter_part),
1401
+ }
1402
+ end
1403
+ end
1404
+
1405
+ Components::Code.new(number: str_value)
1406
+
1407
+ when :crpl_range
1408
+ return nil if value.nil? || value.to_s.strip.empty?
1409
+
1410
+ # For CRPL range patterns like "2_3-1" or "2_3-1A" (with supplement)
1411
+ # Format: X_Y-Z where X,Y,Z are digits, optional trailing letter is supplement
1412
+ # This should split into:
1413
+ # - X → second_number (to combine with first_number as "1-X")
1414
+ # - Y-Z → Part component (with type "pt" for CRPL)
1415
+ # - trailing letter (if present) → Supplement
1416
+ str_value = value.to_s
1417
+
1418
+ # Check for supplement letter suffix (e.g., "2_3-1A" → supplement="A")
1419
+ if str_value =~ /^(\d+)_(\d+-\d+)([A-Z])$/
1420
+ second_num_part = $1 # "2"
1421
+ part_value = $2 # "3-1"
1422
+ supplement_letter = $3 # "A"
1423
+
1424
+ # Return second_number, Part, and Supplement
1425
+ {
1426
+ second_number: Components::Code.new(number: second_num_part),
1427
+ part: Components::Part.new(type: "pt", value: part_value),
1428
+ supplement: supplement_letter,
1429
+ }
1430
+ elsif str_value =~ /^(\d+)_(\d+-\d+)$/
1431
+ # No supplement letter
1432
+ second_num_part = $1 # "2"
1433
+ part_value = $2 # "3-1"
1434
+
1435
+ # Return second_number and Part
1436
+ {
1437
+ second_number: Components::Code.new(number: second_num_part),
1438
+ part: Components::Part.new(type: "pt", value: part_value),
1439
+ }
1440
+ else
1441
+ # Fallback: treat entire value as second_number (shouldn't happen with valid CRPL patterns)
1442
+ Components::Code.new(number: str_value)
1443
+ end
1444
+
1445
+ # ========== V2 COMPONENT CASTING ==========
1446
+
1447
+ when :stage
1448
+ # Stage from nested hash with id and type
1449
+ return nil unless value.is_a?(Hash)
1450
+
1451
+ stage_id = value[:stage_id]&.to_s&.downcase
1452
+ stage_type = value[:stage_type]&.to_s&.downcase
1453
+ return nil if stage_id.nil? || stage_type.nil? || stage_id.empty? || stage_type.empty?
1454
+
1455
+ # Return as hash to set the stage attribute
1456
+ { stage: Components::Stage.new(id: stage_id, type: stage_type) }
1457
+
1458
+ when :stage_id, :stage_type
1459
+ # These are captured by :stage, so skip individual processing
1460
+ nil
1461
+
1462
+ when :parsed_format
1463
+ # Format detection result from parser
1464
+ value&.to_s
1465
+
1466
+ when :translation
1467
+ # V1 TRANSLATION NORMALIZATION
1468
+ return nil if value.nil? || value.to_s.strip.empty?
1469
+
1470
+ code = value.to_s.strip.downcase
1471
+ # Apply normalization map (es → spa, pt → por, etc.)
1472
+ normalized_code = TRANSLATION_MAP[code] || code
1473
+
1474
+ # Return as hash to set translation_component attribute
1475
+ { translation_component: Components::Translation.new(code: normalized_code) }
1476
+
1477
+ when :version
1478
+ # Version component with dotted notation
1479
+ return nil if value.nil? || value.to_s.strip.empty?
1480
+
1481
+ # Return as hash to set version_component attribute
1482
+ { version_component: Components::Version.new(value: value.to_s) }
1483
+
1484
+ when :update
1485
+ # Update component with number, year, and optional month
1486
+ if value.is_a?(Hash)
1487
+ # Convert Parslet slice to regular Hash for reliable key access
1488
+ value_hash = value.to_h
1489
+
1490
+ number = value_hash[:update_number]&.to_s # Don't default to "1"
1491
+ year = value_hash[:update_year]&.to_s # String not integer
1492
+ month = value_hash[:update_month]&.to_s # String not integer
1493
+
1494
+ # Determine prefix from update_prefix key (captured by parser)
1495
+ # If not present, default to "slash" (/Upd format)
1496
+ prefix_str = value_hash[:update_prefix]&.to_s
1497
+ prefix_value = if prefix_str&.include?("-") || prefix_str == "-upd"
1498
+ "dash"
1499
+ else
1500
+ "slash"
1501
+ end
1502
+
1503
+ # Create update with at least number
1504
+ update_obj = Components::Update.new(number: number, year: year,
1505
+ month: month, prefix: prefix_value)
1506
+ {
1507
+ update: update_obj, # Main attribute for tests
1508
+ update_component: update_obj, # V2 component
1509
+ }
1510
+ elsif value.to_s.strip.empty?
1511
+ # Empty update string means "-upd" or "/upd" with no details
1512
+ # Create Update with default number="1" (no year/month)
1513
+ # Check update_prefix key to determine correct prefix format
1514
+ prefix_str = parsed_hash[:update_prefix]&.to_s
1515
+ prefix_value = if prefix_str&.include?("-") || prefix_str == "-upd"
1516
+ "dash"
1517
+ else
1518
+ "slash"
1519
+ end
1520
+ update_obj = Components::Update.new(number: "1", year: nil,
1521
+ month: nil, prefix: prefix_value)
1522
+ {
1523
+ update: update_obj,
1524
+ update_component: update_obj,
1525
+ }
1526
+ else
1527
+ # Simple string value - shouldn't reach here
1528
+ { update: value.to_s.strip } unless value.to_s.strip.empty?
1529
+ end
1530
+
1531
+ when :update_prefix, :update_number, :update_year, :update_month
1532
+ # Captured as part of :update processing
1533
+ nil
1534
+
1535
+ # ========== END V2 COMPONENTS ==========
1536
+
1537
+ when :volume, :section, :appendix, :translation,
1538
+ :errata, :index, :insert, :version
1539
+ return nil if value.nil?
1540
+ return nil if value.is_a?(Array) && value.empty?
1541
+
1542
+ str_value = value.to_s.strip
1543
+ return nil if str_value.empty?
1544
+
1545
+ # For volume, create Volume component from string value
1546
+ # This handles patterns like "v1" that come from parser as simple strings
1547
+ if type == :volume
1548
+ { volume: Components::Volume.new(value: str_value) }
1549
+ else
1550
+ str_value
1551
+ end
1552
+
1553
+ when :revision
1554
+ # Revision MUST be Edition component with type "r"
1555
+ return nil if value.nil? || value.to_s.strip.empty?
1556
+
1557
+ # Handle new structure with :revision_prefix and :revision_id (format preservation)
1558
+ if value.is_a?(Hash) && value[:revision_prefix] && value[:revision_id]
1559
+ prefix = value[:revision_prefix].to_s
1560
+ id = value[:revision_id].to_s.strip
1561
+
1562
+ # Normalize bare "r" → "r1"
1563
+ revision_id = if id.empty? || id == "r" || id == "R"
1564
+ "1"
1565
+ # Handle "r4", "R5", "4" etc. (but prefix already has the r/rev/etc.)
1566
+ elsif id =~ /^(\d+[a-z]?)$/
1567
+ $1
1568
+ else
1569
+ id
1570
+ end
1571
+
1572
+ # Return Edition component with original_prefix for format preservation
1573
+ {
1574
+ edition: Components::Edition.new(type: "r", id: revision_id,
1575
+ original_prefix: prefix),
1576
+ }
1577
+ else
1578
+ # Legacy handling: revision as simple string value
1579
+ str_value = value.to_s.strip
1580
+
1581
+ # Handle bare "r" → normalize to "r1"
1582
+ revision_id = if str_value.empty? || str_value == "r" || str_value == "R"
1583
+ "1"
1584
+ # Handle "r4", "R5", "4" etc.
1585
+ elsif str_value =~ /^[rR]?(\d+[a-z]?)$/
1586
+ $1
1587
+ else
1588
+ str_value
1589
+ end
1590
+
1591
+ # Return Edition component (no original_prefix available)
1592
+ {
1593
+ edition: Components::Edition.new(type: "r", id: revision_id),
1594
+ }
1595
+ end
1596
+
1597
+ when :revision_year, :revision_month
1598
+ # When revision_year comes from parser as separate element (e.g., "1019 r1963")
1599
+ # Create Edition component
1600
+ if type == :revision_year
1601
+ year_value = value.to_s.strip
1602
+ # Check if this should be an Edition component or legacy revision_year
1603
+ # If revision_month is also present, use legacy attributes for "revJune1908" pattern
1604
+ if parsed_hash[:revision_month]
1605
+ # Legacy: revision with month - keep as revision_year/revision_month
1606
+ year_value
1607
+ else
1608
+ # V2: revision with year only - create Edition component
1609
+ {
1610
+ edition: Components::Edition.new(type: "r", id: year_value),
1611
+ }
1612
+ end
1613
+ else
1614
+ # revision_month - preserve as string for legacy rendering
1615
+ return nil if value.nil? || value.to_s.strip.empty?
1616
+
1617
+ value.to_s.strip
1618
+ end
1619
+
1620
+ when :edition_year_separate
1621
+ # NEW: Edition year from "e2-1915" pattern (captured separately by parser)
1622
+ # This comes with first_number like "11e2" and separate year "1915"
1623
+ # Already handled in first_number regex matching above, but if it reaches here
1624
+ # as a separate capture, we need to process it
1625
+ return nil if value.nil? || value.to_s.strip.empty?
1626
+
1627
+ value.to_s # Return as string for potential use
1628
+
1629
+ when :historical_month
1630
+ # NEW: Historical month from "-April1909" pattern
1631
+ # Handled in first_number pattern matching, but return as string if separate
1632
+ return nil if value.nil? || value.to_s.strip.empty?
1633
+
1634
+ value.to_s
1635
+
1636
+ when :historical_year
1637
+ # NEW: Historical year from "-April1909" pattern
1638
+ # Handled in first_number pattern matching, but return as string if separate
1639
+ return nil if value.nil? || value.to_s.strip.empty?
1640
+
1641
+ value.to_s
1642
+
1643
+ when :supplement_year
1644
+ # NEW: Supplement year from "supp-1924" pattern (captured separately by parser)
1645
+ # This comes with first_number like "25supp" and separate year "1924"
1646
+ # Already handled in first_number regex matching above, but if it reaches here
1647
+ # as a separate capture, return as supplement value
1648
+ return nil if value.nil? || value.to_s.strip.empty?
1649
+
1650
+ { supplement: value.to_s } # Return as supplement attribute
1651
+
1652
+ when :supplement
1653
+ handle_supplement_cast(value)
1654
+
1655
+ when :supplement_date_range
1656
+ return nil unless value.is_a?(Hash)
1657
+
1658
+ month_start = value[:supp_month_start]&.to_s
1659
+ year_start = value[:supp_year_start]&.to_s
1660
+ month_end = value[:supp_month_end]&.to_s
1661
+ year_end = value[:supp_year_end]&.to_s
1662
+
1663
+ {
1664
+ supplement_date_range_start: (month_start && year_start ? "#{month_start}#{year_start}" : nil),
1665
+ supplement_date_range_end: (month_end && year_end ? "#{month_end}#{year_end}" : nil),
1666
+ }
1667
+
1668
+ when :supplement_date
1669
+ return nil unless value.is_a?(Hash)
1670
+
1671
+ month = value[:supp_month]&.to_s
1672
+ year = value[:supp_year]&.to_s
1673
+
1674
+ month && year ? "#{month}#{year}" : nil
1675
+
1676
+ when :supplement_slash_year
1677
+ return nil unless value.is_a?(Hash)
1678
+
1679
+ number = value[:supp_number]&.to_s
1680
+ year = value[:supp_year]&.to_s
1681
+
1682
+ number && year ? "#{number}/#{year}" : nil
1683
+
1684
+ when :supplement_with_rev
1685
+ { supplement: "", supplement_has_revision: true }
1686
+
1687
+ when :supp_year
1688
+ # Parser extracts supplement year from patterns like "187supp1924"
1689
+ # This should set the supplement attribute with the year value
1690
+ { supplement: value.to_s }
1691
+
1692
+ # ========== V2 EDITION COMPONENT ==========
1693
+
1694
+ when :edition_e_date
1695
+ # Edition with "e" prefix + 6-digit date (YYYYMM): e199206, e202103
1696
+ # Used for IR revision+month patterns after preprocessing: "4743rJun1992" → "4743e199206"
1697
+ return nil unless value.is_a?(Hash) && value[:edition_date]
1698
+
1699
+ edition_date = value[:edition_date].to_s
1700
+ # Parse 6-digit date as YYYYMM
1701
+ # Store as id directly - renders as "e199206"
1702
+ {
1703
+ edition: Components::Edition.new(type: "e", id: edition_date),
1704
+ edition_component: Components::Edition.new(type: "e",
1705
+ id: edition_date),
1706
+ }
1707
+
1708
+ when :edition_e
1709
+ # Edition with "e" prefix: e2, e2021
1710
+ return nil unless value.is_a?(Hash) && value[:edition_id]
1711
+
1712
+ edition_id = value[:edition_id].to_s
1713
+
1714
+ {
1715
+ edition: Components::Edition.new(type: "e", id: edition_id),
1716
+ edition_component: Components::Edition.new(type: "e",
1717
+ id: edition_id),
1718
+ }
1719
+
1720
+ when :edition_r
1721
+ # Revision with "r" prefix: r5, r2021
1722
+ return nil unless value.is_a?(Hash) && value[:edition_id]
1723
+
1724
+ edition_id = value[:edition_id].to_s
1725
+
1726
+ {
1727
+ edition: Components::Edition.new(type: "r", id: edition_id),
1728
+ edition_component: Components::Edition.new(type: "r",
1729
+ id: edition_id),
1730
+ revision: "r#{edition_id}", # Also set revision string attribute for compatibility
1731
+ }
1732
+
1733
+ when :edition_r_no_space
1734
+ # Revision with "r" prefix (no space pattern): r2, r5
1735
+ # Used for patterns like "800-56Ar2" where edition is "r2"
1736
+ return nil unless value.is_a?(Hash) && value[:edition_id]
1737
+
1738
+ edition_id = value[:edition_id].to_s
1739
+
1740
+ {
1741
+ edition: Components::Edition.new(type: "r", id: edition_id),
1742
+ edition_component: Components::Edition.new(type: "r",
1743
+ id: edition_id),
1744
+ revision: "r#{edition_id}", # Also set revision string attribute for compatibility
1745
+ }
1746
+
1747
+ when :edition_rev
1748
+ # Revision with "rev" prefix (verbose): rev2013, rev 2013
1749
+ return nil unless value.is_a?(Hash) && value[:edition_id]
1750
+
1751
+ edition_id = value[:edition_id].to_s
1752
+
1753
+ {
1754
+ edition: Components::Edition.new(type: "r", id: edition_id),
1755
+ edition_component: Components::Edition.new(type: "r",
1756
+ id: edition_id),
1757
+ revision: "r#{edition_id}", # Also set revision string attribute for compatibility
1758
+ }
1759
+
1760
+ when :edition_r_letter
1761
+ # Revision with "r" prefix and letter suffix: r1a, r2b (for SP patterns like 800-22r1a)
1762
+ return nil unless value.is_a?(Hash) && value[:edition_id] && value[:edition_letter]
1763
+
1764
+ edition_id = value[:edition_id].to_s
1765
+ edition_letter = value[:edition_letter].to_s.downcase
1766
+
1767
+ {
1768
+ edition: Components::Edition.new(type: "r", id: edition_id,
1769
+ additional_text: edition_letter),
1770
+ edition_component: Components::Edition.new(type: "r",
1771
+ id: edition_id,
1772
+ additional_text: edition_letter),
1773
+ revision: "r#{edition_id}#{edition_letter}", # Also set revision string attribute for compatibility
1774
+ }
1775
+
1776
+ when :edition_r_letter_only
1777
+ # Revision with "r" prefix and only letter (no digit): ra, rb (for SP patterns like 800-27ra)
1778
+ return nil unless value.is_a?(Hash) && value[:edition_letter]
1779
+
1780
+ edition_letter = value[:edition_letter].to_s.downcase
1781
+
1782
+ {
1783
+ edition: Components::Edition.new(type: "r", id: edition_letter),
1784
+ edition_component: Components::Edition.new(type: "r",
1785
+ id: edition_letter),
1786
+ revision: "r#{edition_letter}", # Also set revision string attribute for compatibility
1787
+ }
1788
+
1789
+ when :edition_historical
1790
+ # Historical with "-" prefix: -3, -4
1791
+ return nil unless value.is_a?(Hash) && value[:edition_id]
1792
+
1793
+ edition_id = value[:edition_id].to_s
1794
+
1795
+ {
1796
+ edition: Components::Edition.new(type: "-", id: edition_id),
1797
+ edition_component: Components::Edition.new(type: "-",
1798
+ id: edition_id),
1799
+ }
1800
+
1801
+ when :edition_r_with_space_letter
1802
+ # Revision with "r" prefix, space, and letter: r 5A (format preservation)
1803
+ # Used for patterns like "NIST SP 800-53 r5A"
1804
+ # NOTE: If there's an update component, the space was added by preprocessing
1805
+ return nil unless value.is_a?(Hash) && value[:edition_id] && value[:edition_letter]
1806
+
1807
+ edition_id = value[:edition_id].to_s
1808
+ edition_letter = value[:edition_letter].to_s.upcase
1809
+
1810
+ # Check if this is an embedded edition with update (space added by preprocessing)
1811
+ has_update = parsed_hash[:update_prefix] || parsed_hash[:update]
1812
+
1813
+ if has_update
1814
+ # No original_prefix - space was added by preprocessing
1815
+ {
1816
+ edition: Components::Edition.new(type: "r", id: edition_id,
1817
+ additional_text: edition_letter),
1818
+ edition_component: Components::Edition.new(type: "r",
1819
+ id: edition_id,
1820
+ additional_text: edition_letter),
1821
+ revision: "r#{edition_id}#{edition_letter}",
1822
+ }
1823
+ else
1824
+ # Space was in original input - preserve format
1825
+ {
1826
+ edition: Components::Edition.new(type: "r", id: edition_id,
1827
+ additional_text: edition_letter,
1828
+ original_prefix: " r"),
1829
+ edition_component: Components::Edition.new(type: "r",
1830
+ id: edition_id,
1831
+ additional_text: edition_letter,
1832
+ original_prefix: " r"),
1833
+ revision: "r#{edition_id}#{edition_letter}",
1834
+ }
1835
+ end
1836
+
1837
+ when :edition_r_with_space
1838
+ # Revision with "r" prefix and space: r 5 (format preservation)
1839
+ # Used for patterns like "NIST SP 800-53 r5"
1840
+ # NOTE: If there's an update component, the space was added by preprocessing
1841
+ # for patterns like "8115r1/upd" → "8115 r1/upd", so don't set original_prefix
1842
+ return nil unless value.is_a?(Hash) && value[:edition_id]
1843
+
1844
+ edition_id = value[:edition_id].to_s
1845
+
1846
+ # Check if this is an embedded edition with update (space added by preprocessing)
1847
+ # Patterns like "8115r1/upd" become "8115 r1/upd" after preprocessing
1848
+ has_update = parsed_hash[:update_prefix] || parsed_hash[:update]
1849
+
1850
+ if has_update
1851
+ # No original_prefix - space was added by preprocessing
1852
+ {
1853
+ edition: Components::Edition.new(type: "r", id: edition_id),
1854
+ edition_component: Components::Edition.new(type: "r",
1855
+ id: edition_id),
1856
+ revision: "r#{edition_id}",
1857
+ }
1858
+ else
1859
+ # Space was in original input - preserve format
1860
+ {
1861
+ edition: Components::Edition.new(type: "r", id: edition_id,
1862
+ original_prefix: " r"),
1863
+ edition_component: Components::Edition.new(type: "r",
1864
+ id: edition_id,
1865
+ original_prefix: " r"),
1866
+ revision: "r#{edition_id}",
1867
+ }
1868
+ end
1869
+
1870
+ when :edition_id
1871
+ # Captured by edition_e, edition_r, edition_rev, edition_historical
1872
+ nil
1873
+
1874
+ when :edition_date
1875
+ # Captured by edition_e_date
1876
+ nil
1877
+
1878
+ # ========== LEGACY EDITION (for migration) ==========
1879
+
1880
+ when :legacy_edition
1881
+ # Legacy edition patterns - will be phased out
1882
+ # For now, map to old edition_year/edition_month attributes
1883
+ nil # Handled by existing edition_year logic below
1884
+
1885
+ when :edition_month, :edition_year, :edition_day, :edition_has_rev
1886
+ # These work together: edition_month + edition_year → single edition ID
1887
+ # Skip processing if this is edition_month alone (will be processed with edition_year)
1888
+ return nil if type == :edition_month
1889
+
1890
+ # Process edition_year, combining with edition_month if present
1891
+ return nil if value.nil? || value.to_s.strip.empty?
1892
+
1893
+ # Build the edition ID from year and optional month
1894
+ edition_id = value.to_s # Start with year (e.g., "1985")
1895
+
1896
+ # Add month if present (e.g., "Mar" → "03", so "1985" + "03" = "198503")
1897
+ # For FIPS with day: "Sep30/1977" → "19770930" (year + month + day)
1898
+ if parsed_hash[:edition_month]
1899
+ month_str = parsed_hash[:edition_month].to_s
1900
+ month_num = Date::ABBR_MONTHNAMES.index(month_str) ||
1901
+ Date::MONTHNAMES.index(month_str) ||
1902
+ month_str.to_i
1903
+ if month_num&.positive?
1904
+ # Check if this is FIPS series - FIPS uses number format (e198503), not month abbreviations
1905
+ # For historical NBS documents, preserve month name: "April1909" not "190904"
1906
+ is_fips = parsed_hash[:series]&.to_s == "FIPS"
1907
+ if !is_fips && month_str.match?(/^[A-Z][a-z]+/) && edition_id.to_s.match?(/^\d{4}$/)
1908
+ # Historical NBS month+year format: preserve month name, use "-" type for special rendering
1909
+ edition_obj = Components::Edition.new(
1910
+ type: "-",
1911
+ id: "",
1912
+ additional_text: "#{month_str}#{edition_id}",
1913
+ )
1914
+ return {
1915
+ edition: edition_obj,
1916
+ edition_component: edition_obj,
1917
+ edition_year: edition_id.to_s,
1918
+ }
1919
+ else
1920
+ # Modern format (and FIPS): combine year and month as single number: 1985 + 03 = 198503
1921
+ edition_id = "#{edition_id}#{format('%02d', month_num)}"
1922
+
1923
+ # For FIPS with day, append day as well: "Sep30/1977" → "19770930"
1924
+ if is_fips && parsed_hash[:edition_day]
1925
+ day_num = parsed_hash[:edition_day].to_s.to_i
1926
+ if day_num.positive? && day_num <= 31
1927
+ edition_id = "#{edition_id}#{format('%02d', day_num)}"
1928
+ end
1929
+ end
1930
+ end
1931
+ end
1932
+ end
1933
+
1934
+ # Create Edition component with type="e" (edition) and combined ID
1935
+ edition_obj = Components::Edition.new(type: "e", id: edition_id)
1936
+
1937
+ # Return as hash to set edition and edition_year
1938
+ {
1939
+ edition: edition_obj, # Main attribute for tests
1940
+ edition_component: edition_obj, # V2 component
1941
+ edition_year: value.to_s, # Keep string for render logic
1942
+ }
1943
+
1944
+ when :part
1945
+ # Part component - handle part number with optional addendum
1946
+ return nil if value.nil? || value.to_s.strip.empty?
1947
+
1948
+ str_value = value.to_s.strip
1949
+
1950
+ # Pattern: "1adde1" → Part(value: "1"), addendum=true
1951
+ # Note: eN after add is discarded (not included in output per fixture)
1952
+ if str_value =~ /^(\d+)add(e\d+)$/
1953
+ {
1954
+ part: Components::Part.new(type: "pt", value: $1),
1955
+ addendum: "true",
1956
+ }
1957
+ elsif str_value =~ /^(\d+)add/
1958
+ {
1959
+ part: Components::Part.new(type: "pt", value: $1),
1960
+ addendum: "true",
1961
+ }
1962
+ else
1963
+ # Just a part number - return Part component with pt type
1964
+ { part: Components::Part.new(type: "pt", value: str_value) }
1965
+ end
1966
+
1967
+ when :part_extracted
1968
+ # Legacy - this is now handled by :part
1969
+ nil
1970
+
1971
+ when :edition_letter
1972
+ return nil if value.nil? || value.to_s.strip.empty?
1973
+
1974
+ value.to_s
1975
+
1976
+ when :public_draft
1977
+ return nil if value.nil?
1978
+
1979
+ value.to_s
1980
+
1981
+ when :draft
1982
+ # Extract draft number from "-draft N" pattern for pd rendering
1983
+ return nil if value.nil?
1984
+
1985
+ str_value = value.to_s.strip
1986
+ return nil if str_value.empty?
1987
+
1988
+ # Pattern: " -draft 2" or "-draft 2" → extract "2" for pd rendering
1989
+ if str_value =~ /^\s*-draft\s+(\d+)$/
1990
+ { draft_number: $1 }
1991
+ # Pattern: " 2pd" → already in pd format
1992
+ elsif str_value =~ /^\s*(\d+)pd$/
1993
+ { public_draft: $1 }
1994
+ # Other patterns (parenthetical, simple -draft)
1995
+ else
1996
+ str_value
1997
+ end
1998
+
1999
+ when :update
2000
+ handle_update_cast(value)
2001
+
2002
+ when :update_number, :update_year
2003
+ return nil if value.nil? || value.to_s.strip.empty?
2004
+
2005
+ value.to_s
2006
+
2007
+ when :addendum
2008
+ handle_addendum_cast(value)
2009
+
2010
+ when :addendum_number
2011
+ return nil if value.nil? || value.to_s.strip.empty?
2012
+
2013
+ value.to_s
2014
+
2015
+ when :supplement_suffix
2016
+ # Return as hash to set supplement attribute (not supplement_suffix)
2017
+ { supplement: value.to_s }
2018
+
2019
+ when :date
2020
+ # Date component per NIST spec
2021
+ return nil unless value.is_a?(Hash)
2022
+
2023
+ # NEW: Check if this is historical edition pattern ("-April1909")
2024
+ # Parser captures as date with month + year, but semantically it's an edition
2025
+ if value[:date_month] && value[:date_year] && !value[:date_day]
2026
+ month_str = value[:date_month].to_s
2027
+ year_str = value[:date_year].to_s
2028
+ # If month is a word like "April", this is historical edition format
2029
+ if month_str.match?(/^[A-Za-z]+$/)
2030
+ return {
2031
+ edition: Components::Edition.new(type: "-",
2032
+ additional_text: "#{month_str}#{year_str}"),
2033
+ }
2034
+ end
2035
+ end
2036
+
2037
+ # Regular date processing
2038
+ value[:date_year]&.to_s
2039
+ value[:date_month]&.to_s
2040
+ value[:date_day]&.to_s
2041
+
2042
+ else
2043
+ # Unknown types: return the original value for default processing
2044
+ # This allows hashes with arbitrary structures to be processed
2045
+ # e.g., second_number hash with number_only and edition_id
2046
+ value.is_a?(Hash) ? value : nil
2047
+ end
2048
+ end
2049
+
2050
+ # Handle supplement casting with all its variants
2051
+ def handle_supplement_cast(value)
2052
+ return nil unless value
2053
+
2054
+ if value.is_a?(Array) && value.empty?
2055
+ # Empty array means "supp" was present but no suffix
2056
+ ""
2057
+ else
2058
+ str_value = value.to_s.strip
2059
+ str_value.empty? ? nil : str_value
2060
+ end
2061
+ end
2062
+
2063
+ # Handle update casting (number and year)
2064
+ def handle_update_cast(value)
2065
+ if value.is_a?(Hash)
2066
+ {
2067
+ update_number: value[:update_number]&.to_s,
2068
+ update_year: value[:update_year]&.to_s,
2069
+ }.compact
2070
+ elsif value.to_s.strip.empty?
2071
+ # Empty update string (just "-upd" with no details)
2072
+ # Don't create update component - not enough data
2073
+ nil
2074
+ else
2075
+ str_value = value.to_s.strip
2076
+ str_value.empty? ? nil : str_value
2077
+ end
2078
+ end
2079
+
2080
+ # Handle addendum casting (number)
2081
+ def handle_addendum_cast(value)
2082
+ if value.is_a?(Hash)
2083
+ addendum_num = value[:addendum_number]&.to_s&.strip
2084
+ if addendum_num && !addendum_num.empty?
2085
+ { addendum_number: addendum_num }
2086
+ else
2087
+ { addendum: "true" }
2088
+ end
2089
+ else
2090
+ str_value = value.to_s.strip
2091
+ if str_value.empty?
2092
+ { addendum: "true" }
2093
+ else
2094
+ { addendum_number: str_value }
2095
+ end
2096
+ end
2097
+ end
2098
+ end
2099
+ end
2100
+ end