pubid 2.0.0.pre.alpha.1 → 2.0.0.pre.alpha.3

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 (463) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +5 -1
  3. data/data/nist/update_codes.yaml +27 -0
  4. data/lib/pubid/amca/builder.rb +2 -2
  5. data/lib/pubid/amca/identifier.rb +7 -0
  6. data/lib/pubid/amca/identifiers/base.rb +0 -26
  7. data/lib/pubid/amca/identifiers/interpretation.rb +0 -17
  8. data/lib/pubid/amca/identifiers/publication.rb +0 -13
  9. data/lib/pubid/amca/renderer.rb +82 -0
  10. data/lib/pubid/amca/single_identifier.rb +0 -23
  11. data/lib/pubid/amca/urn_parser.rb +28 -0
  12. data/lib/pubid/amca.rb +42 -1
  13. data/lib/pubid/ansi/builder.rb +5 -3
  14. data/lib/pubid/ansi/identifier.rb +1 -1
  15. data/lib/pubid/ansi/identifiers/american_national_standard.rb +2 -1
  16. data/lib/pubid/ansi/identifiers/standard.rb +2 -3
  17. data/lib/pubid/ansi/renderer.rb +53 -0
  18. data/lib/pubid/ansi/single_identifier.rb +2 -31
  19. data/lib/pubid/ansi/urn_generator.rb +3 -38
  20. data/lib/pubid/ansi/urn_parser.rb +23 -0
  21. data/lib/pubid/ansi.rb +38 -3
  22. data/lib/pubid/api/builder.rb +29 -74
  23. data/lib/pubid/api/identifier.rb +0 -4
  24. data/lib/pubid/api/identifiers/base.rb +0 -2
  25. data/lib/pubid/api/identifiers/bulletin.rb +0 -2
  26. data/lib/pubid/api/identifiers/continuous_operations_standard.rb +0 -2
  27. data/lib/pubid/api/identifiers/mpms.rb +1 -17
  28. data/lib/pubid/api/identifiers/publication.rb +0 -2
  29. data/lib/pubid/api/identifiers/recommended_practice.rb +0 -2
  30. data/lib/pubid/api/identifiers/specification.rb +0 -2
  31. data/lib/pubid/api/identifiers/standard.rb +0 -2
  32. data/lib/pubid/api/identifiers/technical_report.rb +0 -2
  33. data/lib/pubid/api/identifiers/typeless_standard.rb +1 -14
  34. data/lib/pubid/api/identifiers.rb +18 -0
  35. data/lib/pubid/api/renderer.rb +89 -0
  36. data/lib/pubid/api/single_identifier.rb +1 -13
  37. data/lib/pubid/api/urn_generator.rb +0 -18
  38. data/lib/pubid/api/urn_parser.rb +35 -0
  39. data/lib/pubid/api.rb +51 -5
  40. data/lib/pubid/ashrae/builder.rb +3 -3
  41. data/lib/pubid/ashrae/identifier.rb +6 -0
  42. data/lib/pubid/ashrae/identifiers/addenda_package.rb +0 -10
  43. data/lib/pubid/ashrae/identifiers/addendum.rb +0 -19
  44. data/lib/pubid/ashrae/identifiers/base.rb +3 -0
  45. data/lib/pubid/ashrae/identifiers/combined_addenda.rb +0 -15
  46. data/lib/pubid/ashrae/identifiers/errata.rb +0 -10
  47. data/lib/pubid/ashrae/identifiers/interpretation.rb +0 -10
  48. data/lib/pubid/ashrae/renderer.rb +117 -0
  49. data/lib/pubid/ashrae/single_identifier.rb +0 -13
  50. data/lib/pubid/ashrae/urn_generator.rb +0 -8
  51. data/lib/pubid/ashrae/urn_parser.rb +27 -0
  52. data/lib/pubid/ashrae.rb +42 -1
  53. data/lib/pubid/asme/components/code.rb +10 -2
  54. data/lib/pubid/asme/identifiers/base.rb +0 -60
  55. data/lib/pubid/asme/renderer.rb +66 -0
  56. data/lib/pubid/asme/urn_parser.rb +31 -0
  57. data/lib/pubid/asme.rb +42 -1
  58. data/lib/pubid/astm/components/code.rb +9 -0
  59. data/lib/pubid/{jis → astm}/components.rb +1 -1
  60. data/lib/pubid/astm/identifiers/adjunct.rb +0 -8
  61. data/lib/pubid/astm/identifiers/data_series.rb +0 -14
  62. data/lib/pubid/astm/identifiers/iso_dual_published.rb +9 -34
  63. data/lib/pubid/astm/identifiers/manual.rb +0 -27
  64. data/lib/pubid/astm/identifiers/monograph.rb +0 -14
  65. data/lib/pubid/astm/identifiers/research_report.rb +0 -7
  66. data/lib/pubid/astm/identifiers/standard.rb +0 -39
  67. data/lib/pubid/astm/identifiers/technical_report.rb +0 -13
  68. data/lib/pubid/astm/identifiers/work_in_progress.rb +0 -11
  69. data/lib/pubid/astm/identifiers.rb +18 -0
  70. data/lib/pubid/astm/renderer.rb +172 -0
  71. data/lib/pubid/astm/single_identifier.rb +0 -10
  72. data/lib/pubid/astm/urn_parser.rb +30 -0
  73. data/lib/pubid/astm.rb +39 -27
  74. data/lib/pubid/bsi/builder.rb +21 -12
  75. data/lib/pubid/bsi/identifier.rb +8 -2
  76. data/lib/pubid/bsi/identifiers/addendum_document.rb +3 -33
  77. data/lib/pubid/bsi/identifiers/adopted_european_norm.rb +11 -47
  78. data/lib/pubid/bsi/identifiers/adopted_international_standard.rb +11 -38
  79. data/lib/pubid/bsi/identifiers/aerospace_standard.rb +3 -53
  80. data/lib/pubid/bsi/identifiers/amendment.rb +3 -19
  81. data/lib/pubid/bsi/identifiers/british_industrial_practice.rb +2 -4
  82. data/lib/pubid/bsi/identifiers/british_standard.rb +2 -1
  83. data/lib/pubid/bsi/identifiers/bundled_identifier.rb +3 -84
  84. data/lib/pubid/bsi/identifiers/committee_document.rb +1 -14
  85. data/lib/pubid/bsi/identifiers/consolidated_identifier.rb +3 -84
  86. data/lib/pubid/bsi/identifiers/corrigendum.rb +3 -7
  87. data/lib/pubid/bsi/identifiers/detailed_specification.rb +1 -34
  88. data/lib/pubid/bsi/identifiers/disc.rb +1 -27
  89. data/lib/pubid/bsi/identifiers/draft_document.rb +3 -44
  90. data/lib/pubid/bsi/identifiers/electronic_book.rb +3 -36
  91. data/lib/pubid/bsi/identifiers/expert_commentary.rb +3 -15
  92. data/lib/pubid/bsi/identifiers/explanatory_supplement.rb +1 -45
  93. data/lib/pubid/bsi/identifiers/flex.rb +1 -33
  94. data/lib/pubid/bsi/identifiers/handbook.rb +2 -13
  95. data/lib/pubid/bsi/identifiers/index.rb +1 -30
  96. data/lib/pubid/bsi/identifiers/method.rb +1 -39
  97. data/lib/pubid/bsi/identifiers/national_annex.rb +5 -27
  98. data/lib/pubid/bsi/identifiers/practice_guide.rb +2 -4
  99. data/lib/pubid/bsi/identifiers/publicly_available_specification.rb +3 -52
  100. data/lib/pubid/bsi/identifiers/published_document.rb +3 -52
  101. data/lib/pubid/bsi/identifiers/section.rb +1 -28
  102. data/lib/pubid/bsi/identifiers/set.rb +3 -17
  103. data/lib/pubid/bsi/identifiers/standalone_amendment.rb +1 -7
  104. data/lib/pubid/bsi/identifiers/supplement_document.rb +3 -21
  105. data/lib/pubid/bsi/identifiers/supplementary_index.rb +1 -44
  106. data/lib/pubid/bsi/identifiers/technical_specification.rb +3 -45
  107. data/lib/pubid/bsi/identifiers/test_method.rb +1 -30
  108. data/lib/pubid/bsi/identifiers/value_added_publication.rb +3 -14
  109. data/lib/pubid/bsi/identifiers.rb +0 -1
  110. data/lib/pubid/bsi/renderer.rb +1050 -0
  111. data/lib/pubid/bsi/single_identifier.rb +6 -70
  112. data/lib/pubid/bsi/urn_generator.rb +2 -3
  113. data/lib/pubid/bsi/urn_parser.rb +52 -0
  114. data/lib/pubid/bsi.rb +224 -1
  115. data/lib/pubid/builder/base.rb +57 -10
  116. data/lib/pubid/bundled_identifier.rb +0 -1
  117. data/lib/pubid/ccsds/builder.rb +4 -3
  118. data/lib/pubid/ccsds/identifier.rb +66 -1
  119. data/lib/pubid/ccsds/identifiers/base.rb +11 -50
  120. data/lib/pubid/ccsds/identifiers/corrigendum.rb +7 -6
  121. data/lib/pubid/ccsds/parser.rb +4 -2
  122. data/lib/pubid/ccsds/single_identifier.rb +4 -1
  123. data/lib/pubid/ccsds/supplement_identifier.rb +15 -11
  124. data/lib/pubid/ccsds/urn_generator.rb +3 -3
  125. data/lib/pubid/ccsds/urn_parser.rb +20 -0
  126. data/lib/pubid/ccsds.rb +39 -1
  127. data/lib/pubid/cen_cenelec/builder.rb +12 -14
  128. data/lib/pubid/cen_cenelec/identifier.rb +8 -2
  129. data/lib/pubid/cen_cenelec/identifiers/adopted_european_norm.rb +13 -4
  130. data/lib/pubid/cen_cenelec/identifiers/amendment.rb +2 -8
  131. data/lib/pubid/cen_cenelec/identifiers/base.rb +5 -41
  132. data/lib/pubid/cen_cenelec/identifiers/cen_report.rb +2 -1
  133. data/lib/pubid/cen_cenelec/identifiers/cen_workshop_agreement.rb +2 -1
  134. data/lib/pubid/cen_cenelec/identifiers/consolidated_identifier.rb +2 -25
  135. data/lib/pubid/cen_cenelec/identifiers/corrigendum.rb +2 -13
  136. data/lib/pubid/cen_cenelec/identifiers/european_norm.rb +2 -1
  137. data/lib/pubid/cen_cenelec/identifiers/european_prestandard.rb +4 -7
  138. data/lib/pubid/cen_cenelec/identifiers/european_specification.rb +2 -1
  139. data/lib/pubid/cen_cenelec/identifiers/fragment.rb +2 -2
  140. data/lib/pubid/cen_cenelec/identifiers/harmonization_document.rb +2 -1
  141. data/lib/pubid/cen_cenelec/identifiers/technical_report.rb +2 -1
  142. data/lib/pubid/cen_cenelec/identifiers/technical_specification.rb +2 -1
  143. data/lib/pubid/cen_cenelec/renderer.rb +261 -0
  144. data/lib/pubid/cen_cenelec/single_identifier.rb +11 -89
  145. data/lib/pubid/cen_cenelec/urn_generator.rb +6 -6
  146. data/lib/pubid/cen_cenelec/urn_parser.rb +28 -0
  147. data/lib/pubid/cen_cenelec.rb +168 -1
  148. data/lib/pubid/cie/components/code.rb +8 -0
  149. data/lib/pubid/cie/identifier.rb +6 -4
  150. data/lib/pubid/cie/urn_parser.rb +28 -0
  151. data/lib/pubid/cie.rb +43 -1
  152. data/lib/pubid/components/adoption.rb +104 -0
  153. data/lib/pubid/components/code.rb +22 -8
  154. data/lib/pubid/components/date.rb +23 -16
  155. data/lib/pubid/components/edition.rb +9 -6
  156. data/lib/pubid/components/iteration.rb +32 -0
  157. data/lib/pubid/components/language.rb +6 -4
  158. data/lib/pubid/components/locality.rb +10 -1
  159. data/lib/pubid/components/publisher.rb +9 -6
  160. data/lib/pubid/components/relationship.rb +151 -0
  161. data/lib/pubid/components/stage.rb +5 -14
  162. data/lib/pubid/components/supplement.rb +184 -0
  163. data/lib/pubid/components/type.rb +5 -15
  164. data/lib/pubid/components/typed_stage.rb +11 -8
  165. data/lib/pubid/components.rb +4 -0
  166. data/lib/pubid/core/update_codes.rb +28 -7
  167. data/lib/pubid/csa/identifier.rb +0 -3
  168. data/lib/pubid/csa/identifiers/base.rb +2 -122
  169. data/lib/pubid/csa/identifiers/cec.rb +2 -101
  170. data/lib/pubid/csa/identifiers/series.rb +2 -102
  171. data/lib/pubid/csa/renderer.rb +292 -0
  172. data/lib/pubid/csa/urn_generator.rb +1 -1
  173. data/lib/pubid/csa/urn_parser.rb +33 -0
  174. data/lib/pubid/csa.rb +42 -1
  175. data/lib/pubid/etsi/components/code.rb +9 -2
  176. data/lib/pubid/etsi/identifiers/base.rb +1 -4
  177. data/lib/pubid/etsi/identifiers/supplement_identifier.rb +2 -9
  178. data/lib/pubid/etsi/renderer.rb +42 -0
  179. data/lib/pubid/etsi/urn_parser.rb +34 -0
  180. data/lib/pubid/etsi.rb +42 -1
  181. data/lib/pubid/export/exporter.rb +4 -46
  182. data/lib/pubid/export/flavor_exporter.rb +111 -278
  183. data/lib/pubid/export.rb +0 -6
  184. data/lib/pubid/identifier.rb +10 -18
  185. data/lib/pubid/identifier_facade.rb +114 -0
  186. data/lib/pubid/identifier_metadata.rb +1 -1
  187. data/lib/pubid/idf/builder.rb +3 -3
  188. data/lib/pubid/idf/identifier.rb +3 -6
  189. data/lib/pubid/idf/identifiers/amendment.rb +2 -1
  190. data/lib/pubid/idf/identifiers/corrigendum.rb +2 -1
  191. data/lib/pubid/idf/identifiers/international_standard.rb +2 -1
  192. data/lib/pubid/idf/identifiers/reviewed_method.rb +2 -1
  193. data/lib/pubid/idf/parser.rb +3 -2
  194. data/lib/pubid/idf/renderer.rb +84 -0
  195. data/lib/pubid/idf/supplement_identifier.rb +2 -10
  196. data/lib/pubid/idf/urn_generator.rb +4 -39
  197. data/lib/pubid/idf/urn_parser.rb +25 -0
  198. data/lib/pubid/idf.rb +51 -1
  199. data/lib/pubid/iec/builder.rb +48 -65
  200. data/lib/pubid/iec/components/code.rb +9 -32
  201. data/lib/pubid/iec/components/publisher.rb +1 -1
  202. data/lib/pubid/iec/components.rb +14 -0
  203. data/lib/pubid/iec/identifier.rb +276 -3
  204. data/lib/pubid/iec/identifiers/amendment.rb +2 -3
  205. data/lib/pubid/iec/identifiers/base.rb +8 -28
  206. data/lib/pubid/iec/identifiers/component_specification.rb +3 -3
  207. data/lib/pubid/iec/identifiers/conformity_assessment.rb +1 -2
  208. data/lib/pubid/iec/identifiers/consolidated_identifier.rb +27 -30
  209. data/lib/pubid/iec/identifiers/corrigendum.rb +2 -3
  210. data/lib/pubid/iec/identifiers/fragment_identifier.rb +37 -26
  211. data/lib/pubid/iec/identifiers/guide.rb +0 -2
  212. data/lib/pubid/iec/identifiers/international_standard.rb +2 -3
  213. data/lib/pubid/iec/identifiers/interpretation_sheet.rb +2 -3
  214. data/lib/pubid/iec/identifiers/operational_document.rb +3 -3
  215. data/lib/pubid/iec/identifiers/publicly_available_specification.rb +2 -3
  216. data/lib/pubid/iec/identifiers/sheet_identifier.rb +21 -15
  217. data/lib/pubid/iec/identifiers/societal_technology_trend_report.rb +3 -3
  218. data/lib/pubid/iec/identifiers/systems_reference_document.rb +2 -3
  219. data/lib/pubid/iec/identifiers/technical_report.rb +2 -3
  220. data/lib/pubid/iec/identifiers/technical_specification.rb +2 -3
  221. data/lib/pubid/iec/identifiers/technology_report.rb +1 -2
  222. data/lib/pubid/iec/identifiers/test_report_form.rb +5 -34
  223. data/lib/pubid/iec/identifiers/vap_identifier.rb +26 -23
  224. data/lib/pubid/iec/identifiers/white_paper.rb +3 -3
  225. data/lib/pubid/iec/identifiers/working_document.rb +4 -48
  226. data/lib/pubid/iec/identifiers.rb +30 -0
  227. data/lib/pubid/iec/parser.rb +20 -14
  228. data/lib/pubid/iec/renderer.rb +254 -0
  229. data/lib/pubid/iec/single_identifier.rb +6 -12
  230. data/lib/pubid/iec/supplement_identifier.rb +58 -54
  231. data/lib/pubid/iec/urn_generator.rb +57 -171
  232. data/lib/pubid/iec/urn_parser.rb +53 -252
  233. data/lib/pubid/iec.rb +40 -68
  234. data/lib/pubid/ieee/builder.rb +12 -12
  235. data/lib/pubid/ieee/components/code.rb +8 -0
  236. data/lib/pubid/ieee/components/draft.rb +14 -0
  237. data/lib/pubid/ieee/components/relationship.rb +5 -149
  238. data/lib/pubid/ieee/identifier.rb +6 -0
  239. data/lib/pubid/ieee/identifiers/adopted_standard.rb +1 -6
  240. data/lib/pubid/ieee/identifiers/base.rb +101 -458
  241. data/lib/pubid/ieee/identifiers/conformance_identifier.rb +1 -7
  242. data/lib/pubid/ieee/identifiers/corrigendum.rb +1 -9
  243. data/lib/pubid/ieee/identifiers/csa_dual_published.rb +1 -7
  244. data/lib/pubid/ieee/identifiers/dual_identifier.rb +1 -1
  245. data/lib/pubid/ieee/identifiers/dual_published.rb +1 -1
  246. data/lib/pubid/ieee/identifiers/iec_ieee_copublished.rb +1 -6
  247. data/lib/pubid/ieee/identifiers/interpretation_identifier.rb +1 -7
  248. data/lib/pubid/ieee/identifiers/joint_development.rb +2 -0
  249. data/lib/pubid/ieee/identifiers/multi_numbered_identifier.rb +1 -15
  250. data/lib/pubid/ieee/identifiers/parenthetical_identifier.rb +1 -3
  251. data/lib/pubid/ieee/identifiers/project_draft_identifier.rb +15 -0
  252. data/lib/pubid/ieee/identifiers/redlined_standard.rb +1 -4
  253. data/lib/pubid/ieee/identifiers/si_standard.rb +1 -35
  254. data/lib/pubid/ieee/identifiers/standard.rb +1 -1
  255. data/lib/pubid/ieee/pre_parser.rb +301 -0
  256. data/lib/pubid/ieee/renderer.rb +307 -0
  257. data/lib/pubid/ieee/urn_parser.rb +34 -0
  258. data/lib/pubid/ieee.rb +62 -1
  259. data/lib/pubid/ieee_debug.rb +0 -1
  260. data/lib/pubid/iho/builder.rb +2 -2
  261. data/lib/pubid/iho/identifier.rb +8 -0
  262. data/lib/pubid/iho/identifiers/base.rb +49 -10
  263. data/lib/pubid/iho/identifiers/bibliographic.rb +0 -4
  264. data/lib/pubid/iho/identifiers/circular_letter.rb +0 -4
  265. data/lib/pubid/iho/identifiers/miscellaneous.rb +0 -4
  266. data/lib/pubid/iho/identifiers/publication.rb +0 -4
  267. data/lib/pubid/iho/identifiers/standard.rb +0 -4
  268. data/lib/pubid/iho/parser.rb +3 -3
  269. data/lib/pubid/iho/renderer.rb +30 -0
  270. data/lib/pubid/iho/urn_generator.rb +3 -3
  271. data/lib/pubid/iho/urn_parser.rb +58 -0
  272. data/lib/pubid/iho.rb +50 -1
  273. data/lib/pubid/iso/builder.rb +59 -53
  274. data/lib/pubid/iso/bundled_identifier.rb +51 -0
  275. data/lib/pubid/iso/components/code.rb +7 -19
  276. data/lib/pubid/iso/components/publisher.rb +10 -8
  277. data/lib/pubid/iso/components.rb +2 -4
  278. data/lib/pubid/iso/identifier.rb +233 -6
  279. data/lib/pubid/iso/identifiers/addendum.rb +9 -6
  280. data/lib/pubid/iso/identifiers/amendment.rb +8 -4
  281. data/lib/pubid/iso/identifiers/corrigendum.rb +4 -4
  282. data/lib/pubid/iso/identifiers/data.rb +0 -1
  283. data/lib/pubid/iso/identifiers/directives.rb +8 -2
  284. data/lib/pubid/iso/identifiers/directives_supplement.rb +43 -14
  285. data/lib/pubid/iso/identifiers/extract.rb +2 -2
  286. data/lib/pubid/iso/identifiers/guide.rb +0 -1
  287. data/lib/pubid/iso/identifiers/international_standard.rb +4 -4
  288. data/lib/pubid/iso/identifiers/international_standardized_profile.rb +4 -4
  289. data/lib/pubid/iso/identifiers/international_workshop_agreement.rb +10 -4
  290. data/lib/pubid/iso/identifiers/pas.rb +2 -2
  291. data/lib/pubid/iso/identifiers/recommendation.rb +2 -2
  292. data/lib/pubid/iso/identifiers/supplement.rb +11 -3
  293. data/lib/pubid/iso/identifiers/tc_document.rb +44 -15
  294. data/lib/pubid/iso/identifiers/technical_report.rb +4 -4
  295. data/lib/pubid/iso/identifiers/technical_specification.rb +2 -2
  296. data/lib/pubid/iso/identifiers/technology_trends_assessments.rb +2 -2
  297. data/lib/pubid/iso/identifiers.rb +0 -1
  298. data/lib/pubid/iso/normalizer.rb +89 -0
  299. data/lib/pubid/iso/parser.rb +26 -6
  300. data/lib/pubid/iso/single_identifier.rb +6 -3
  301. data/lib/pubid/iso/supplement_identifier.rb +15 -2
  302. data/lib/pubid/iso/urn_generator.rb +74 -176
  303. data/lib/pubid/iso/urn_parser.rb +28 -9
  304. data/lib/pubid/iso.rb +173 -2
  305. data/lib/pubid/itu/builder.rb +0 -12
  306. data/lib/pubid/itu/components/code.rb +8 -0
  307. data/lib/pubid/itu/components.rb +11 -0
  308. data/lib/pubid/itu/identifier.rb +6 -39
  309. data/lib/pubid/itu/identifiers/amendment.rb +0 -2
  310. data/lib/pubid/itu/identifiers/annex.rb +0 -2
  311. data/lib/pubid/itu/identifiers/base.rb +0 -6
  312. data/lib/pubid/itu/identifiers/combined_identifier.rb +0 -2
  313. data/lib/pubid/itu/identifiers/corrigendum.rb +0 -2
  314. data/lib/pubid/itu/identifiers/recommendation.rb +0 -2
  315. data/lib/pubid/itu/identifiers/special_publication.rb +0 -2
  316. data/lib/pubid/itu/identifiers/supplement.rb +0 -2
  317. data/lib/pubid/itu/urn_parser.rb +23 -0
  318. data/lib/pubid/itu.rb +42 -1
  319. data/lib/pubid/jcgm/builder.rb +16 -8
  320. data/lib/pubid/jcgm/identifiers/amendment.rb +2 -7
  321. data/lib/pubid/jcgm/identifiers/gum_guide.rb +2 -10
  322. data/lib/pubid/jcgm/renderer.rb +68 -0
  323. data/lib/pubid/jcgm/single_identifier.rb +1 -5
  324. data/lib/pubid/jcgm/urn_generator.rb +4 -6
  325. data/lib/pubid/jcgm/urn_parser.rb +23 -0
  326. data/lib/pubid/jcgm.rb +43 -2
  327. data/lib/pubid/jis/builder.rb +44 -52
  328. data/lib/pubid/jis/identifier.rb +132 -3
  329. data/lib/pubid/jis/identifiers/amendment.rb +1 -1
  330. data/lib/pubid/jis/identifiers/corrigendum.rb +16 -0
  331. data/lib/pubid/jis/identifiers/standard.rb +2 -1
  332. data/lib/pubid/jis/identifiers/technical_report.rb +2 -1
  333. data/lib/pubid/jis/identifiers/technical_specification.rb +2 -1
  334. data/lib/pubid/jis/identifiers.rb +1 -1
  335. data/lib/pubid/jis/parser.rb +31 -5
  336. data/lib/pubid/jis/renderer.rb +69 -0
  337. data/lib/pubid/jis/single_identifier.rb +6 -12
  338. data/lib/pubid/jis/supplement_identifier.rb +17 -14
  339. data/lib/pubid/jis/urn_parser.rb +23 -0
  340. data/lib/pubid/jis.rb +42 -2
  341. data/lib/pubid/nist/builder.rb +122 -1761
  342. data/lib/pubid/nist/caster.rb +1272 -0
  343. data/lib/pubid/nist/circular_supplement_builder.rb +291 -0
  344. data/lib/pubid/nist/components/code.rb +9 -20
  345. data/lib/pubid/nist/components/edition.rb +16 -0
  346. data/lib/pubid/nist/components/supplement.rb +88 -21
  347. data/lib/pubid/nist/components.rb +0 -1
  348. data/lib/pubid/nist/identifier.rb +25 -0
  349. data/lib/pubid/nist/identifiers/base.rb +206 -64
  350. data/lib/pubid/nist/identifiers/circular.rb +7 -2
  351. data/lib/pubid/nist/identifiers/circular_supplement.rb +3 -2
  352. data/lib/pubid/nist/identifiers/commercial_standard.rb +2 -1
  353. data/lib/pubid/nist/identifiers/commercial_standard_emergency.rb +6 -4
  354. data/lib/pubid/nist/identifiers/commercial_standards_monthly.rb +10 -3
  355. data/lib/pubid/nist/identifiers/crpl_report.rb +8 -11
  356. data/lib/pubid/nist/identifiers/dated_document.rb +49 -0
  357. data/lib/pubid/nist/identifiers/federal_information_processing_standards.rb +17 -16
  358. data/lib/pubid/nist/identifiers/grant_contractor_report.rb +2 -1
  359. data/lib/pubid/nist/identifiers/handbook.rb +2 -1
  360. data/lib/pubid/nist/identifiers/internal_report.rb +2 -1
  361. data/lib/pubid/nist/identifiers/letter_circular.rb +2 -1
  362. data/lib/pubid/nist/identifiers/miscellaneous_publication.rb +5 -4
  363. data/lib/pubid/nist/identifiers/monograph.rb +7 -3
  364. data/lib/pubid/nist/identifiers/report.rb +4 -3
  365. data/lib/pubid/nist/identifiers/special_publication.rb +2 -1
  366. data/lib/pubid/nist/identifiers/technical_note.rb +3 -2
  367. data/lib/pubid/nist/identifiers.rb +1 -0
  368. data/lib/pubid/nist/parser.rb +67 -424
  369. data/lib/pubid/nist/parser_output_normalizer.rb +233 -0
  370. data/lib/pubid/nist/preprocessor.rb +416 -0
  371. data/lib/pubid/nist/renderer.rb +43 -0
  372. data/lib/pubid/nist/router.rb +148 -0
  373. data/lib/pubid/nist/series/base.rb +58 -0
  374. data/lib/pubid/nist/series/crpl.rb +13 -0
  375. data/lib/pubid/nist/series/fips.rb +14 -0
  376. data/lib/pubid/nist/series/ir.rb +60 -0
  377. data/lib/pubid/nist/series/letter_preserving.rb +15 -0
  378. data/lib/pubid/nist/series/mono.rb +19 -0
  379. data/lib/pubid/nist/series/ncstar.rb +20 -0
  380. data/lib/pubid/nist/series.rb +49 -0
  381. data/lib/pubid/nist/supplement_identifier.rb +11 -25
  382. data/lib/pubid/nist/urn_generator.rb +14 -8
  383. data/lib/pubid/nist/urn_parser.rb +67 -0
  384. data/lib/pubid/nist.rb +83 -4
  385. data/lib/pubid/oiml/components/code.rb +10 -0
  386. data/lib/pubid/oiml/identifiers/annex.rb +3 -45
  387. data/lib/pubid/oiml/identifiers/base.rb +2 -17
  388. data/lib/pubid/oiml/renderer.rb +161 -0
  389. data/lib/pubid/oiml/single_identifier.rb +6 -45
  390. data/lib/pubid/oiml/supplement_identifier.rb +4 -19
  391. data/lib/pubid/oiml/urn_generator.rb +0 -8
  392. data/lib/pubid/oiml/urn_parser.rb +22 -0
  393. data/lib/pubid/oiml.rb +42 -1
  394. data/lib/pubid/plateau/identifier.rb +23 -0
  395. data/lib/pubid/plateau/identifiers/handbook.rb +1 -3
  396. data/lib/pubid/plateau/identifiers/technical_report.rb +1 -1
  397. data/lib/pubid/plateau/renderer.rb +51 -0
  398. data/lib/pubid/plateau/supplement_identifier.rb +1 -1
  399. data/lib/pubid/plateau/urn_parser.rb +43 -0
  400. data/lib/pubid/plateau.rb +44 -1
  401. data/lib/pubid/renderers/base.rb +34 -0
  402. data/lib/pubid/renderers/directives_renderer.rb +27 -14
  403. data/lib/pubid/renderers/guide_renderer.rb +7 -1
  404. data/lib/pubid/renderers/human_readable.rb +31 -8
  405. data/lib/pubid/renderers/iwa_renderer.rb +5 -1
  406. data/lib/pubid/renderers/supplement_renderer.rb +4 -1
  407. data/lib/pubid/rendering/context.rb +33 -21
  408. data/lib/pubid/rendering.rb +0 -3
  409. data/lib/pubid/sae/components/date.rb +8 -0
  410. data/lib/pubid/sae/components/type.rb +5 -1
  411. data/lib/pubid/sae/identifiers/base.rb +2 -16
  412. data/lib/pubid/sae/renderer.rb +36 -0
  413. data/lib/pubid/sae/urn_generator.rb +2 -10
  414. data/lib/pubid/sae/urn_parser.rb +36 -0
  415. data/lib/pubid/sae.rb +42 -1
  416. data/lib/pubid/urn_generator/base.rb +12 -12
  417. data/lib/pubid/urn_parser/base.rb +81 -0
  418. data/lib/pubid/urn_parser/errors.rb +9 -0
  419. data/lib/pubid/urn_parser.rb +14 -0
  420. data/lib/pubid/version.rb +1 -1
  421. data/lib/pubid.rb +29 -7
  422. data/lib/tasks/website-data.json +1940 -1882
  423. metadata +77 -43
  424. data/lib/pubid/amca/scheme.rb +0 -16
  425. data/lib/pubid/ansi/scheme.rb +0 -15
  426. data/lib/pubid/api/scheme.rb +0 -66
  427. data/lib/pubid/ashrae/scheme.rb +0 -53
  428. data/lib/pubid/asme/scheme.rb +0 -37
  429. data/lib/pubid/astm/scheme.rb +0 -55
  430. data/lib/pubid/bsi/identifiers/base.rb +0 -11
  431. data/lib/pubid/bsi/scheme.rb +0 -243
  432. data/lib/pubid/ccsds/scheme.rb +0 -57
  433. data/lib/pubid/cen_cenelec/scheme.rb +0 -164
  434. data/lib/pubid/cie/scheme.rb +0 -64
  435. data/lib/pubid/csa/scheme.rb +0 -44
  436. data/lib/pubid/etsi/scheme.rb +0 -42
  437. data/lib/pubid/export/data_class_exporter.rb +0 -59
  438. data/lib/pubid/export/ieee_exporter.rb +0 -78
  439. data/lib/pubid/export/itu_exporter.rb +0 -66
  440. data/lib/pubid/export/nist_exporter.rb +0 -64
  441. data/lib/pubid/export/registry_exporter.rb +0 -90
  442. data/lib/pubid/export/scheme_exporter.rb +0 -70
  443. data/lib/pubid/identifier_registry.rb +0 -198
  444. data/lib/pubid/idf/scheme.rb +0 -61
  445. data/lib/pubid/iec/scheme.rb +0 -71
  446. data/lib/pubid/ieee/scheme.rb +0 -90
  447. data/lib/pubid/iho/scheme.rb +0 -29
  448. data/lib/pubid/iso/identifiers/base.rb +0 -115
  449. data/lib/pubid/iso/scheme.rb +0 -187
  450. data/lib/pubid/itu/scheme.rb +0 -174
  451. data/lib/pubid/jcgm/scheme.rb +0 -60
  452. data/lib/pubid/jis/components/code.rb +0 -59
  453. data/lib/pubid/jis/identifiers/base.rb +0 -72
  454. data/lib/pubid/jis/scheme.rb +0 -49
  455. data/lib/pubid/nist/components/publisher.rb +0 -24
  456. data/lib/pubid/nist/scheme.rb +0 -199
  457. data/lib/pubid/oiml/scheme.rb +0 -46
  458. data/lib/pubid/plateau/scheme.rb +0 -45
  459. data/lib/pubid/rendering/base.rb +0 -73
  460. data/lib/pubid/rendering/common.rb +0 -211
  461. data/lib/pubid/rendering/format.rb +0 -25
  462. data/lib/pubid/sae/scheme.rb +0 -47
  463. data/lib/pubid/scheme.rb +0 -207
@@ -0,0 +1,1272 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Nist
5
+ # Caster class for NIST type coercion
6
+ # Single Responsibility: Convert parsed values to domain component objects
7
+ #
8
+ # Extracted from Builder to isolate the 1200+ line type coercion switch
9
+ # from orchestration logic. Caster is stateless — each call to #cast
10
+ # receives all the context it needs via parameters.
11
+ class Caster
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
+ # Cast parsed value to appropriate component type
25
+ # ALL conversions happen in this single method
26
+ # @param type [Symbol] the parameter type
27
+ # @param value [Object] the parsed value
28
+ # @param parsed_hash [Hash] the full parsed hash for context
29
+ # @return [Object, Hash, nil] the cast component(s)
30
+ def cast(type, value, parsed_hash = {})
31
+ case type
32
+ when :publisher
33
+ return nil if value.nil? || value.to_s.strip.empty?
34
+
35
+ # publisher is a plain string attribute (see Identifiers::Base).
36
+ value.to_s
37
+
38
+ when :dated_date
39
+ # Date-style identifier (DatedDocument): carry the YYYY-MM-DD parts
40
+ # as string attributes.
41
+ return nil unless value.is_a?(Hash)
42
+
43
+ { date_year: value[:date_year]&.to_s, date_month: value[:date_month]&.to_s,
44
+ date_day: value[:date_day]&.to_s }
45
+
46
+ when :dated_seq
47
+ return nil if value.to_s.strip.empty?
48
+
49
+ { dated_seq: value.to_s }
50
+
51
+ when :series
52
+ return nil if value.nil? || value.to_s.strip.empty?
53
+
54
+ str_value = value.to_s
55
+ publisher_extracted = nil
56
+
57
+ # Compound series carry the publisher inside the series token (e.g.
58
+ # "NBS CIRC", "NIST DCI") because the bare series code isn't recognized
59
+ # by the grammar on its own. Split the leading publisher out so
60
+ # `series` holds just the code and `publisher` is populated — matching
61
+ # how standalone series (NIST SP, NBS CS) already parse, and avoiding a
62
+ # nil publisher (which renders fine via the series string but would
63
+ # otherwise force a misleading publisher_was_parsed: false). Routing
64
+ # already ran on the raw compound series, so stripping here can't
65
+ # change the identifier class.
66
+ if (m = str_value.match(/\A(NBS|NIST) (.+)\z/))
67
+ publisher_extracted = m[1]
68
+ str_value = m[2]
69
+ end
70
+
71
+ # Return composite hash with both publisher and series if extracted
72
+ if publisher_extracted
73
+ {
74
+ publisher: publisher_extracted,
75
+ series: Components::Code.new(value: str_value),
76
+ }
77
+ else
78
+ Components::Code.new(value: str_value)
79
+ end
80
+
81
+ when :volume_number
82
+ # Volume from v#n# pattern - return Volume component
83
+ return nil if value.nil? || value.to_s.strip.empty?
84
+
85
+ { volume: Components::Volume.new(value: value.to_s) }
86
+
87
+ when :issue_number
88
+ # Issue number from v#n# pattern - return Part component
89
+ return nil if value.nil? || value.to_s.strip.empty?
90
+
91
+ { part: Components::Part.new(type: "n", value: value.to_s) }
92
+
93
+ when :part_number
94
+ # Part number from GCR pattern (e.g., 85-3273-37)
95
+ # Return raw value for inclusion in compound number
96
+ return nil if value.nil? || value.to_s.strip.empty?
97
+
98
+ value # Return raw value to be tracked in builder
99
+
100
+ when :letter_number
101
+ # Letter suffix from a dashed pattern (e.g., 800-56A → {:letter_base=>"56", :letter_suffix=>"A"}).
102
+ # Series policy decides whether the suffix becomes a Part component
103
+ # (default) or stays in the number (MONO/NCSTAR/IR-with-R-or-Ur).
104
+ return nil if value.nil? || !value.is_a?(Hash)
105
+
106
+ Series.for(parsed_hash).cast_letter_number(value, parsed_hash)
107
+
108
+ when :fips_part
109
+ # Part number from FIPS date pattern (e.g., 11-1-Sep30/1977)
110
+ # Return Part component with pt type
111
+ return nil if value.nil? || value.to_s.strip.empty?
112
+
113
+ { part: Components::Part.new(type: "pt", value: value.to_s) }
114
+
115
+ when :owmwp_date_number
116
+ # OWMWP date-based number format (MM-DD-YYYY)
117
+ # Parser returns: {:owmwp_month=>"06", :owmwp_day=>"13", :owmwp_year=>"2018"}
118
+ # Convert to number + edition: "06-13" + edition "e2018"
119
+ return nil if value.nil?
120
+
121
+ number_part = "#{value[:owmwp_month]}-#{value[:owmwp_day]}"
122
+ edition_part = Components::Edition.new(type: "e",
123
+ id: value[:owmwp_year])
124
+ { first_number: Components::Code.new(value: number_part), edition: edition_part }
125
+
126
+ when :first_number, :second_number
127
+ return nil if value.nil? || value.to_s.strip.empty?
128
+
129
+ # NEW: Handle OWMWP date-based number (nested hash structure)
130
+ # Parser returns: {:owmwp_date_number=>{:owmwp_month=>"06", :owmwp_day=>"13", :owmwp_year=>"2018"}}
131
+ # Convert to number + edition: "06-13" + edition "e2018"
132
+ if value.is_a?(Hash) && value[:owmwp_date_number]
133
+ owmwp_hash = value[:owmwp_date_number]
134
+ number_part = "#{owmwp_hash[:owmwp_month]}-#{owmwp_hash[:owmwp_day]}"
135
+ edition_part = Components::Edition.new(type: "e",
136
+ id: owmwp_hash[:owmwp_year])
137
+ return { type => Components::Code.new(value: number_part), edition: edition_part }
138
+ end
139
+
140
+ # NEW: Handle second_number with edition (hash with :number_only and :edition_id)
141
+ # This handles "126r2013" pattern where parser returns {:number_only=>"126", :edition_id=>"2013"}
142
+ # CRITICAL: Wrap in a structure that builder loop can recognize
143
+ # The builder loop expects keys like :second_number to be present in the hash
144
+ if type == :second_number && value.is_a?(Hash) && value[:number_only] && value[:edition_id]
145
+ # Return wrapped hash so builder loop finds :second_number key
146
+ return { second_number: value }
147
+ end
148
+
149
+ # NEW: Handle second_number with revision_letter (hash with :revision_letter containing :number_only and :letter)
150
+ # This handles "27ra" pattern where parser returns {revision_letter: {number_only: "27", letter: "a"}}
151
+ # Should be combined to "27rA" format
152
+ if type == :second_number && value.is_a?(Hash) && value[:revision_letter]
153
+ revision_data = value[:revision_letter]
154
+ number_only = revision_data[:number_only].to_s
155
+ letter = revision_data[:letter].to_s.upcase
156
+ # Return as second_number with combined format "27rA"
157
+ return { second_number: Components::Code.new(value: "#{number_only}r#{letter}") }
158
+ end
159
+
160
+ # Handle v#n# pattern (CSM series) - comes as hash from parser
161
+ # Return Volume and Part components separately
162
+ if value.is_a?(Hash) && value[:volume_number] && value[:issue_number]
163
+ volume_num = value[:volume_number].to_s
164
+ issue_num = value[:issue_number].to_s
165
+ return {
166
+ volume: Components::Volume.new(value: volume_num),
167
+ part: Components::Part.new(type: "n", value: issue_num),
168
+ }
169
+ end
170
+
171
+ str_value = value.to_s
172
+
173
+ # Handle special patterns embedded in first_number
174
+ if type == :first_number
175
+
176
+ # NEW: Handle first_number hash with number_with_rev_year (e.g., "1013rv1953")
177
+ # Parser returns: {:number_with_rev_year=>{:number=>"1013", :revision_year=>"1953"}}
178
+ if value.is_a?(Hash) && value[:number_with_rev_year]
179
+ number_part = value[:number_with_rev_year][:number].to_s
180
+ revision_year = value[:number_with_rev_year][:revision_year].to_s
181
+ return {
182
+ first_number: Components::Code.new(value: number_part),
183
+ edition: Components::Edition.new(type: "rv", id: revision_year),
184
+ }
185
+ end
186
+
187
+ # Handle first_number with letter suffix and revision (e.g., "8278Ar1", "256Ar1930")
188
+ # Parser returns: {:number_with_letter_revision=>{:number=>"8278", :letter_suffix=>"A", :revision_id=>"1"}}
189
+ # Splits into number + Part(letter) + Edition(r, id) to mirror how :letter_number
190
+ # (dash-separated "56Ar2") is decomposed for SpecialPublication and similar series.
191
+ if value.is_a?(Hash) && value[:number_with_letter_revision]
192
+ data = value[:number_with_letter_revision]
193
+ return {
194
+ first_number: Components::Code.new(value: data[:number].to_s),
195
+ part: Components::Part.new(type: "",
196
+ value: data[:letter_suffix].to_s.upcase),
197
+ edition: Components::Edition.new(type: "r",
198
+ id: data[:revision_id].to_s),
199
+ edition_component: Components::Edition.new(type: "r",
200
+ id: data[:revision_id].to_s),
201
+ revision: "r#{data[:revision_id]}",
202
+ }
203
+ end
204
+
205
+ # NEW: Handle first_number hash with language_code (e.g., "1262es")
206
+ # Parser returns: {:number=>"1262", :language_code=>"es"}
207
+ if value.is_a?(Hash) && value[:number] && value[:language_code]
208
+ number_part = value[:number].to_s
209
+ language_code = value[:language_code].to_s.strip.downcase
210
+ # Apply normalization map (es -> spa, pt -> por, etc.)
211
+ normalized_code = TRANSLATION_MAP[language_code] || language_code
212
+ return {
213
+ first_number: Components::Code.new(value: number_part),
214
+ translation_component: Components::Translation.new(code: normalized_code),
215
+ }
216
+ end
217
+
218
+ # NEW: Handle first_number hash with number, part_number, and edition_year (MR format)
219
+ # Parser returns: {:number=>"28", :part_number=>"1", :edition_year=>"1969"}
220
+ # For "NBS.HB.28pt1e1969" MR format input
221
+ if value.is_a?(Hash) && value[:number] && value[:part_number] && value[:edition_year]
222
+ number_part = value[:number].to_s
223
+ part_number = value[:part_number].to_s
224
+ edition_year = value[:edition_year].to_s
225
+ return {
226
+ first_number: Components::Code.new(value: number_part),
227
+ part: Components::Part.new(type: "pt", value: part_number),
228
+ edition: Components::Edition.new(type: "e", id: edition_year),
229
+ }
230
+ end
231
+
232
+ # NEW: Check for edition_year_separate in parsed_hash context
233
+ # This handles "11e2-1915" where first_number="11e2" and edition_year_separate="1915"
234
+ if parsed_hash[:edition_year_separate] && str_value =~ /^(\d+)e(\d+)$/
235
+ number_part = $1
236
+ edition_id = $2
237
+ year_part = parsed_hash[:edition_year_separate].to_s
238
+ return {
239
+ first_number: Components::Code.new(value: number_part),
240
+ edition: Components::Edition.new(type: "e", id: edition_id,
241
+ additional_text: year_part),
242
+ }
243
+ end
244
+
245
+ # NEW: Check for number_with_volume in value hash (for first_number)
246
+ # This handles "539v10" where parser captures :number and :volume_suffix separately
247
+ # Parse tree: value = {:number_with_volume => {:number => "539", :volume_suffix => "10"}}
248
+ if value.is_a?(Hash) && value[:number_with_volume] && value[:number_with_volume][:volume_suffix]
249
+ number_part = value[:number_with_volume][:number].to_s
250
+ volume_value = value[:number_with_volume][:volume_suffix].to_s
251
+ return {
252
+ first_number: Components::Code.new(value: number_part),
253
+ volume: Components::Volume.new(value: volume_value),
254
+ }
255
+ end
256
+
257
+ # NEW: Check for historical_month and historical_year in parsed_hash context
258
+ # This handles "-April1909" where it's captured as separate month/year
259
+ if parsed_hash[:historical_month] && parsed_hash[:historical_year]
260
+ month_part = parsed_hash[:historical_month].to_s
261
+ year_part = parsed_hash[:historical_year].to_s
262
+ # Check if str_value is just a number (the part before dash)
263
+ if /^\d+$/.match?(str_value)
264
+ return {
265
+ first_number: Components::Code.new(value: str_value),
266
+ edition: Components::Edition.new(type: "-",
267
+ additional_text: "#{month_part}#{year_part}"),
268
+ }
269
+ else
270
+ # No number, just historical edition
271
+ return {
272
+ edition: Components::Edition.new(type: "-",
273
+ additional_text: "#{month_part}#{year_part}"),
274
+ }
275
+ end
276
+ end
277
+
278
+ # Pattern "9350sup"/"5893supp" - number with bare supplement marker
279
+ # (no trailing payload). Accept both single-p "sup" and double-p
280
+ # "supp" so the marker is isolated as supplement="" and rendered as
281
+ # canonical single-p "sup", instead of staying baked into the number
282
+ # as an opaque suffix. E.g. "NBS RPT 5893supp", "NBS MONO 32supp".
283
+ if str_value =~ /^(\d+)supp?$/
284
+ return {
285
+ first_number: Components::Code.new(value: $1),
286
+ supplement: "",
287
+ }
288
+ end
289
+
290
+ # NEW: Check for supplement_year in parsed_hash context
291
+ # This handles "25supp-1924" where first_number="25supp" and supplement_year="1924"
292
+ if parsed_hash[:supplement_year] && str_value =~ /^(\d+)supp?$/
293
+ number_part = $1
294
+ year_part = parsed_hash[:supplement_year].to_s
295
+ return {
296
+ first_number: Components::Code.new(value: number_part),
297
+ supplement: year_part,
298
+ }
299
+ end
300
+
301
+ # Pattern: "154supprev" - supplement with revision
302
+ if str_value =~ /^(\d+)supprev$/
303
+ return {
304
+ first_number: Components::Code.new(value: $1),
305
+ supplement: "",
306
+ supplement_has_revision: true,
307
+ }
308
+ # NEW: Pattern "11e2-1915" - edition with separate year (inline match)
309
+ # Creates: number="11", Edition(type: "e", id: "2", additional_text: "1915")
310
+ # Renders: "NBS CIRC 11e2.1915"
311
+ elsif str_value =~ /^(\d+)e(\d+)-(\d{4})$/
312
+ number_part = $1
313
+ edition_id = $2
314
+ year_part = $3
315
+ return {
316
+ first_number: Components::Code.new(value: number_part),
317
+ edition: Components::Edition.new(type: "e", id: edition_id,
318
+ additional_text: year_part),
319
+ }
320
+ # NEW: Pattern "-April1909" - historical edition with month+year (inline match)
321
+ # Creates: Edition(type: "-", additional_text: "April1909")
322
+ # Renders: "NBS CIRC -April1909"
323
+ elsif str_value =~ /^-([A-Za-z]{3,9})(\d{4})$/
324
+ month_part = $1
325
+ year_part = $2
326
+ return {
327
+ edition: Components::Edition.new(type: "-",
328
+ additional_text: "#{month_part}#{year_part}"),
329
+ }
330
+ # NEW: CS Emergency pattern "e104" or "e104-43" -> extract number
331
+ # This must come BEFORE bare edition check to avoid conflict
332
+ # CS emergency always has 3+ digit number (e104, not e2)
333
+ # NOTE: If second_number exists (e104-43 pattern), defer to compound number logic
334
+ elsif /^e(\d{3,})$/.match?(str_value) && !parsed_hash[:second_number]
335
+ # Extract emergency number: e104 -> 104 (only when no second_number)
336
+ emergency_num = str_value.sub(/^e/, "")
337
+ return {
338
+ first_number: Components::Code.new(value: emergency_num),
339
+ }
340
+ # If e104-43 pattern (with second_number), keep e prefix for compound number logic
341
+ elsif /^e(\d{3,})$/.match?(str_value) && parsed_hash[:second_number]
342
+ # Keep e104 as-is, let compound number logic handle it
343
+ return {
344
+ first_number: Components::Code.new(value: str_value),
345
+ }
346
+ # NEW: Bare edition pattern like "100e1" (CS series without year)
347
+ # ONLY when NO second_number present (to avoid conflict with "123e2-50")
348
+ # Creates: number="100", Edition(type: "e", id: "1")
349
+ # Renders: "NBS CS 100e1"
350
+ # CRITICAL: Skip if edition_dash_year is present - let that handler create Edition with additional_text
351
+ elsif str_value =~ /^(\d+)e(\d+)$/ && !parsed_hash[:second_number] && !parsed_hash[:edition_dash_year]
352
+ number_part = $1
353
+ edition_id = $2
354
+
355
+ return {
356
+ first_number: Components::Code.new(value: number_part),
357
+ edition: Components::Edition.new(type: "e", id: edition_id),
358
+ }
359
+ # NEW: Bare edition pattern "e2" - just edition without number prefix
360
+ # Creates: Edition(type: "e", id: "2")
361
+ # Renders: "NBS CIRC e2"
362
+ # Only matches single or double digit (e1, e2, not e104 which is emergency)
363
+ elsif str_value =~ /^e(\d{1,2})$/
364
+ edition_id = $1
365
+ return {
366
+ edition: Components::Edition.new(type: "e", id: edition_id),
367
+ }
368
+ # Pattern: "13e2rev1908" - edition with revision year-only (NO month)
369
+ # Creates: Edition(type: "e", id: "2", additional_text: "1908")
370
+ # Renders: "e2.1908" (DOT separator)
371
+ elsif str_value =~ /^(\d+)e(\d+)rev(\d{4})$/
372
+ # CRITICAL: Capture BEFORE any regex method calls!
373
+ number_part = $1
374
+ edition_id_part = $2
375
+ year_part = $3
376
+ return {
377
+ first_number: Components::Code.new(value: number_part),
378
+ edition: Components::Edition.new(type: "e",
379
+ id: edition_id_part, additional_text: year_part),
380
+ }
381
+ # Pattern: "13e2revJune1908" - edition with revision month+year
382
+ # Creates: Edition(type: "e", id: "2", additional_text: "June1908")
383
+ # Renders: "e2.June1908" (DOT separator)
384
+ elsif str_value =~ /^(\d+)e(\d+)(rev.+)$/
385
+ # CRITICAL: Capture $1, $2, $3 BEFORE calling .sub() which resets them!
386
+ number_part = $1
387
+ edition_id_part = $2
388
+ rev_part = $3
389
+ # Strip "rev" prefix from additional_text - store only "June1908" or "1908"
390
+ additional_text = rev_part.sub(/^rev/, "")
391
+ return {
392
+ first_number: Components::Code.new(value: number_part),
393
+ edition: Components::Edition.new(type: "e",
394
+ id: edition_id_part, additional_text: additional_text),
395
+ }
396
+ # NEW: Pattern "24suppJan1924" - supplement with month and year in first_number
397
+ # Creates: number="24", supplement="Jan1924"
398
+ elsif str_value =~ /^(\d+)supp([A-Za-z]{3,9})(\d{4})$/
399
+ number_part = $1
400
+ month_part = $2
401
+ year_part = $3
402
+ return {
403
+ first_number: Components::Code.new(value: number_part),
404
+ supplement: "#{month_part}#{year_part}",
405
+ }
406
+ # NEW: Pattern "25supp1924" - supplement with year (no dash, no month)
407
+ # Creates: number="25", supplement="1924"
408
+ # Renders: "NBS SP 25supp1924"
409
+ elsif str_value =~ /^(\d+)supp(\d{4})$/
410
+ number_part = $1
411
+ year_part = $2
412
+ return {
413
+ first_number: Components::Code.new(value: number_part),
414
+ supplement: year_part,
415
+ }
416
+ # NEW: Pattern "25supp-1924" - supplement with dash-year (inline match)
417
+ # Creates: number="25", supplement="1924"
418
+ # Renders: "NBS CIRC 25supp-1924"
419
+ elsif str_value =~ /^(\d+)supp-(\d{4})$/
420
+ number_part = $1
421
+ year_part = $2
422
+ return {
423
+ first_number: Components::Code.new(value: number_part),
424
+ supplement: year_part,
425
+ }
426
+ # NEW: Pattern "101e2supp" - edition + supplement
427
+ # Creates: number="101", Edition(type: "e", id: "2"), supplement=""
428
+ # Renders: "NBS CIRC 101e2supp"
429
+ elsif str_value =~ /^(\d+)e(\d+)supp$/
430
+ number_part = $1
431
+ edition_id = $2
432
+ return {
433
+ first_number: Components::Code.new(value: number_part),
434
+ edition: Components::Edition.new(type: "e", id: edition_id),
435
+ supplement: "",
436
+ }
437
+ end
438
+ elsif type == :second_number && value.is_a?(Hash) && value[:first_number]
439
+ # Handle second_number as a hash with first_number context
440
+ # e.g., for pattern 800-57pt1r4
441
+ number_part = value[:first_number].to_s
442
+ part_value = value[:part_value]&.to_s
443
+ revision_value = value[:revision_value]&.to_s
444
+ return {
445
+ first_number: Components::Code.new(value: number_part),
446
+ part: Components::Part.new(value: part_value),
447
+ edition: Components::Edition.new(type: "r", id: revision_value),
448
+ }
449
+ end
450
+
451
+ # Extract revision suffix from number (e.g., "53r5" -> "53" + Edition(r, 5))
452
+ # ENHANCED: Also extract revision with slash-year (e.g., "53r5/1917" -> "53" + Edition)
453
+ # ENHANCED: Also extract revision with 4-digit year (e.g., "1019r1963" -> "1019" + Edition)
454
+ # ENHANCED: Also extract revision with month+year (e.g., "4743rJun1992" -> "4743" + Edition)
455
+
456
+ # NEW: Extract part suffix from number (e.g., "800-57pt1" -> "800-57" + Part(1))
457
+ # This handles SP series part notation
458
+ # IMPORTANT: Handle combined part+revision first (e.g., "800-57pt1r4")
459
+ if str_value =~ /^(.+?)pt(\d+)r(\d+[a-z]?)$/
460
+ number_part = $1
461
+ part_value = $2
462
+ revision_value = $3
463
+ return {
464
+ type => Components::Code.new(value: number_part),
465
+ part: Components::Part.new(type: "pt", value: part_value),
466
+ edition: Components::Edition.new(type: "r", id: revision_value),
467
+ }
468
+ elsif str_value =~ /^(.+?)pt(\d+)$/
469
+ number_part = $1
470
+ part_value = $2
471
+ return {
472
+ type => Components::Code.new(value: number_part),
473
+ part: Components::Part.new(type: "pt", value: part_value),
474
+ }
475
+ end
476
+
477
+ # NEW: Extract volume suffix from number (e.g., "539v10" -> "539" + volume="10")
478
+ # This handles CIRC volume notation
479
+ if str_value =~ /^(\d+)v(\d+)$/
480
+ number_part = $1
481
+ volume_part = $2
482
+ return {
483
+ type => Components::Code.new(value: number_part),
484
+ volume: volume_part,
485
+ }
486
+ end
487
+
488
+ # REVISION PATTERNS - These must come BEFORE letter suffix to avoid conflicts
489
+ case str_value
490
+ when /^(.+?)(r\d+\/\d{4})$/i
491
+ # Pattern: r6/1925 (revision with slash-year)
492
+ number_part = $1
493
+ revision_with_year = $2 # e.g., "r6/1925"
494
+ # Extract revision and year
495
+ if revision_with_year =~ /^r(\d+)\/(\d{4})$/
496
+ revision_id = $1
497
+ year_part = $2
498
+ return {
499
+ type => Components::Code.new(value: number_part),
500
+ edition: Components::Edition.new(type: "r", id: revision_id,
501
+ additional_text: year_part),
502
+ }
503
+ end
504
+ when /^(.*\d)(r\d{4})$/i
505
+ # Pattern: r1963 (revision as 4-digit year)
506
+ number_part = $1
507
+ year_value = $2.sub(/^r/, "") # Strip 'r' prefix
508
+ return {
509
+ type => Components::Code.new(value: number_part),
510
+ edition: Components::Edition.new(type: "r", id: year_value),
511
+ }
512
+ when /^(.+?)(r[A-Za-z]{3,9}\d{4})$/i
513
+ # Pattern: rJun1992 (revision with month and year)
514
+ number_part = $1
515
+ revision_with_date = $2 # e.g., "rJun1992"
516
+ # Extract month and year
517
+ if revision_with_date =~ /^r([A-Za-z]{3,9})(\d{4})$/
518
+ month_part = $1
519
+ year_part = $2
520
+ return {
521
+ type => Components::Code.new(value: number_part),
522
+ edition: Components::Edition.new(type: "r",
523
+ id: "#{month_part}#{year_part}"),
524
+ }
525
+ end
526
+ when /^(.*\d)(r\d+[a-z]?)$/i
527
+ # Pattern: r5, r1a (simple revision)
528
+ number_part = $1
529
+ revision_value = $2.sub(/^r/, "") # Strip 'r' prefix
530
+ return {
531
+ type => Components::Code.new(value: number_part),
532
+ edition: Components::Edition.new(type: "r", id: revision_value),
533
+ }
534
+ when /^(.+?)(?<![a-zA-Z])(r)$/i
535
+ # Pattern: bare r with no digits (e.g., "800-90r")
536
+ # Negative lookbehind ensures r is NOT preceded by a letter (avoids matching Ur, Ua, etc.)
537
+ number_part = $1
538
+ return {
539
+ type => Components::Code.new(value: number_part),
540
+ edition: Components::Edition.new(type: "r", id: "1"),
541
+ }
542
+ end
543
+
544
+ # NEW: Extract UPPERCASE letter suffix as Part component (e.g., "800-56A" -> "800-56" + Part)
545
+ # IMPORTANT: These patterns come AFTER revision patterns to avoid conflicts
546
+ # Letter suffixes are UPPERCASE letters A-Z only (no lowercase to avoid revision markers)
547
+
548
+ # Pattern: UPPERCASE letter + revision (e.g., "800-56Ar2" -> number + Part("", "A") + Edition(r, 2))
549
+ # NO /i flag - only match uppercase letters!
550
+ if str_value =~ /^(.+?)([A-Z])(r\d+[a-z]?)$/
551
+ number_part = $1
552
+ letter_part = $2
553
+ revision_part = $3.sub(/^r/, "")
554
+ return {
555
+ type => Components::Code.new(value: number_part),
556
+ part: Components::Part.new(type: "", value: letter_part),
557
+ edition: Components::Edition.new(type: "r", id: revision_part),
558
+ }
559
+ # Pattern: bare UPPERCASE letter suffix (e.g., "800-56A" -> number + Part("", "A"))
560
+ # Only matches uppercase letters - won't match revision markers.
561
+ # Series policy decides whether the letter stays in the number
562
+ # (MR format, RPT/FIPS/IR/CRPL/LC/MONO/MP) or becomes a Part.
563
+ elsif str_value =~ /^(.+?)([A-Z])$/
564
+ number_part = $1
565
+ letter_part = $2
566
+
567
+ if Series.for(parsed_hash).preserve_letter_suffix?(parsed_hash)
568
+ return { type => Components::Code.new(value: str_value) }
569
+ else
570
+ return {
571
+ type => Components::Code.new(value: number_part),
572
+ part: Components::Part.new(type: "", value: letter_part),
573
+ }
574
+ end
575
+ end
576
+
577
+ Components::Code.new(value: str_value)
578
+
579
+ when :crpl_range
580
+ return nil if value.nil? || value.to_s.strip.empty?
581
+
582
+ # For CRPL range patterns like "2_3-1" or "2_3-1A" (with supplement)
583
+ # Format: X_Y-Z where X,Y,Z are digits, optional trailing letter is supplement
584
+ # This should split into:
585
+ # - X -> second_number (to combine with first_number as "1-X")
586
+ # - Y-Z -> Part component (with type "pt" for CRPL)
587
+ # - trailing letter (if present) -> Supplement
588
+ str_value = value.to_s
589
+
590
+ # Check for supplement letter suffix (e.g., "2_3-1A" -> supplement="A")
591
+ if str_value =~ /^(\d+)_(\d+-\d+)([A-Z])$/
592
+ second_num_part = $1 # "2"
593
+ part_value = $2 # "3-1"
594
+ supplement_letter = $3 # "A"
595
+
596
+ # Return second_number, Part, and Supplement
597
+ {
598
+ second_number: Components::Code.new(value: second_num_part),
599
+ part: Components::Part.new(type: "pt", value: part_value),
600
+ supplement: supplement_letter,
601
+ }
602
+ elsif str_value =~ /^(\d+)_(\d+-\d+)$/
603
+ # No supplement letter
604
+ second_num_part = $1 # "2"
605
+ part_value = $2 # "3-1"
606
+
607
+ # Return second_number and Part
608
+ {
609
+ second_number: Components::Code.new(value: second_num_part),
610
+ part: Components::Part.new(type: "pt", value: part_value),
611
+ }
612
+ else
613
+ # Fallback: treat entire value as second_number (shouldn't happen with valid CRPL patterns)
614
+ Components::Code.new(value: str_value)
615
+ end
616
+
617
+ # ========== V2 COMPONENT CASTING ==========
618
+
619
+ when :stage
620
+ # Stage from nested hash with id and type
621
+ return nil unless value.is_a?(Hash)
622
+
623
+ stage_id = value[:stage_id]&.to_s&.downcase
624
+ stage_type = value[:stage_type]&.to_s&.downcase
625
+ return nil if stage_id.nil? || stage_type.nil? || stage_id.empty? || stage_type.empty?
626
+
627
+ # Return as hash to set the stage attribute
628
+ { stage: Components::Stage.new(id: stage_id, type: stage_type) }
629
+
630
+ when :stage_id, :stage_type
631
+ # These are captured by :stage, so skip individual processing
632
+ nil
633
+
634
+ when :parsed_format
635
+ # Format detection result from parser. :short is the render default
636
+ # (a nil parsed_format renders short — see Identifiers::Base#to_s), so
637
+ # store only non-default formats (e.g. "mr"); "short" stays unset and
638
+ # is omitted from to_hash. detect_format only emits :mr or :short.
639
+ v = value&.to_s
640
+ v unless v == "short"
641
+
642
+ when :translation
643
+ # V1 TRANSLATION NORMALIZATION
644
+ return nil if value.nil? || value.to_s.strip.empty?
645
+
646
+ code = value.to_s.strip.downcase
647
+ # Apply normalization map (es -> spa, pt -> por, etc.)
648
+ normalized_code = TRANSLATION_MAP[code] || code
649
+
650
+ # Return as hash to set translation_component attribute
651
+ { translation_component: Components::Translation.new(code: normalized_code) }
652
+
653
+ when :version
654
+ # Version component with dotted notation
655
+ return nil if value.nil? || value.to_s.strip.empty?
656
+
657
+ # Return as hash to set version_component attribute
658
+ { version_component: Components::Version.new(value: value.to_s) }
659
+
660
+ when :update
661
+ # Update component with number, year, and optional month
662
+ if value.is_a?(Hash)
663
+ # Convert Parslet slice to regular Hash for reliable key access
664
+ value_hash = value.to_h
665
+
666
+ number = value_hash[:update_number]&.to_s # Don't default to "1"
667
+ year = value_hash[:update_year]&.to_s # String not integer
668
+ month = value_hash[:update_month]&.to_s # String not integer
669
+
670
+ # Determine prefix from update_prefix key (captured by parser)
671
+ # If not present, default to "slash" (/Upd format)
672
+ prefix_str = value_hash[:update_prefix]&.to_s
673
+ prefix_value = if prefix_str&.include?("-") || prefix_str == "-upd"
674
+ "dash"
675
+ else
676
+ "slash"
677
+ end
678
+
679
+ # Create update with at least number
680
+ update_obj = Components::Update.new(number: number, year: year,
681
+ month: month, prefix: prefix_value)
682
+ {
683
+ update: update_obj, # Main attribute for tests
684
+ update_component: update_obj, # V2 component
685
+ }
686
+ elsif value.to_s.strip.empty?
687
+ # Empty update string means "-upd" or "/upd" with no details
688
+ # Create Update with default number="1" (no year/month)
689
+ # Check update_prefix key to determine correct prefix format
690
+ prefix_str = parsed_hash[:update_prefix]&.to_s
691
+ prefix_value = if prefix_str&.include?("-") || prefix_str == "-upd"
692
+ "dash"
693
+ else
694
+ "slash"
695
+ end
696
+ update_obj = Components::Update.new(number: "1", year: nil,
697
+ month: nil, prefix: prefix_value)
698
+ {
699
+ update: update_obj,
700
+ update_component: update_obj,
701
+ }
702
+ else
703
+ # Simple string value - shouldn't reach here
704
+ { update: value.to_s.strip } unless value.to_s.strip.empty?
705
+ end
706
+
707
+ when :update_prefix, :update_number, :update_year, :update_month
708
+ # Captured as part of :update processing
709
+ nil
710
+
711
+ # ========== END V2 COMPONENTS ==========
712
+
713
+ when :volume, :section, :appendix, :translation,
714
+ :errata, :index, :insert, :version
715
+ return nil if value.nil?
716
+ return nil if value.is_a?(Array) && value.empty?
717
+
718
+ str_value = value.to_s.strip
719
+ return nil if str_value.empty?
720
+
721
+ # For volume, create Volume component from string value
722
+ # This handles patterns like "v1" that come from parser as simple strings
723
+ if type == :volume
724
+ { volume: Components::Volume.new(value: str_value) }
725
+ else
726
+ str_value
727
+ end
728
+
729
+ when :revision
730
+ # Revision MUST be Edition component with type "r"
731
+ return nil if value.nil? || value.to_s.strip.empty?
732
+
733
+ # Handle new structure with :revision_prefix and :revision_id (format preservation)
734
+ if value.is_a?(Hash) && value[:revision_prefix] && value[:revision_id]
735
+ prefix = value[:revision_prefix].to_s
736
+ id = value[:revision_id].to_s.strip
737
+
738
+ # Normalize bare "r" -> "r1"
739
+ revision_id = if id.empty? || id == "r" || id == "R"
740
+ "1"
741
+ # Handle "r4", "R5", "4" etc. (but prefix already has the r/rev/etc.)
742
+ elsif id =~ /^(\d+[a-z]?)$/
743
+ $1
744
+ else
745
+ id
746
+ end
747
+
748
+ # Return Edition component with original_prefix for format preservation
749
+ {
750
+ edition: Components::Edition.new(type: "r", id: revision_id,
751
+ original_prefix: prefix),
752
+ }
753
+ else
754
+ # Legacy handling: revision as simple string value
755
+ str_value = value.to_s.strip
756
+
757
+ # Handle bare "r" -> normalize to "r1"
758
+ revision_id = if str_value.empty? || str_value == "r" || str_value == "R"
759
+ "1"
760
+ # Handle "r4", "R5", "4" etc.
761
+ elsif str_value =~ /^[rR]?(\d+[a-z]?)$/
762
+ $1
763
+ else
764
+ str_value
765
+ end
766
+
767
+ # Return Edition component (no original_prefix available)
768
+ {
769
+ edition: Components::Edition.new(type: "r", id: revision_id),
770
+ }
771
+ end
772
+
773
+ when :revision_year, :revision_month
774
+ # When revision_year comes from parser as separate element (e.g., "1019 r1963")
775
+ # Create Edition component
776
+ if type == :revision_year
777
+ year_value = value.to_s.strip
778
+ # Check if this should be an Edition component or legacy revision_year
779
+ # If revision_month is also present, use legacy attributes for "revJune1908" pattern
780
+ if parsed_hash[:revision_month]
781
+ # Legacy: revision with month - keep as revision_year/revision_month
782
+ year_value
783
+ else
784
+ # V2: revision with year only - create Edition component
785
+ {
786
+ edition: Components::Edition.new(type: "r", id: year_value),
787
+ }
788
+ end
789
+ else
790
+ # revision_month - preserve as string for legacy rendering
791
+ return nil if value.nil? || value.to_s.strip.empty?
792
+
793
+ value.to_s.strip
794
+ end
795
+
796
+ when :edition_year_separate
797
+ # NEW: Edition year from "e2-1915" pattern (captured separately by parser)
798
+ # This comes with first_number like "11e2" and separate year "1915"
799
+ # Already handled in first_number regex matching above, but if it reaches here
800
+ # as a separate capture, we need to process it
801
+ return nil if value.nil? || value.to_s.strip.empty?
802
+
803
+ value.to_s # Return as string for potential use
804
+
805
+ when :historical_month
806
+ # NEW: Historical month from "-April1909" pattern
807
+ # Handled in first_number pattern matching, but return as string if separate
808
+ return nil if value.nil? || value.to_s.strip.empty?
809
+
810
+ value.to_s
811
+
812
+ when :historical_year
813
+ # NEW: Historical year from "-April1909" pattern
814
+ # Handled in first_number pattern matching, but return as string if separate
815
+ return nil if value.nil? || value.to_s.strip.empty?
816
+
817
+ value.to_s
818
+
819
+ when :supplement_year
820
+ # NEW: Supplement year from "supp-1924" pattern (captured separately by parser)
821
+ # This comes with first_number like "25supp" and separate year "1924"
822
+ # Already handled in first_number regex matching above, but if it reaches here
823
+ # as a separate capture, return as supplement value
824
+ return nil if value.nil? || value.to_s.strip.empty?
825
+
826
+ { supplement: value.to_s } # Return as supplement attribute
827
+
828
+ when :supplement
829
+ handle_supplement_cast(value)
830
+
831
+ when :supplement_date_range
832
+ return nil unless value.is_a?(Hash)
833
+
834
+ month_start = value[:supp_month_start]&.to_s
835
+ year_start = value[:supp_year_start]&.to_s
836
+ month_end = value[:supp_month_end]&.to_s
837
+ year_end = value[:supp_year_end]&.to_s
838
+
839
+ {
840
+ supplement_date_range_start: (month_start && year_start ? "#{month_start}#{year_start}" : nil),
841
+ supplement_date_range_end: (month_end && year_end ? "#{month_end}#{year_end}" : nil),
842
+ }
843
+
844
+ when :supplement_date
845
+ return nil unless value.is_a?(Hash)
846
+
847
+ month = value[:supp_month]&.to_s
848
+ year = value[:supp_year]&.to_s
849
+
850
+ month && year ? "#{month}#{year}" : nil
851
+
852
+ when :supplement_slash_year
853
+ return nil unless value.is_a?(Hash)
854
+
855
+ number = value[:supp_number]&.to_s
856
+ year = value[:supp_year]&.to_s
857
+
858
+ number && year ? "#{number}/#{year}" : nil
859
+
860
+ when :supplement_with_rev
861
+ { supplement: "", supplement_has_revision: true }
862
+
863
+ when :supp_year
864
+ # Parser extracts supplement year from patterns like "187supp1924"
865
+ # This should set the supplement attribute with the year value
866
+ { supplement: value.to_s }
867
+
868
+ # ========== V2 EDITION COMPONENT ==========
869
+
870
+ when :edition_e_date
871
+ # Edition with "e" prefix + 6-digit date (YYYYMM): e199206, e202103
872
+ # Used for IR revision+month patterns after preprocessing: "4743rJun1992" -> "4743e199206"
873
+ return nil unless value.is_a?(Hash) && value[:edition_date]
874
+
875
+ edition_date = value[:edition_date].to_s
876
+ # Parse 6-digit date as YYYYMM
877
+ # Store as id directly - renders as "e199206"
878
+ {
879
+ edition: Components::Edition.new(type: "e", id: edition_date),
880
+ edition_component: Components::Edition.new(type: "e",
881
+ id: edition_date),
882
+ }
883
+
884
+ when :edition_e
885
+ # Edition with "e" prefix: e2, e2021
886
+ return nil unless value.is_a?(Hash) && value[:edition_id]
887
+
888
+ edition_id = value[:edition_id].to_s
889
+
890
+ {
891
+ edition: Components::Edition.new(type: "e", id: edition_id),
892
+ edition_component: Components::Edition.new(type: "e",
893
+ id: edition_id),
894
+ }
895
+
896
+ when :edition_r
897
+ # Revision with "r" prefix: r5, r2021
898
+ return nil unless value.is_a?(Hash) && value[:edition_id]
899
+
900
+ edition_id = value[:edition_id].to_s
901
+
902
+ {
903
+ edition: Components::Edition.new(type: "r", id: edition_id),
904
+ edition_component: Components::Edition.new(type: "r",
905
+ id: edition_id),
906
+ revision: "r#{edition_id}", # Also set revision string attribute for compatibility
907
+ }
908
+
909
+ when :edition_r_no_space
910
+ # Revision with "r" prefix (no space pattern): r2, r5
911
+ # Used for patterns like "800-56Ar2" where edition is "r2"
912
+ return nil unless value.is_a?(Hash) && value[:edition_id]
913
+
914
+ edition_id = value[:edition_id].to_s
915
+
916
+ {
917
+ edition: Components::Edition.new(type: "r", id: edition_id),
918
+ edition_component: Components::Edition.new(type: "r",
919
+ id: edition_id),
920
+ revision: "r#{edition_id}", # Also set revision string attribute for compatibility
921
+ }
922
+
923
+ when :edition_rev
924
+ # Revision with "rev" prefix (verbose): rev2013, rev 2013
925
+ return nil unless value.is_a?(Hash) && value[:edition_id]
926
+
927
+ edition_id = value[:edition_id].to_s
928
+
929
+ {
930
+ edition: Components::Edition.new(type: "r", id: edition_id),
931
+ edition_component: Components::Edition.new(type: "r",
932
+ id: edition_id),
933
+ revision: "r#{edition_id}", # Also set revision string attribute for compatibility
934
+ }
935
+
936
+ when :edition_r_letter
937
+ # Revision with "r" prefix and letter suffix: r1a, r2b (for SP patterns like 800-22r1a)
938
+ return nil unless value.is_a?(Hash) && value[:edition_id] && value[:edition_letter]
939
+
940
+ edition_id = value[:edition_id].to_s
941
+ edition_letter = value[:edition_letter].to_s.downcase
942
+
943
+ {
944
+ edition: Components::Edition.new(type: "r", id: edition_id,
945
+ additional_text: edition_letter),
946
+ edition_component: Components::Edition.new(type: "r",
947
+ id: edition_id,
948
+ additional_text: edition_letter),
949
+ revision: "r#{edition_id}#{edition_letter}", # Also set revision string attribute for compatibility
950
+ }
951
+
952
+ when :edition_r_letter_only
953
+ # Revision with "r" prefix and only letter (no digit): ra, rb (for SP patterns like 800-27ra)
954
+ return nil unless value.is_a?(Hash) && value[:edition_letter]
955
+
956
+ edition_letter = value[:edition_letter].to_s.downcase
957
+
958
+ {
959
+ edition: Components::Edition.new(type: "r", id: edition_letter),
960
+ edition_component: Components::Edition.new(type: "r",
961
+ id: edition_letter),
962
+ revision: "r#{edition_letter}", # Also set revision string attribute for compatibility
963
+ }
964
+
965
+ when :edition_historical
966
+ # Historical with "-" prefix: -3, -4
967
+ return nil unless value.is_a?(Hash) && value[:edition_id]
968
+
969
+ edition_id = value[:edition_id].to_s
970
+
971
+ {
972
+ edition: Components::Edition.new(type: "-", id: edition_id),
973
+ edition_component: Components::Edition.new(type: "-",
974
+ id: edition_id),
975
+ }
976
+
977
+ when :edition_r_with_space_letter
978
+ # Revision with "r" prefix, space, and letter: r 5A (format preservation)
979
+ # Used for patterns like "NIST SP 800-53 r5A"
980
+ # NOTE: If there's an update component, the space was added by preprocessing
981
+ return nil unless value.is_a?(Hash) && value[:edition_id] && value[:edition_letter]
982
+
983
+ edition_id = value[:edition_id].to_s
984
+ edition_letter = value[:edition_letter].to_s.upcase
985
+
986
+ # Check if this is an embedded edition with update (space added by preprocessing)
987
+ has_update = parsed_hash[:update_prefix] || parsed_hash[:update]
988
+
989
+ if has_update
990
+ # No original_prefix - space was added by preprocessing
991
+ {
992
+ edition: Components::Edition.new(type: "r", id: edition_id,
993
+ additional_text: edition_letter),
994
+ edition_component: Components::Edition.new(type: "r",
995
+ id: edition_id,
996
+ additional_text: edition_letter),
997
+ revision: "r#{edition_id}#{edition_letter}",
998
+ }
999
+ else
1000
+ # Space was in original input - preserve format
1001
+ {
1002
+ edition: Components::Edition.new(type: "r", id: edition_id,
1003
+ additional_text: edition_letter,
1004
+ original_prefix: " r"),
1005
+ edition_component: Components::Edition.new(type: "r",
1006
+ id: edition_id,
1007
+ additional_text: edition_letter,
1008
+ original_prefix: " r"),
1009
+ revision: "r#{edition_id}#{edition_letter}",
1010
+ }
1011
+ end
1012
+
1013
+ when :edition_r_with_space
1014
+ # Revision with "r" prefix and space: r 5 (format preservation)
1015
+ # Used for patterns like "NIST SP 800-53 r5"
1016
+ # NOTE: If there's an update component, the space was added by preprocessing
1017
+ # for patterns like "8115r1/upd" -> "8115 r1/upd", so don't set original_prefix
1018
+ return nil unless value.is_a?(Hash) && value[:edition_id]
1019
+
1020
+ edition_id = value[:edition_id].to_s
1021
+
1022
+ # Check if this is an embedded edition with update (space added by preprocessing)
1023
+ # Patterns like "8115r1/upd" become "8115 r1/upd" after preprocessing
1024
+ has_update = parsed_hash[:update_prefix] || parsed_hash[:update]
1025
+
1026
+ if has_update
1027
+ # No original_prefix - space was added by preprocessing
1028
+ {
1029
+ edition: Components::Edition.new(type: "r", id: edition_id),
1030
+ edition_component: Components::Edition.new(type: "r",
1031
+ id: edition_id),
1032
+ revision: "r#{edition_id}",
1033
+ }
1034
+ else
1035
+ # Space was in original input - preserve format
1036
+ {
1037
+ edition: Components::Edition.new(type: "r", id: edition_id,
1038
+ original_prefix: " r"),
1039
+ edition_component: Components::Edition.new(type: "r",
1040
+ id: edition_id,
1041
+ original_prefix: " r"),
1042
+ revision: "r#{edition_id}",
1043
+ }
1044
+ end
1045
+
1046
+ when :edition_id
1047
+ # Captured by edition_e, edition_r, edition_rev, edition_historical
1048
+ nil
1049
+
1050
+ when :edition_date
1051
+ # Captured by edition_e_date
1052
+ nil
1053
+
1054
+ # ========== LEGACY EDITION (for migration) ==========
1055
+
1056
+ when :legacy_edition
1057
+ # Legacy edition patterns - will be phased out
1058
+ # For now, map to old edition_year/edition_month attributes
1059
+ nil # Handled by existing edition_year logic below
1060
+
1061
+ when :edition_month, :edition_year, :edition_day, :edition_has_rev
1062
+ # These work together: edition_month + edition_year -> single edition ID
1063
+ # Skip processing if this is edition_month alone (will be processed with edition_year)
1064
+ return nil if type == :edition_month
1065
+
1066
+ # Process edition_year, combining with edition_month if present
1067
+ return nil if value.nil? || value.to_s.strip.empty?
1068
+
1069
+ # Build the edition ID from year and optional month
1070
+ edition_id = value.to_s # Start with year (e.g., "1985")
1071
+
1072
+ # Add month if present (e.g., "Mar" -> "03", so "1985" + "03" = "198503")
1073
+ # For FIPS with day: "Sep30/1977" -> "19770930" (year + month + day)
1074
+ if parsed_hash[:edition_month]
1075
+ month_str = parsed_hash[:edition_month].to_s
1076
+ month_num = Date::ABBR_MONTHNAMES.index(month_str) ||
1077
+ Date::MONTHNAMES.index(month_str) ||
1078
+ month_str.to_i
1079
+ if month_num&.positive?
1080
+ # FIPS uses modern (numeric) date format; historical NBS keeps
1081
+ # month names like "April1909" instead of "190904".
1082
+ modern = Series.for(parsed_hash).modern_edition_date?
1083
+ if !modern && month_str.match?(/^[A-Z][a-z]+/) && edition_id.to_s.match?(/^\d{4}$/)
1084
+ # Historical NBS month+year format: preserve month name, use "-" type for special rendering
1085
+ edition_obj = Components::Edition.new(
1086
+ type: "-",
1087
+ id: "",
1088
+ additional_text: "#{month_str}#{edition_id}",
1089
+ )
1090
+ return {
1091
+ edition: edition_obj,
1092
+ edition_component: edition_obj,
1093
+ edition_year: edition_id.to_s,
1094
+ }
1095
+ else
1096
+ # Modern format (and FIPS): combine year and month as single number: 1985 + 03 = 198503
1097
+ edition_id = "#{edition_id}#{format('%02d', month_num)}"
1098
+
1099
+ # For FIPS with day, append day as well: "Sep30/1977" -> "19770930"
1100
+ if modern && parsed_hash[:edition_day]
1101
+ day_num = parsed_hash[:edition_day].to_s.to_i
1102
+ if day_num.positive? && day_num <= 31
1103
+ edition_id = "#{edition_id}#{format('%02d', day_num)}"
1104
+ end
1105
+ end
1106
+ end
1107
+ end
1108
+ end
1109
+
1110
+ # Create Edition component with type="e" (edition) and combined ID
1111
+ edition_obj = Components::Edition.new(type: "e", id: edition_id)
1112
+
1113
+ # Return as hash to set edition and edition_year
1114
+ {
1115
+ edition: edition_obj, # Main attribute for tests
1116
+ edition_component: edition_obj, # V2 component
1117
+ edition_year: value.to_s, # Keep string for render logic
1118
+ }
1119
+
1120
+ when :part
1121
+ # Part component - handle part number with optional addendum
1122
+ return nil if value.nil? || value.to_s.strip.empty?
1123
+
1124
+ str_value = value.to_s.strip
1125
+
1126
+ # Pattern: "1adde1" -> Part(value: "1"), addendum=true
1127
+ # Note: eN after add is discarded (not included in output per fixture)
1128
+ if str_value =~ /^(\d+)add(e\d+)$/
1129
+ {
1130
+ part: Components::Part.new(type: "pt", value: $1),
1131
+ addendum: "true",
1132
+ }
1133
+ elsif str_value =~ /^(\d+)add/
1134
+ {
1135
+ part: Components::Part.new(type: "pt", value: $1),
1136
+ addendum: "true",
1137
+ }
1138
+ else
1139
+ # Just a part number - return Part component with pt type
1140
+ { part: Components::Part.new(type: "pt", value: str_value) }
1141
+ end
1142
+
1143
+ when :part_extracted
1144
+ # Legacy - this is now handled by :part
1145
+ nil
1146
+
1147
+ when :edition_letter
1148
+ return nil if value.nil? || value.to_s.strip.empty?
1149
+
1150
+ value.to_s
1151
+
1152
+ when :public_draft
1153
+ return nil if value.nil?
1154
+
1155
+ value.to_s
1156
+
1157
+ when :draft
1158
+ # Extract draft number from "-draft N" pattern for pd rendering
1159
+ return nil if value.nil?
1160
+
1161
+ str_value = value.to_s.strip
1162
+ return nil if str_value.empty?
1163
+
1164
+ # Pattern: " -draft 2" or "-draft 2" -> extract "2" for pd rendering
1165
+ if str_value =~ /^\s*-draft\s+(\d+)$/
1166
+ { draft_number: $1 }
1167
+ # Pattern: " 2pd" -> already in pd format
1168
+ elsif str_value =~ /^\s*(\d+)pd$/
1169
+ { public_draft: $1 }
1170
+ # Other patterns (parenthetical, simple -draft)
1171
+ else
1172
+ str_value
1173
+ end
1174
+
1175
+ when :addendum
1176
+ handle_addendum_cast(value)
1177
+
1178
+ when :addendum_number
1179
+ return nil if value.nil? || value.to_s.strip.empty?
1180
+
1181
+ value.to_s
1182
+
1183
+ when :supplement_suffix
1184
+ # Return as hash to set supplement attribute (not supplement_suffix)
1185
+ { supplement: value.to_s }
1186
+
1187
+ when :date
1188
+ # Date component per NIST spec
1189
+ return nil unless value.is_a?(Hash)
1190
+
1191
+ # NEW: Check if this is historical edition pattern ("-April1909")
1192
+ # Parser captures as date with month + year, but semantically it's an edition
1193
+ if value[:date_month] && value[:date_year] && !value[:date_day]
1194
+ month_str = value[:date_month].to_s
1195
+ year_str = value[:date_year].to_s
1196
+ # If month is a word like "April", this is historical edition format
1197
+ if month_str.match?(/^[A-Za-z]+$/)
1198
+ return {
1199
+ edition: Components::Edition.new(type: "-",
1200
+ additional_text: "#{month_str}#{year_str}"),
1201
+ }
1202
+ end
1203
+ end
1204
+
1205
+ # Regular date processing
1206
+ value[:date_year]&.to_s
1207
+ value[:date_month]&.to_s
1208
+ value[:date_day]&.to_s
1209
+
1210
+ else
1211
+ # Unknown types: return the original value for default processing
1212
+ # This allows hashes with arbitrary structures to be processed
1213
+ # e.g., second_number hash with number_only and edition_id
1214
+ value.is_a?(Hash) ? value : nil
1215
+ end
1216
+ end
1217
+
1218
+ # Convert month name to month number
1219
+ # @param month_name [String] month abbreviation (Jan, Feb, Mar, etc.)
1220
+ # @return [Integer] month number (1-12)
1221
+ def month_name_to_number(month_name)
1222
+ month_map = {
1223
+ "Jan" => 1, "January" => 1,
1224
+ "Feb" => 2, "February" => 2,
1225
+ "Mar" => 3, "March" => 3,
1226
+ "Apr" => 4, "April" => 4,
1227
+ "May" => 5,
1228
+ "Jun" => 6, "June" => 6,
1229
+ "Jul" => 7, "July" => 7,
1230
+ "Aug" => 8, "August" => 8,
1231
+ "Sep" => 9, "Sept" => 9, "September" => 9,
1232
+ "Oct" => 10, "October" => 10,
1233
+ "Nov" => 11, "November" => 11,
1234
+ "Dec" => 12, "December" => 12,
1235
+ }
1236
+ month_map[month_name] || 1 # Default to January if not found
1237
+ end
1238
+
1239
+ # Handle supplement casting with all its variants
1240
+ def handle_supplement_cast(value)
1241
+ return nil unless value
1242
+
1243
+ if value.is_a?(Array) && value.empty?
1244
+ # Empty array means "supp" was present but no suffix
1245
+ ""
1246
+ else
1247
+ str_value = value.to_s.strip
1248
+ str_value.empty? ? nil : str_value
1249
+ end
1250
+ end
1251
+
1252
+ # Handle addendum casting (number)
1253
+ def handle_addendum_cast(value)
1254
+ if value.is_a?(Hash)
1255
+ addendum_num = value[:addendum_number]&.to_s&.strip
1256
+ if addendum_num && !addendum_num.empty?
1257
+ { addendum_number: addendum_num }
1258
+ else
1259
+ { addendum: "true" }
1260
+ end
1261
+ else
1262
+ str_value = value.to_s.strip
1263
+ if str_value.empty?
1264
+ { addendum: "true" }
1265
+ else
1266
+ { addendum_number: str_value }
1267
+ end
1268
+ end
1269
+ end
1270
+ end
1271
+ end
1272
+ end