pubid 2.0.0.pre.alpha.12 → 2.0.0.pre.alpha.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +43 -1
  3. data/data/ieee/update_codes.yaml +17 -4
  4. data/data/nist/update_codes.yaml +7 -3
  5. data/lib/pubid/adobe/builder.rb +2 -0
  6. data/lib/pubid/all_parts.rb +201 -0
  7. data/lib/pubid/all_parts_identifier.rb +19 -0
  8. data/lib/pubid/amca/CLAUDE.md +47 -0
  9. data/lib/pubid/amca/builder.rb +3 -5
  10. data/lib/pubid/amca/identifiers/base.rb +10 -0
  11. data/lib/pubid/amca/identifiers/publication.rb +13 -0
  12. data/lib/pubid/amca/parser.rb +2 -1
  13. data/lib/pubid/amca/renderer.rb +22 -33
  14. data/lib/pubid/amca/urn_generator.rb +21 -2
  15. data/lib/pubid/amca/urn_parser.rb +36 -10
  16. data/lib/pubid/ansi/builder.rb +6 -0
  17. data/lib/pubid/api/CLAUDE.md +23 -0
  18. data/lib/pubid/api/builder.rb +2 -0
  19. data/lib/pubid/ashrae/CLAUDE.md +13 -0
  20. data/lib/pubid/ashrae/builder.rb +58 -14
  21. data/lib/pubid/ashrae/identifiers/errata.rb +14 -2
  22. data/lib/pubid/ashrae/identifiers/interpretation.rb +2 -10
  23. data/lib/pubid/ashrae/parser.rb +62 -28
  24. data/lib/pubid/ashrae/renderer.rb +32 -1
  25. data/lib/pubid/ashrae/urn_generator.rb +32 -9
  26. data/lib/pubid/asme/CLAUDE.md +25 -0
  27. data/lib/pubid/asme/builder.rb +16 -9
  28. data/lib/pubid/asme/components/code.rb +2 -0
  29. data/lib/pubid/asme/identifiers/standard.rb +6 -1
  30. data/lib/pubid/asme/parser.rb +41 -14
  31. data/lib/pubid/astm/CLAUDE.md +9 -0
  32. data/lib/pubid/astm/builder.rb +2 -0
  33. data/lib/pubid/astm/components/code.rb +2 -0
  34. data/lib/pubid/astm/parser.rb +4 -1
  35. data/lib/pubid/bipm/CLAUDE.md +11 -0
  36. data/lib/pubid/bipm/builder.rb +2 -0
  37. data/lib/pubid/bsi/CLAUDE.md +93 -0
  38. data/lib/pubid/bsi/builder.rb +13 -11
  39. data/lib/pubid/bsi/identifiers/addendum_document.rb +2 -0
  40. data/lib/pubid/bsi/identifiers/adopted_european_norm.rb +6 -54
  41. data/lib/pubid/bsi/identifiers/adopted_international_standard.rb +5 -22
  42. data/lib/pubid/bsi/identifiers/amendment.rb +36 -12
  43. data/lib/pubid/bsi/identifiers/bundled_identifier.rb +2 -0
  44. data/lib/pubid/bsi/identifiers/consolidated_identifier.rb +23 -26
  45. data/lib/pubid/bsi/identifiers/corrigendum.rb +29 -12
  46. data/lib/pubid/bsi/identifiers/expert_commentary.rb +6 -7
  47. data/lib/pubid/bsi/identifiers/national_annex.rb +18 -20
  48. data/lib/pubid/bsi/identifiers/root_identity.rb +31 -0
  49. data/lib/pubid/bsi/identifiers/set.rb +2 -0
  50. data/lib/pubid/bsi/identifiers/supplement_document.rb +2 -0
  51. data/lib/pubid/bsi/identifiers.rb +1 -0
  52. data/lib/pubid/bsi/parser.rb +8 -8
  53. data/lib/pubid/bsi/renderer.rb +20 -20
  54. data/lib/pubid/bsi/urn_generator.rb +28 -18
  55. data/lib/pubid/builder/base.rb +27 -0
  56. data/lib/pubid/calconnect/builder.rb +2 -0
  57. data/lib/pubid/calconnect/identifier.rb +4 -0
  58. data/lib/pubid/ccsds/builder.rb +2 -0
  59. data/lib/pubid/ccsds/identifier.rb +8 -0
  60. data/lib/pubid/cen_cenelec/CLAUDE.md +59 -0
  61. data/lib/pubid/cen_cenelec/builder.rb +6 -1
  62. data/lib/pubid/cen_cenelec/identifier.rb +10 -27
  63. data/lib/pubid/cen_cenelec/identifiers/amendment.rb +3 -10
  64. data/lib/pubid/cen_cenelec/identifiers/corrigendum.rb +3 -10
  65. data/lib/pubid/cen_cenelec/parser.rb +11 -3
  66. data/lib/pubid/cie/CLAUDE.md +58 -0
  67. data/lib/pubid/cie/builder.rb +2 -0
  68. data/lib/pubid/cie/components/language.rb +2 -0
  69. data/lib/pubid/components/adoption.rb +2 -0
  70. data/lib/pubid/components/code.rb +2 -0
  71. data/lib/pubid/components/date.rb +8 -6
  72. data/lib/pubid/components/edition.rb +2 -0
  73. data/lib/pubid/components/iteration.rb +2 -0
  74. data/lib/pubid/components/language.rb +2 -0
  75. data/lib/pubid/components/locality.rb +2 -0
  76. data/lib/pubid/components/publisher.rb +2 -0
  77. data/lib/pubid/components/relationship.rb +2 -0
  78. data/lib/pubid/components/stage.rb +2 -0
  79. data/lib/pubid/components/supplement.rb +2 -0
  80. data/lib/pubid/components/type.rb +2 -0
  81. data/lib/pubid/components/typed_stage.rb +8 -0
  82. data/lib/pubid/csa/CLAUDE.md +41 -0
  83. data/lib/pubid/csa/builder.rb +2 -0
  84. data/lib/pubid/csa/identifier.rb +13 -1
  85. data/lib/pubid/csa/renderer.rb +12 -12
  86. data/lib/pubid/csa/single_identifier.rb +17 -0
  87. data/lib/pubid/doi/builder.rb +2 -0
  88. data/lib/pubid/easc/builder.rb +2 -0
  89. data/lib/pubid/ecma/CLAUDE.md +28 -0
  90. data/lib/pubid/ecma/builder.rb +2 -0
  91. data/lib/pubid/ecma/identifier.rb +7 -0
  92. data/lib/pubid/etsi/CLAUDE.md +34 -0
  93. data/lib/pubid/etsi/builder.rb +2 -0
  94. data/lib/pubid/etsi/components/code.rb +6 -0
  95. data/lib/pubid/etsi/components/version.rb +2 -0
  96. data/lib/pubid/etsi/identifiers/etsi_standard.rb +7 -0
  97. data/lib/pubid/evs/CLAUDE.md +58 -0
  98. data/lib/pubid/evs/builder.rb +2 -0
  99. data/lib/pubid/gb/CLAUDE.md +140 -0
  100. data/lib/pubid/gb/builder.rb +7 -2
  101. data/lib/pubid/gb/identifier.rb +5 -3
  102. data/lib/pubid/gb/identifiers/all_parts.rb +17 -0
  103. data/lib/pubid/gb/identifiers.rb +1 -0
  104. data/lib/pubid/gb/renderer.rb +0 -1
  105. data/lib/pubid/gost/CLAUDE.md +64 -0
  106. data/lib/pubid/gost/builder.rb +3 -1
  107. data/lib/pubid/gost/identifier.rb +5 -0
  108. data/lib/pubid/iala/CLAUDE.md +82 -0
  109. data/lib/pubid/iala/builder.rb +2 -0
  110. data/lib/pubid/iana/CLAUDE.md +7 -0
  111. data/lib/pubid/iana/builder.rb +2 -0
  112. data/lib/pubid/identifier.rb +161 -17
  113. data/lib/pubid/idf/builder.rb +6 -1
  114. data/lib/pubid/idf/identifier.rb +5 -0
  115. data/lib/pubid/idf/identifiers/all_parts.rb +17 -0
  116. data/lib/pubid/idf/identifiers.rb +1 -0
  117. data/lib/pubid/iec/CLAUDE.md +31 -0
  118. data/lib/pubid/iec/builder.rb +7 -1
  119. data/lib/pubid/iec/components/consolidated_amendment.rb +4 -0
  120. data/lib/pubid/iec/components/sheet.rb +2 -0
  121. data/lib/pubid/iec/components/trf_info.rb +2 -0
  122. data/lib/pubid/iec/components/vap_suffix.rb +2 -0
  123. data/lib/pubid/iec/identifier.rb +7 -2
  124. data/lib/pubid/iec/identifiers/all_parts.rb +19 -0
  125. data/lib/pubid/iec/identifiers.rb +1 -0
  126. data/lib/pubid/iec/renderer.rb +0 -1
  127. data/lib/pubid/iec/urn_generator.rb +9 -1
  128. data/lib/pubid/iec/urn_parser.rb +3 -2
  129. data/lib/pubid/ieee/CLAUDE.md +97 -0
  130. data/lib/pubid/ieee/builder.rb +134 -5
  131. data/lib/pubid/ieee/components/code.rb +2 -0
  132. data/lib/pubid/ieee/components/draft.rb +35 -2
  133. data/lib/pubid/ieee/components/typed_stage.rb +2 -0
  134. data/lib/pubid/ieee/identifiers/base.rb +20 -0
  135. data/lib/pubid/ieee/identifiers/iec_ieee_copublished.rb +9 -0
  136. data/lib/pubid/ieee/identifiers/joint_development.rb +17 -10
  137. data/lib/pubid/ieee/identifiers/project_draft_identifier.rb +8 -1
  138. data/lib/pubid/ieee/parser.rb +134 -21
  139. data/lib/pubid/ieee/renderer.rb +40 -7
  140. data/lib/pubid/ieee/urn_generator.rb +31 -0
  141. data/lib/pubid/ietf/CLAUDE.md +7 -0
  142. data/lib/pubid/ietf/builder.rb +2 -0
  143. data/lib/pubid/iho/builder.rb +2 -0
  144. data/lib/pubid/isbn/builder.rb +2 -0
  145. data/lib/pubid/iso/CLAUDE.md +47 -0
  146. data/lib/pubid/iso/builder.rb +19 -5
  147. data/lib/pubid/iso/components/publisher.rb +2 -0
  148. data/lib/pubid/iso/identifier.rb +6 -14
  149. data/lib/pubid/iso/identifiers/all_parts.rb +19 -0
  150. data/lib/pubid/iso/identifiers/directives_supplement.rb +4 -2
  151. data/lib/pubid/iso/identifiers.rb +1 -0
  152. data/lib/pubid/iso/rendering_style.rb +0 -1
  153. data/lib/pubid/itu/CLAUDE.md +69 -0
  154. data/lib/pubid/itu/builder.rb +2 -0
  155. data/lib/pubid/itu/components/code.rb +2 -0
  156. data/lib/pubid/itu/components/designation.rb +2 -0
  157. data/lib/pubid/itu/components/sector.rb +2 -0
  158. data/lib/pubid/itu/components/series.rb +2 -0
  159. data/lib/pubid/itu/identifiers/supplement.rb +15 -0
  160. data/lib/pubid/jcgm/CLAUDE.md +7 -0
  161. data/lib/pubid/jcgm/builder.rb +2 -0
  162. data/lib/pubid/jcgm/components/publisher.rb +2 -0
  163. data/lib/pubid/jis/builder.rb +5 -1
  164. data/lib/pubid/jis/identifier.rb +5 -17
  165. data/lib/pubid/jis/identifiers/all_parts.rb +19 -0
  166. data/lib/pubid/jis/identifiers.rb +1 -0
  167. data/lib/pubid/jis/renderer.rb +0 -2
  168. data/lib/pubid/jis/urn_generator.rb +0 -1
  169. data/lib/pubid/nist/CLAUDE.md +56 -0
  170. data/lib/pubid/nist/builder.rb +2 -0
  171. data/lib/pubid/nist/components/edition.rb +2 -0
  172. data/lib/pubid/nist/components/issue_number.rb +2 -0
  173. data/lib/pubid/nist/components/part.rb +2 -0
  174. data/lib/pubid/nist/components/stage.rb +2 -0
  175. data/lib/pubid/nist/components/supplement.rb +2 -0
  176. data/lib/pubid/nist/components/translation.rb +2 -0
  177. data/lib/pubid/nist/components/update.rb +2 -0
  178. data/lib/pubid/nist/components/version.rb +2 -0
  179. data/lib/pubid/nist/components/volume.rb +2 -0
  180. data/lib/pubid/nist/identifiers/base.rb +24 -3
  181. data/lib/pubid/nist/parser.rb +15 -2
  182. data/lib/pubid/nist/preprocessor.rb +53 -2
  183. data/lib/pubid/oasis/CLAUDE.md +19 -0
  184. data/lib/pubid/oasis/builder.rb +2 -0
  185. data/lib/pubid/oasis/identifier.rb +19 -0
  186. data/lib/pubid/ogc/CLAUDE.md +34 -0
  187. data/lib/pubid/ogc/builder.rb +2 -0
  188. data/lib/pubid/ogc/identifier.rb +11 -0
  189. data/lib/pubid/oiml/CLAUDE.md +189 -0
  190. data/lib/pubid/oiml/builder.rb +20 -0
  191. data/lib/pubid/oiml/components/code.rb +6 -0
  192. data/lib/pubid/oiml/identifier.rb +13 -0
  193. data/lib/pubid/oiml/identifiers/annex.rb +4 -0
  194. data/lib/pubid/oiml/identifiers/certification_system.rb +34 -0
  195. data/lib/pubid/oiml/identifiers/code_number.rb +8 -0
  196. data/lib/pubid/oiml/identifiers/dual_published.rb +174 -0
  197. data/lib/pubid/oiml/identifiers.rb +2 -0
  198. data/lib/pubid/oiml/parser.rb +35 -4
  199. data/lib/pubid/oiml/renderer.rb +23 -1
  200. data/lib/pubid/oiml/single_identifier.rb +4 -0
  201. data/lib/pubid/oiml/supplement_identifier.rb +7 -0
  202. data/lib/pubid/oiml/urn_generator.rb +28 -0
  203. data/lib/pubid/oiml.rb +5 -0
  204. data/lib/pubid/omg/CLAUDE.md +15 -0
  205. data/lib/pubid/omg/builder.rb +2 -0
  206. data/lib/pubid/parser/grammar.rb +23 -0
  207. data/lib/pubid/plateau/builder.rb +2 -0
  208. data/lib/pubid/plateau/identifiers/base.rb +4 -0
  209. data/lib/pubid/plateau/supplement_identifier.rb +14 -2
  210. data/lib/pubid/plateau/urn_generator.rb +7 -1
  211. data/lib/pubid/renderers/human_readable.rb +0 -1
  212. data/lib/pubid/sae/builder.rb +2 -0
  213. data/lib/pubid/sae/components/date.rb +2 -0
  214. data/lib/pubid/sae/components/type.rb +2 -0
  215. data/lib/pubid/subset_match.rb +197 -0
  216. data/lib/pubid/tgpp/CLAUDE.md +43 -0
  217. data/lib/pubid/tgpp/builder.rb +2 -0
  218. data/lib/pubid/tgpp/identifier.rb +14 -0
  219. data/lib/pubid/type_resolver.rb +14 -2
  220. data/lib/pubid/un/builder.rb +2 -0
  221. data/lib/pubid/version.rb +1 -1
  222. data/lib/pubid/w3c/CLAUDE.md +7 -0
  223. data/lib/pubid/w3c/builder.rb +2 -0
  224. data/lib/pubid/xsf/CLAUDE.md +11 -0
  225. data/lib/pubid/xsf/builder.rb +2 -0
  226. data/lib/pubid.rb +16 -3
  227. metadata +44 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b4e0a5a8d4b5c1410b934712637a882a703f34ded60d92ed1d8f0a6465974396
4
- data.tar.gz: 35bf6a1a075f68f35c682a3cfdde6bd5166523535491a74f1cb3473abcfaacec
3
+ metadata.gz: 177a3443a2daa5ff1848fb31a5136ea7eb05986005590c5938b0ae88874b3ef2
4
+ data.tar.gz: 4eeb9f9a26023aa610e821951febd68b5ef5a30db366eeeef07d595e67097557
5
5
  SHA512:
6
- metadata.gz: 7dcd7c3d8f91d1ef5fa1ab296e4f922b435b04b70a06d184e6d2449d32a270bf31b14f352523686921072aac37bd0f91e0b6f62a6d44c28349b04ca7079092ed
7
- data.tar.gz: ff8bc5531169fa3dace613d7369e656fdc2418f559adf3b527722e80144fd5a7aaa2db28da23979b2e3b8bc5af31132dca631e955a454d6262ad6a3ddf459b77
6
+ metadata.gz: 6d1a4ad6d0ec70be65ed8e8773c92feda1f63ec623ff59ee4e15a92683e762fa7ed9fc687391d8456731cfe6120c1ef144971422002ecdc9dfe40dfb5482959a
7
+ data.tar.gz: fa1d3d71d2dd96d24ac9e4804a8be54ecfbe02b8fd460a329f0ae6aa2366838670ed5242a3bb3f3d94b5e325dc6494902a94223a94a9ef377374f8ac8baf8fc3
data/README.adoc CHANGED
@@ -229,6 +229,48 @@ id.exclude(:year, :part).to_s
229
229
  # => "ISO 9001"
230
230
  ----
231
231
 
232
+ === Matching a Partial Reference
233
+
234
+ The `===` operator is a subset match. It returns true when the candidate holds
235
+ every part that the reference states. A part that the reference omits matches
236
+ any value. The receiver is the reference, so the operator is not symmetric:
237
+
238
+ [source,ruby]
239
+ ----
240
+ require 'pubid/iso'
241
+
242
+ reference = Pubid::Iso.parse("ISO 9001")
243
+ reference === Pubid::Iso.parse("ISO 9001:2015")
244
+ # => true
245
+ reference === Pubid::Iso.parse("ISO/DIS 9001")
246
+ # => false (a default value, here the published stage, is stated)
247
+
248
+ catalogue.grep(reference)
249
+ # => every edition of ISO 9001 in the catalogue
250
+ ----
251
+
252
+ Some flavors state a part even when it is nil, because the document really
253
+ has none. Those classes declare it strict, so the reference does not widen:
254
+
255
+ [source,ruby]
256
+ ----
257
+ Pubid::Ecma.parse("ECMA-418") === Pubid::Ecma.parse("ECMA-418-1")
258
+ # => false (ECMA-418 has no part)
259
+ Pubid::Ecma.parse("ECMA-418-1") === Pubid::Ecma.parse("ECMA-418-1 ed1")
260
+ # => true
261
+ ----
262
+
263
+ A reference asks for every part of a document with `all_parts`:
264
+
265
+ [source,ruby]
266
+ ----
267
+ Pubid::Iso.parse("ISO 9001 (all parts)") === Pubid::Iso.parse("ISO 9001-1:2015")
268
+ # => true
269
+ ----
270
+
271
+ See `docs/SUBSET_MATCH.md` for the full rules and the list of strict
272
+ attributes.
273
+
232
274
  === Comparing Editions
233
275
 
234
276
  The `new_edition_of?` method checks if an identifier is a newer edition of the same document:
@@ -1148,7 +1190,7 @@ ASME BPVC.CC.BPV-2021 # Case Code BPV
1148
1190
 
1149
1191
  Special Variants:
1150
1192
  ASME BPVC COMPLETE CODE BIND-2019 # Complete code set
1151
- ASME BPVC-CC-BPV-2019 # Dash notation variant
1193
+ ASME BPVC-CC-BPV-2019 # Dash notation; ASME lists it apart from BPVC.CC.BPV
1152
1194
  ----
1153
1195
 
1154
1196
  .BPVC Components
@@ -157,15 +157,15 @@ ISO/IEC/IEEE CD P26513/D1, August 2015: ISO/IEC/IEEE P26513/D1, August 2015
157
157
  "ISO/IEC/IEEE/P16085_DIS, March 2020": "ISO/IEC/IEEE DIS P16085-2020-03"
158
158
  "ANSI/IEEE Std: Outdoor Apparatus Bushings": "ANSI/IEEE Std 21-1976-11"
159
159
  "Draft National Electrical Safety Code, January 2016": "IEEE PC2-2016-01"
160
- "International Standard ISO/IEC 8802-9: 1996(E) ANSI/IEEE Std 802.9, 1996 Edition": "ISO/IEC/IEEE Std 802.9-1996"
161
- "ISO/IEC13210: 1994 (E) ANSI/IEEE Std 1003.3-1991": "ISO/IEC/IEEE 13210-1994"
160
+ "International Standard ISO/IEC 8802-9: 1996(E) ANSI/IEEE Std 802.9, 1996 Edition": "ISO/IEC 8802-9:1996(E); ANSI/IEEE Std 802.9, 1996 Edition"
161
+ "ISO/IEC13210: 1994 (E) ANSI/IEEE Std 1003.3-1991": "ISO/IEC 13210:1994 (E); ANSI/IEEE Std 1003.3-1991"
162
162
  "J-STD-016-1995": "IEEE 016-1995"
163
163
  "Nuclear EQ Sourcebook and Supplement": "IEEE 7438946"
164
164
  "PC37.30.2/D043 Rev 18, May 2015": "IEEE PC37.30-2/D-043/R-18-2015"
165
165
  "IEEE/IEC P62582 CD2 proposal, May 2017": "IEEE/IEC CD2 P62582-2017-05"
166
- "ISO/IEC/IEEE P16326:201x WD.4a, July 2017": "ISO/IEC/IEEE P16326/D-4a-2017-07"
166
+ "ISO/IEC/IEEE P16326:201x WD.4a, July 2017": "ISO/IEC/IEEE P16326:2017/D=WD.4a"
167
167
  "ISO/IEC/IEEE CD.1 P21839, October 2017": "ISO/IEC/IEEE CD1 P21839-2017-10"
168
- "ISO/IEC/IEEE P16326:201x WD5, December 2017": "ISO/IEC/IEEE P16326/D-5-2017-12"
168
+ "ISO/IEC/IEEE P16326:201x WD5, December 2017": "ISO/IEC/IEEE P16326:2017/D=WD.5"
169
169
  "IEEE/ISO/IEC P42010.CD1-V1.0, April 2020": "IEEE/ISO/IEC CD1 P42010-2020-04"
170
170
  "Unapproved Draft Std ISO/IEC FDIS 15288:2007(E) IEEE P15288/D3,": "ISO/IEC/IEEE FDIS Std P15288/D-3-2007"
171
171
  "IS0/IEC/IEEE 8802-11:2012/Amd.5:2015(E) (Adoption of IEEE Std 802.11af-2014)": "ISO/IEC/IEEE 802.11/Amd5-2012"
@@ -208,3 +208,16 @@ ISO/IEC/IEEE CD P26513/D1, August 2015: ISO/IEC/IEEE P26513/D1, August 2015
208
208
  "ISO/IEC/IEEE/ P12207.2/D-3_FDIS-2020": "ISO/IEC/IEEE FDIS P12207.2-2020/D3"
209
209
  "ISO/IEC/IEEE/ P24748.3/D-IS-2019": "ISO/IEC/IEEE DIS P24748.3-2019"
210
210
  "P1635/D10/ASHARE 21/D-10-2016": "IEEE P1635/D10-2016"
211
+ # The joint-publisher underscore drafts ("ISO /IEC/IEEE P24774_D1, February
212
+ # 2020") are the slash spellings ("/D1") of rows the ledger already pins
213
+ # as project drafts; the underscore form parsed through the printed-joint
214
+ # family and flipped the classification (pubid#430). Applied after the
215
+ # exact one-off rules above so their targets stay reachable.
216
+ # Dated: the trailing text date rides in the draft clause with the long
217
+ # comma the project-draft route pins ("...D1, February, 2020").
218
+ # Undated.
219
+ # Dated: the trailing text date rides in the draft clause with the long
220
+ # comma the project-draft route pins ("...D1, February, 2020").
221
+ /\b(ISO ?\/ ?IEC ?\/ ?IEEE|IEC ?\/ ?IEEE) (P\d+)_D(\d+), ([A-Z][a-z]+.?) (\d{4})\b/: "\\1 \\2/D\\3, \\4, \\5"
222
+ # Undated.
223
+ /\b(ISO ?\/ ?IEC ?\/ ?IEEE|IEC ?\/ ?IEEE) (P\d+)_D(\d+)\b/: "\\1 \\2/D\\3"
@@ -38,6 +38,8 @@ NIST AI NIST AI 100-2e2023 ipd: NIST AI 100-2e2023 ipd
38
38
  NBS CIRC 154suprev: NBS CIRC 154r1sup
39
39
  NBS CIRC supJun1925-Jun1926: NBS CIRC 24e7sup2
40
40
  NBS CIRC supJun1925-Jun1927: NBS CIRC 24e7sup3
41
+ NBS.CIRC.supJun1925-Jun1926: NBS CIRC 24e7sup2
42
+ NBS.CIRC.supJun1925-Jun1927: NBS CIRC 24e7sup3
41
43
  /^NBS CIRC sup$/: NBS CIRC 24e7sup
42
44
  NBS CIRC 25sup-1924: NBS CIRC 25sup
43
45
  NIST SP 260-126 rev 2013: NIST SP 260-126r2013
@@ -63,6 +65,7 @@ NIST AMS 300-8r1/upd: NIST AMS 300-8r1/Upd1-202102
63
65
  NIST IR 8115r1-upd: NIST IR 8115r1/Upd1-202103
64
66
  NIST IR 8170-upd: NIST IR 8170/Upd1-202003
65
67
  NIST TN 2150-upd: NIST TN 2150/Upd1-202102
68
+ NIST.TN.2150-upd: NIST.TN.2150-upd1-202102
66
69
  NBS IR 73-197r: NBS IR 73-197r1
67
70
  NBS.HB.105-1r1990: NIST.HB.105-1r1990
68
71
  NBS.HB.28p11969: NBS.HB.28-1969pt1
@@ -97,6 +100,7 @@ NIST SP 500-281-v1 0: NIST SP 500-281v1.0
97
100
  NIST SP 800-63v1 0 1: NIST SP 800-63v1.0.1
98
101
  NIST SP 800-63v1 0 2: NIST SP 800-63v1.0.2
99
102
  NIST SP 955 Suppl: NIST SP 955sup
103
+ NIST.SP.955.Suppl: NIST.SP.955-S.uppl
100
104
  NIST SP 984 4: NIST SP 984-4
101
105
  NISTPUB 0413171251: NIST TN 1648-2009
102
106
  /\-draft$/: " (Draft)"
@@ -109,9 +113,9 @@ NIST IR 8413-upd1: NIST IR 8413/Upd1-202207
109
113
  NIST IR 85-3273-37-upd1: NIST IR 85-3273-37/Upd1-202205
110
114
  /NBS CRPL-F-B(?=\d+)/: "NBS CRPL-F-B "
111
115
  "NIST.AMS.300-8r1/upd": NIST.AMS.300-8r1-upd1
112
- NIST.hb.150-1-2017: NIST HB 105-1r2017
113
- nbs.tn.671: NBS TN 671
114
- NIST.TN.1648_2009: NIST TN 1648-2009
116
+ NIST.hb.150-1-2017: NIST.HB.105-1r2017
117
+ nbs.tn.671: NBS.TN.671
118
+ NIST.TN.1648_2009: NIST.TN.1648e2009
115
119
  NBS NSRDS: NSRDS-NBS
116
120
  nist ir: NIST IR
117
121
  /(?<=\d)es/: (spa)
@@ -50,3 +50,5 @@ module Pubid
50
50
  end
51
51
  end
52
52
  end
53
+
54
+ Pubid::Adobe::Builder.prepend(Pubid::Builder::AllPartsWrap)
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pubid"
4
+ module Pubid
5
+ # Every part of one document, e.g. "ISO 9000 (all parts)".
6
+ #
7
+ # The class that includes this module holds part identifiers of one
8
+ # document in `identifiers`, in natural order and without duplicates. A
9
+ # member with no part stands for the whole document; it is permitted only
10
+ # as the sole member.
11
+ #
12
+ # A flavor includes it in a subclass of its own `Identifier`, so the
13
+ # all-parts identifier of a flavor is an identifier of that flavor
14
+ # (`Pubid::Iso::Identifiers::AllParts < Pubid::Iso::Identifier`). The
15
+ # subclass names its own suffix or URN; everything else comes from here.
16
+ # A flavor with no such class uses {Pubid::AllPartsIdentifier}.
17
+ #
18
+ # `==` compares the members. `===` compares the document only: the first
19
+ # member without its part and its edition (see #identity).
20
+ module AllParts
21
+ SUFFIX = " (all parts)"
22
+
23
+ # The members hold the whole identity, so the hash carries nothing else.
24
+ # Without the key_value block the serializer reads every inherited
25
+ # attribute, and the derived #number below would be written too.
26
+ def self.included(base)
27
+ base.attribute :identifiers, ::Pubid::Identifier, polymorphic: true,
28
+ collection: true,
29
+ initialize_empty: true
30
+ declare_key_value(base)
31
+ base.extend(ClassMethods)
32
+ end
33
+
34
+ # lutaml MERGES a subclass block onto the flavor's own block, so the
35
+ # derived #number below would be written as a top-level key. A no-op
36
+ # converter keeps it out.
37
+ def self.declare_key_value(base)
38
+ base.key_value do
39
+ map "_type", to: :_type
40
+ map "identifiers", to: :identifiers
41
+ map "number", with: { to: :number_to_kv, from: :number_from_kv }
42
+ end
43
+ end
44
+
45
+ module ClassMethods
46
+ # lutaml's from_hash builds the object with no attributes and assigns
47
+ # `identifiers` after #initialize, so it normalizes here too.
48
+ def from_hash(data, options = {})
49
+ super.tap do |id|
50
+ id.send(:normalize_identifiers!) if id.is_a?(::Pubid::AllParts)
51
+ end
52
+ end
53
+ end
54
+
55
+ def initialize(attrs = {}, options = {})
56
+ super
57
+ normalize_identifiers!
58
+ end
59
+
60
+ # The members hold the number, so it is neither written nor read.
61
+ def number_to_kv(_model, _doc); end
62
+
63
+ def number_from_kv(_model, _value); end
64
+
65
+ # @param other [Object] a candidate identifier
66
+ # @return [Boolean] true when +other+ is any part of the same document,
67
+ # in any edition
68
+ def ===(other)
69
+ return false unless other.is_a?(::Pubid::Identifier) && identity
70
+
71
+ identity === document_of(other)
72
+ end
73
+
74
+ # Another all-parts identifier with +other+ added. The receiver does not
75
+ # change.
76
+ # @param other [Pubid::Identifier] a part of the same document, or
77
+ # another all-parts identifier of it
78
+ # @return [Pubid::Identifier] an all-parts identifier of the same class
79
+ # @raise [ArgumentError] for a different document, an identifier with no
80
+ # part, or a part that is already a member
81
+ def +(other)
82
+ members = other.is_a?(::Pubid::AllParts) ? other.identifiers : [other]
83
+ members.each { |member| validate_member!(member) }
84
+ self.class.new(identifiers: identifiers + members)
85
+ end
86
+
87
+ def to_s(**opts)
88
+ return "" unless identity
89
+
90
+ annotate_plain_render(
91
+ "#{identity.to_s(**opts.except(:annotated))}#{self.class::SUFFIX}",
92
+ **opts,
93
+ )
94
+ end
95
+
96
+ # The all-parts attributes are empty, so the flavor renderers cannot
97
+ # print it. Only the human form exists yet; `to_mr_string` and `to_slug`
98
+ # come through here and raise too.
99
+ def render(format: :human, **opts)
100
+ return to_s(**opts) if format == :human
101
+
102
+ raise NotImplementedError,
103
+ "an all-parts identifier has no #{format} form yet"
104
+ end
105
+
106
+ def to_urn
107
+ raise NotImplementedError, "an all-parts identifier has no URN yet"
108
+ end
109
+
110
+ def to_all_parts
111
+ self
112
+ end
113
+
114
+ def root
115
+ identifiers.first&.root
116
+ end
117
+
118
+ # The document number, e.g. "9000". The index keys on it.
119
+ def number
120
+ identity&.number
121
+ end
122
+
123
+ # True here and false for every other identifier, so a caller that reads
124
+ # the old flag still works.
125
+ # rubocop:disable Naming/PredicateMethod
126
+ def all_parts
127
+ true
128
+ end
129
+ # rubocop:enable Naming/PredicateMethod
130
+
131
+ def all_parts?
132
+ true
133
+ end
134
+
135
+ def base_document
136
+ identifiers.first&.base_document
137
+ end
138
+
139
+ protected
140
+
141
+ # The document: the first member without its part, its subpart and its
142
+ # edition. #document_of reads it from another all-parts identifier, so it
143
+ # is protected and not private.
144
+ # @return [Pubid::Identifier, nil]
145
+ def identity
146
+ first = identifiers.first
147
+ first && document_of(first)
148
+ end
149
+
150
+ private
151
+
152
+ # The document of +id+: +id+ without its part and its edition.
153
+ def document_of(id)
154
+ return id.identity if id.is_a?(::Pubid::AllParts)
155
+
156
+ id.without_parts(*id.class.all_parts_edition_keys)
157
+ end
158
+
159
+ def validate_member!(id)
160
+ unless id.is_a?(::Pubid::Identifier)
161
+ raise ArgumentError, "not an identifier: #{id.inspect}"
162
+ end
163
+
164
+ raise ArgumentError, "#{id} is not a part of #{self}" unless self === id
165
+ raise ArgumentError, "#{id} has no part" unless id.part?
166
+ return unless identifiers.any? { |member| same_member?(member, id) }
167
+
168
+ raise ArgumentError, "#{id} is already a member of #{self}"
169
+ end
170
+
171
+ def same_member?(one, other)
172
+ one.normalized_copy == other.normalized_copy
173
+ end
174
+
175
+ # Sort, remove duplicates, and drop the whole-document member when a
176
+ # part is present. `==` and `to_hash` depend on this order.
177
+ def normalize_identifiers!
178
+ members = unique_members
179
+ parts = members.select(&:part?)
180
+ self.identifiers = sorted(parts.empty? ? members : parts)
181
+ end
182
+
183
+ def unique_members
184
+ identifiers.each_with_object([]) do |member, acc|
185
+ acc << member unless acc.any? { |kept| same_member?(kept, member) }
186
+ end
187
+ end
188
+
189
+ # One member needs no sort, and #to_s raises on an abstract class.
190
+ def sorted(members)
191
+ return members if members.size < 2
192
+
193
+ members.sort_by { |member| natural_key(member.to_s) }
194
+ end
195
+
196
+ # "ISO 9000-10" sorts after "ISO 9000-2".
197
+ def natural_key(str)
198
+ str.split(/(\d+)/).map { |s| s.match?(/\A\d+\z/) ? [0, s.to_i] : [1, s] }
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pubid"
4
+ module Pubid
5
+ # The all-parts identifier of a flavor that has no class of its own.
6
+ # A flavor that prints "(all parts)" or has a series URN gives its own
7
+ # class instead: see {Pubid::AllParts} and `Identifier.all_parts_class`.
8
+ class AllPartsIdentifier < Identifier
9
+ include AllParts
10
+
11
+ # This class belongs to no flavor, so its name carries no flavor segment
12
+ # and {TypeResolver} resolves it directly.
13
+ def self.polymorphic_name
14
+ return super unless self == ::Pubid::AllPartsIdentifier
15
+
16
+ "pubid:all-parts"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,47 @@
1
+ # AMCA flavor notes
2
+
3
+ AMCA index key and MR slug.
4
+
5
+ These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/amca/` or `spec/pubid/amca/`. The root file keeps the cross-flavor contract that every flavor obeys.
6
+
7
+ ## From the root note "AMCA / ASME / ASTM index key (`root.number`): three flavors, three different shapes"
8
+
9
+ **(1) AMCA — flat `:string` columns on the leaves, and a latent `==` bug fixed with them.** `code` is deleted and each of the three leaves declares `attribute :number, :string`. An earlier draft kept the inherited `Components::Code` instead (AMCA's own `Components::Code` resolved to `Pubid::Components::Code` — there is no `lib/pubid/amca/components/code.rb` — so nothing needed retyping, the CSA case); that works too, and the serialized bytes are identical either way, but the flat string matches the other flavors here and lets the custom `key_value` converters go entirely. **The converters were what hid a real bug.** `year` was declared `Components::Date`, yet the builder assigns a String and lutaml does not cast it — so the **parse** path held a String while **from_hash** produced a `Components::Date`. `to_hash` and `to_s` agreed, because `year_to_kv` normalised both shapes to a scalar, so the relaton index gate (`from_hash(to_hash) == to_hash`) passed — while **`parse(x) == from_hash(parse(x).to_hash)` was false for every AMCA id**, breaking `#matches?`, which is built on `==`. `year` is now `:string` too, both `with:` converters are gone (every mapped attribute is a plain scalar, and the canonical `to_hash` already drops empty and default values), and a spec asserts round-trip **equality**, not just hash equality — the stronger check that would have caught it. **Lesson: a `with:` converter that normalises two possible runtime shapes is a signal that the attribute type is wrong, not a fix.** The serialized key moved `code` → `number` (no consumer). **AMCA's `Publication` and `Interpretation` also lost hand-written keyword `initialize` methods** that assigned ivars directly and bypassed lutaml — costing them their `_type` (so `from_hash` could not route back), their `publisher` default, and the serialization of `revision`/`interpretation_code`, which were `attr_reader`s `to_hash` dropped. Both are now ordinary lutaml attributes; the builder already called `new(**attributes)` exactly as it does for `Standard`, which never had the problem. This is the AIEE/IRE/NESC migration applied again.
10
+
11
+ ## Revision, rendering and URN (hand-off `asme-bpvc-and-amca-residue`)
12
+
13
+ **The publication revision never reached the object.** The grammar captured only `01` of `Rev. 01-23`, and `Builder#build_publication` read it under `parsed[:revision]`, where the grammar did not put it. The grammar now captures the whole `01-23` as `:revision`, so `revision`, `to_hash`, `to_s` and the URN (`rev.01-23`) carry it.
14
+
15
+ **The renderer lost text.** It printed `AMCA Publication 211 -22` (a space before the year), `AMCA 99 – JW` (no `Interp`), and `(2010)` for `(R2010)`. Every parseable AMCA fixture id now renders as it was written, except the real normalizations (`ANSI/AMCA 220-21` → `ANSI/AMCA Standard 220-21`).
16
+
17
+ **Every AMCA URN held a Ruby Hash.** `urn_type` interpolated the `type` metadata Hash (`copub.amca:{key: :publication, ...}`); it now reads `type[:key]`, the source `mr_type` reads. The URN also carries the interpretation code (`interp.jw`), so the interpretations of one standard no longer share a URN. `UrnParser` reads every token back (type, copublisher, interpretation code, revision, reaffirmation), so all 50 parseable fixture ids come back `==` from their URN; before, it rebuilt a bare `AMCA <number>-<year>` Standard.
18
+
19
+ **Not fixed, pre-existing:** `AMCA 204 – 1` and `ANSI/AMCA Standard 210-25 / ASHRAE 51-25` do not parse. `ANSI/AMCA 210-16 /ASHRAE 51-16` parses but silently drops `/ASHRAE 51-16`, because `additional_copublisher` has no capture. The `suffix` rule of `standard_identifier` has no capture either, so `suffix` is never set on a `Standard`.
20
+
21
+ ## `all_parts_edition_keys`: Publication's `revision`, then the shared `reaffirmed`
22
+
23
+ `Identifier.all_parts_edition_keys` defaults to `%i[date year edition
24
+ version]`. A first pass fixed `Identifiers::Publication#revision` (the
25
+ `Rev. 01-23` token), a separate discriminator that list never covered, so
26
+ `"AMCA Publication 211-22 (Rev. 01-23)".to_all_parts` did not collapse
27
+ onto another revision of the same publication under `#to_all_parts`/`#===`.
28
+ Fixed with `Identifiers::Publication.all_parts_edition_keys` (`super +
29
+ %i[revision]`), declared on `Publication` itself rather than the shared
30
+ `Pubid::Amca::Identifier` base — `Standard`/`Interpretation` don't carry
31
+ `revision`.
32
+
33
+ Code review of that same change found a second, wider gap it had missed:
34
+ `reaffirmed` (the `"(R2010)"` year) is declared on the shared
35
+ `Pubid::Amca::Identifier` base (`identifiers/base.rb`), not on
36
+ `Publication`, and `Renderer#render_base` reads it for **every** leaf —
37
+ `Standard` as well as `Publication` — so `"AMCA 210-16 (R2010)"` failed to
38
+ collapse onto `"AMCA 210-16 (R2015)"` even though neither is a
39
+ `Publication`. Fixed on the shared base itself:
40
+ `Pubid::Amca::Identifier.all_parts_edition_keys` is `super +
41
+ %i[reaffirmed]`; `Publication`'s own override chains through `super` and
42
+ picks it up automatically. **Lesson**: a discriminator declared on a
43
+ shared base needs the override on that base, not repeated per leaf — and,
44
+ per the IEEE entry above, finding one missed attribute is not proof the
45
+ audit is complete. Not part of the original hand-off's audit list (which
46
+ didn't cover AMCA); found during code review of the OGC/3GPP/NIST fix.
47
+ Locked by `spec/pubid/all_parts_edition_keys_audit_spec.rb`.
@@ -154,11 +154,7 @@ module Pubid
154
154
 
155
155
  # Extract revision (Rev. 01-23)
156
156
  if parsed[:revision]
157
- revision_data = parsed[:revision]
158
- if revision_data && revision_data[:revision_year]
159
- attributes[:revision] =
160
- extract_value(revision_data[:revision_year])
161
- end
157
+ attributes[:revision] = extract_value(parsed[:revision])
162
158
  end
163
159
 
164
160
  Identifiers::Publication.new(**attributes)
@@ -174,3 +170,5 @@ module Pubid
174
170
  end
175
171
  end
176
172
  end
173
+
174
+ Pubid::Amca::Builder.prepend(Pubid::Builder::AllPartsWrap)
@@ -87,6 +87,16 @@ module Pubid
87
87
  def mr_year
88
88
  year&.to_s
89
89
  end
90
+
91
+ # `reaffirmed` (the "(R2010)" year, read by Renderer#render_base — used
92
+ # for Standard as well as Publication) is a discriminator the default
93
+ # %i[date year edition version] list misses, shared by every leaf that
94
+ # inherits this class. Declared here, not per-leaf, so Standard's own
95
+ # reaffirmations collapse too; Publication's revision (see
96
+ # publication.rb) is layered on top via `super`.
97
+ def self.all_parts_edition_keys
98
+ super + %i[reaffirmed]
99
+ end
90
100
  end
91
101
  end
92
102
  end
@@ -28,6 +28,19 @@ module Pubid
28
28
  def self.type
29
29
  { key: :publication, title: "Publication", short: nil }
30
30
  end
31
+
32
+ # `revision` is Publication's edition/version discriminator and is
33
+ # not in the default %i[date year edition version] list, so
34
+ # #to_all_parts/#=== failed to collapse two revisions of the same
35
+ # publication (e.g. "AMCA Publication 211-22 (Rev. 01-23)" vs
36
+ # "... (Rev. 02-23)"). Declared here, not on the shared
37
+ # Pubid::Amca::Identifier base, because `revision` is
38
+ # Publication-only — Standard and Interpretation don't carry it.
39
+ # `super` also picks up `reaffirmed`, added on the shared base
40
+ # (identifiers/base.rb) since Standard carries that one too.
41
+ def self.all_parts_edition_keys
42
+ super + %i[revision]
43
+ end
31
44
  end
32
45
  end
33
46
  end
@@ -62,7 +62,8 @@ module Pubid
62
62
 
63
63
  # Publication revision (Rev. 01-23)
64
64
  rule(:revision) do
65
- lparen >> str("Rev") >> dot.maybe >> space >> digits.as(:revision_year) >> dash >> digits >> rparen
65
+ lparen >> str("Rev") >> dot.maybe >> space >>
66
+ (digits >> dash >> digits).as(:revision) >> rparen
66
67
  end
67
68
 
68
69
  # Interpretation code (JW, KB, RG, AW, AH, or just a number)
@@ -29,53 +29,42 @@ module Pubid
29
29
  private
30
30
 
31
31
  def render_base(id)
32
- parts = []
33
- parts << id.copublisher if id.copublisher
34
32
  t = id.class.respond_to?(:type) ? id.class.type : nil
35
- if t.is_a?(Hash) && t[:title]
36
- parts << t[:title].to_s
37
- end
38
- parts << id.number.to_s
39
- parts << "-#{id.year}" if id.year
40
-
41
- result = parts.compact.join(" ")
42
-
43
- if id.copublisher&.include?("/") && id.year
44
- type_title = t.is_a?(Hash) ? t[:title].to_s : ""
45
- result = "#{id.copublisher} #{type_title} #{id.number}-#{id.year}"
46
- end
47
-
48
- result += " (#{id.reaffirmed})" if id.reaffirmed
49
-
33
+ title = t[:title].to_s if t.is_a?(Hash) && t[:title]
34
+ result = document(id, title)
35
+ result += " (R#{id.reaffirmed})" if id.reaffirmed
50
36
  result
51
37
  end
52
38
 
39
+ # The revision and the reaffirmation are separate optional groups in
40
+ # the grammar, so either or both can appear.
53
41
  def render_publication(id)
54
- parts = []
55
- parts << id.copublisher if id.copublisher
56
- parts << "Publication"
57
- parts << id.number.to_s
58
- parts << "-#{id.year}" if id.year
59
- parts << " (Rev. #{id.revision})" if id.revision
60
- parts << " (#{id.reaffirmed})" if id.reaffirmed && !id.revision
61
-
62
- parts.join(" ").squeeze(" ")
42
+ result = document(id, "Publication")
43
+ result += " (Rev. #{id.revision})" if id.revision
44
+ result += " (R#{id.reaffirmed})" if id.reaffirmed
45
+ result
63
46
  end
64
47
 
65
48
  def render_interpretation(id)
66
- parts = []
67
- parts << id.copublisher if id.copublisher
68
- parts << id.number.to_s
49
+ result = [id.copublisher, id.number.to_s].compact.join(" ")
69
50
 
70
51
  if id.interpretation_code
71
- parts << "– #{id.interpretation_code}"
52
+ result += " #{id.interpretation_code} Interp"
72
53
  elsif id.year
73
- parts << "-#{id.year}"
54
+ result += " – #{id.year}"
55
+ else
56
+ result += " Interp"
74
57
  end
75
58
 
76
- parts << " #{id.suffix}" if id.suffix
59
+ result += " #{id.suffix}" if id.suffix
60
+ result
61
+ end
77
62
 
78
- parts.join(" ").squeeze(" ")
63
+ # "AMCA Standard 803-02": the year joins the number with a bare dash.
64
+ def document(id, title)
65
+ result = [id.copublisher, title, id.number.to_s].compact.join(" ")
66
+ result += "-#{id.year}" if id.year
67
+ result
79
68
  end
80
69
  end
81
70
  end
@@ -27,9 +27,26 @@ module Pubid
27
27
  "copub.#{identifier.copublisher.to_s.downcase}" if identifier.copublisher
28
28
  end
29
29
 
30
+ # Identity-bearing: "AMCA 99 JW Interp" and "AMCA 99 KB Interp" are
31
+ # different documents.
32
+ def urn_interpretation
33
+ return nil unless identifier.respond_to?(:interpretation_code)
34
+
35
+ code = identifier.interpretation_code
36
+ "interp.#{code.downcase}" if code
37
+ end
38
+
39
+ def urn_revision
40
+ revision = identifier.revision if identifier.respond_to?(:revision)
41
+ "rev.#{revision}" if revision
42
+ end
43
+
44
+ # `type` is a metadata Hash; interpolating it put a Ruby Hash literal
45
+ # into every AMCA URN. The key is the source Identifier#mr_type reads.
30
46
  def urn_type
31
- (identifier.class.respond_to?(:type) ? identifier.class.type : nil)
32
- &.to_s&.downcase
47
+ return nil unless identifier.class.respond_to?(:type)
48
+
49
+ identifier.class.type[:key]&.to_s
33
50
  end
34
51
 
35
52
  def generate
@@ -37,6 +54,8 @@ module Pubid
37
54
  parts << urn_number if urn_number
38
55
  parts << urn_year if urn_year
39
56
  parts << urn_suffix if urn_suffix
57
+ parts << urn_interpretation if urn_interpretation
58
+ parts << urn_revision if urn_revision
40
59
  parts << urn_reaffirmed if urn_reaffirmed
41
60
  parts << urn_copublisher if urn_copublisher
42
61