pubid 2.0.0.pre.alpha.11 → 2.0.0.pre.alpha.12

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/lib/pubid/bsi/components/date.rb +10 -4
  3. data/lib/pubid/bsi/identifiers/british_industrial_practice.rb +1 -1
  4. data/lib/pubid/bsi/identifiers/handbook.rb +1 -1
  5. data/lib/pubid/bsi/identifiers/practice_guide.rb +1 -1
  6. data/lib/pubid/bsi/single_identifier.rb +11 -2
  7. data/lib/pubid/ccsds/identifier.rb +4 -0
  8. data/lib/pubid/ccsds/single_identifier.rb +14 -10
  9. data/lib/pubid/csa/builder.rb +3 -3
  10. data/lib/pubid/csa/identifiers/bundled.rb +2 -2
  11. data/lib/pubid/csa/identifiers/cec.rb +1 -1
  12. data/lib/pubid/csa/single_identifier.rb +1 -1
  13. data/lib/pubid/csa/urn_generator.rb +1 -1
  14. data/lib/pubid/evs/builder.rb +1 -1
  15. data/lib/pubid/evs/identifier.rb +6 -6
  16. data/lib/pubid/evs/identifiers/national_adoption.rb +7 -2
  17. data/lib/pubid/evs/renderer.rb +1 -1
  18. data/lib/pubid/evs/urn_generator.rb +1 -1
  19. data/lib/pubid/evs/urn_parser.rb +1 -1
  20. data/lib/pubid/gb/builder.rb +14 -9
  21. data/lib/pubid/gb/identifier.rb +23 -4
  22. data/lib/pubid/gb/parser.rb +4 -0
  23. data/lib/pubid/gb/renderer.rb +4 -3
  24. data/lib/pubid/gb.rb +1 -0
  25. data/lib/pubid/identifier.rb +11 -3
  26. data/lib/pubid/idf/identifier.rb +7 -0
  27. data/lib/pubid/idf/identifiers/amendment.rb +1 -1
  28. data/lib/pubid/idf/identifiers/corrigendum.rb +1 -1
  29. data/lib/pubid/idf/identifiers/international_standard.rb +1 -1
  30. data/lib/pubid/idf/identifiers/reviewed_method.rb +1 -1
  31. data/lib/pubid/idf/single_identifier.rb +1 -1
  32. data/lib/pubid/ieee/builder.rb +42 -2
  33. data/lib/pubid/ieee/identifiers/si_standard.rb +5 -2
  34. data/lib/pubid/ieee/ire/parser.rb +19 -1
  35. data/lib/pubid/ieee/parser.rb +207 -32
  36. data/lib/pubid/ieee/project_renderer.rb +46 -0
  37. data/lib/pubid/ieee/renderer.rb +6 -7
  38. data/lib/pubid/ieee.rb +2 -0
  39. data/lib/pubid/iso/builder.rb +10 -11
  40. data/lib/pubid/iso/components.rb +0 -1
  41. data/lib/pubid/iso/identifier.rb +11 -4
  42. data/lib/pubid/iso/identifiers/directives.rb +7 -4
  43. data/lib/pubid/iso/identifiers/tc_document.rb +31 -46
  44. data/lib/pubid/iso/normalizer.rb +1 -1
  45. data/lib/pubid/iso/urn_generator.rb +10 -10
  46. data/lib/pubid/iso.rb +5 -4
  47. data/lib/pubid/itu/builder.rb +10 -0
  48. data/lib/pubid/itu/identifiers/base.rb +10 -0
  49. data/lib/pubid/itu/identifiers/contribution.rb +31 -0
  50. data/lib/pubid/itu/identifiers.rb +1 -0
  51. data/lib/pubid/itu/parser.rb +19 -2
  52. data/lib/pubid/nist/builder.rb +12 -12
  53. data/lib/pubid/nist/identifiers/base.rb +10 -3
  54. data/lib/pubid/nist/identifiers/commercial_standard_emergency.rb +1 -1
  55. data/lib/pubid/nist/identifiers/crpl_report.rb +5 -6
  56. data/lib/pubid/nist/identifiers/federal_information_processing_standards.rb +2 -2
  57. data/lib/pubid/nist/identifiers/miscellaneous_publication.rb +2 -2
  58. data/lib/pubid/nist/series/ir.rb +3 -7
  59. data/lib/pubid/oiml/builder.rb +2 -2
  60. data/lib/pubid/oiml/identifiers/bulletin.rb +30 -36
  61. data/lib/pubid/oiml/identifiers/code_number.rb +2 -2
  62. data/lib/pubid/oiml/parser.rb +7 -2
  63. data/lib/pubid/oiml/renderer.rb +3 -3
  64. data/lib/pubid/oiml/single_identifier.rb +1 -1
  65. data/lib/pubid/oiml/urn_generator.rb +4 -4
  66. data/lib/pubid/renderers/directives_renderer.rb +3 -1
  67. data/lib/pubid/sae/builder.rb +1 -1
  68. data/lib/pubid/sae/identifiers/base.rb +4 -1
  69. data/lib/pubid/sae/urn_generator.rb +1 -1
  70. data/lib/pubid/version.rb +1 -1
  71. data/lib/tasks/docs.rake +13 -11
  72. metadata +4 -4
  73. data/lib/pubid/ccsds/identifiers/base_BASE_88929.rb +0 -70
  74. data/lib/pubid/iso/components/code.rb +0 -24
@@ -73,7 +73,7 @@ module Pubid
73
73
  end
74
74
 
75
75
  # Complete IRE identifier - YEAR FIRST format
76
- rule(:ire_identifier) do
76
+ rule(:ire_identifier_year_first) do
77
77
  # Format: "52 IRE 7.S2" or "1952 IRE 7.S2"
78
78
  year.as(:year) >>
79
79
  space >>
@@ -84,6 +84,24 @@ module Pubid
84
84
  (space? >> (dash | str(",")).maybe >> space? >> year_full.as(:full_year)).maybe
85
85
  end
86
86
 
87
+ # Prefix-first format: "IRE 7.S2-1952" (pubid#316 family 2) — the
88
+ # year trails the designation instead of leading the identifier.
89
+ # The renderer always emits the year-first short-year form, so this
90
+ # spelling canonicalizes onto it ("IRE 7.S2-1952" → "52 IRE 7.S2").
91
+ rule(:ire_identifier_prefix_first) do
92
+ ire_prefix >>
93
+ space >>
94
+ (ire_type >> space).maybe >>
95
+ number >>
96
+ date
97
+ end
98
+
99
+ # Year-first is tried first so the original spellings keep their
100
+ # match; prefix-first claims only what year-first cannot.
101
+ rule(:ire_identifier) do
102
+ ire_identifier_year_first | ire_identifier_prefix_first
103
+ end
104
+
87
105
  root(:ire_identifier)
88
106
  end
89
107
  end
@@ -611,19 +611,31 @@ module Pubid
611
611
  # "-YYYY[-MM]" date (instead of ":YYYY"), multi-digit committee-draft
612
612
  # stage codes (CD1..CD4) plus CDV, and a wider set of joint publishers.
613
613
  # (roadmap items 2/3, phase 1). longest publisher token first.
614
- (str("ISO/IEC/IEEE") | str("IEEE/ISO/IEC") | str("IEEE/IEC/ISO") |
615
- str("ISO/IEEE") | str("IEC/IEEE") | str("IEEE/IEC") | str("ISO/IEC") |
616
- str("IEEE")).as(:joint_publishers) >>
617
- space >>
618
- # ISO stage codes: FDIS, FCD, CDV; DIS/CD with an optional round digit
619
- # (DIS2, CD1..CD4); WD/PWI/NP. (FCD before FDIS is fine — distinct.)
620
- (str("FDIS") | str("FCD") | str("CDV") |
621
- (str("DIS") >> digit.maybe) |
622
- (str("CD") >> digit.maybe) |
623
- str("WD") | str("PWI") | str("NP")).as(:iso_stage) >>
624
- # optional " Std" noise word after the stage (e.g. "FDIS Std P15288")
625
- (space >> str("Std")).maybe >>
626
- space >>
614
+ # The stage is OPTIONAL ONLY for ISO-led joint prefixes - the
615
+ # stage-less PUBLISHED form (pubid#317): "ISO/IEC/IEEE 26511:2018",
616
+ # "ISO/IEEE 11073-20101:2004(E)". Every other prefix keeps the
617
+ # stage REQUIREMENT so this rule cannot steal what belongs to
618
+ # iec_ieee_copublished ("IEC/IEEE 60076-2016"), the bare-IEEE
619
+ # project rules ("IEEE P802.16/D-3") or the ISO flavor
620
+ # ("ISO/IEC <n>:<year>").
621
+ iso_led = (str("ISO/IEC/IEEE") | str("IEEE/ISO/IEC") |
622
+ str("IEEE/IEC/ISO") | str("ISO/IEEE"))
623
+ staged_only = (str("IEC/IEEE") | str("IEEE/IEC") |
624
+ str("ISO/IEC") | str("IEEE"))
625
+ # ISO stage codes: FDIS, FCD, CDV; DIS/CD with an optional round
626
+ # digit (DIS2, CD1..CD4); WD/PWI/NP.
627
+ iso_stage = (str("FDIS") | str("FCD") | str("CDV") |
628
+ (str("DIS") >> digit.maybe) |
629
+ (str("CD") >> digit.maybe) |
630
+ str("WD") | str("PWI") | str("NP")).as(:iso_stage)
631
+ std_noise = (space >> str("Std")).maybe
632
+ # The empty-string marker distinguishes the stage-less PUBLISHED
633
+ # tree in the builder (absence of :iso_stage alone means IEEE format).
634
+ ((iso_led.as(:joint_publishers) >> space >>
635
+ (iso_stage >> std_noise >> space |
636
+ str("").as(:iso_published) >> space.maybe)) |
637
+ (staged_only.as(:joint_publishers) >> space >>
638
+ iso_stage >> std_noise >> space)) >>
627
639
  str("P").maybe >> # optional project marker on the number
628
640
  digits.as(:number) >>
629
641
  # part must not swallow the trailing year (year_digits.absent?)
@@ -641,12 +653,33 @@ module Pubid
641
653
  # historical "…/D-3-2017" onto the number as "…-2017/D3", so by the
642
654
  # time this rule runs the draft usually trails the date (bucket 5);
643
655
  # a date-less "/D-4" keeps its hyphen (bucket 7), hence dash.maybe.
656
+ # A text date may trail the DRAFT itself (pubid#216:
657
+ # "CD P26515/D1, March 2017", "FDIS P15289/D3, 2017") — distinct
658
+ # keys (:draft_month/:draft_year), because a plain :month/:year
659
+ # here collides with the date clause above and parslet drops the
660
+ # subtree with a "Duplicate subtrees" warning.
644
661
  (slash >> str("D") >> dash.maybe >>
645
- match('[0-9.]').repeat(1).as(:draft_version)).maybe >>
662
+ match('[0-9.]').repeat(1).as(:draft_version) >>
663
+ (((comma | space) >> month_name.as(:draft_month) >> space >>
664
+ year_digits.as(:draft_year)) |
665
+ (comma >> year_digits.as(:draft_year))).maybe).maybe >>
666
+ # Optional amendment tail (pubid#317:
667
+ # "8802-11:2012/Amd.1:2014(E)") - the flat tree keys reuse
668
+ # build_flat_amendment.
669
+ (slash >> str("Amd") >> (dot | space).maybe >>
670
+ digits.as(:amd_number) >>
671
+ ((str(":") | dash) >> year_digits.as(:amd_year)).maybe).maybe >>
646
672
  # Optional edition, from relaton's "/E-<n>" suffix normalized to
647
673
  # "Edition <n>.0[ YYYY]" (nil-residue hand-off item 1).
648
674
  edition.maybe >>
649
- revision_suffix.maybe
675
+ revision_suffix.maybe >>
676
+ # Published joint docs print a language marker, after either the
677
+ # year or the amendment tail: "9945:2009(E)", "...Amd.1:2014(E)".
678
+ # The crawl sometimes spaces it ("…(E), January 2017" family,
679
+ # pubid#216), and a relationship parenthetical may follow
680
+ # ("…CD2 P15288-2013-09 (Revision of …)").
681
+ space.maybe >> (str("(E)") | str("(F)")).maybe >>
682
+ parenthetical.maybe
650
683
  end
651
684
 
652
685
  # Embedded (stage-LAST) ISO-led designations: the corpus writes the ISO
@@ -701,9 +734,13 @@ module Pubid
701
734
  end
702
735
 
703
736
  # IEEE P pattern (without Std): "IEEE P1003.1..." OR just "P1003.1..." (prefix optional)
737
+ # `P` is not consumed here: the `number` rule carries its own optional
738
+ # leading P and captures it into :number ("P1003.1"), so Code.parse
739
+ # peels it as code.prefix and the project marker survives the parse —
740
+ # a bare `str("P")` swallowed it and "P1201/D0.3" came back without
741
+ # its P at all (pubid#18's data-loss complaint).
704
742
  rule(:ieee_p_identifier) do
705
743
  (str("IEEE").as(:publisher) >> space).maybe >> # Make IEEE prefix optional
706
- str("P") >> space.maybe >> # Make space after P optional
707
744
  number >>
708
745
  (part_subpart_year | edition).maybe >>
709
746
  # Pattern for /08 style drafts (digits without D prefix) - MUST come before corrigendum
@@ -759,15 +796,17 @@ module Pubid
759
796
  # status is captured (draft_status ends in a space, so "Approved Draft"
760
797
  # splits cleanly) and round-trips as the draft_status attribute; the literal
761
798
  # "Draft" stays a bare marker (dropped on render, like the plain form).
799
+ # `P` is not consumed here: the `number` rule carries its own optional
800
+ # leading P and captures it into :number ("P802.11"), so Code.parse peels
801
+ # it as code.prefix and the project marker round-trips (pubid#318) — a
802
+ # bare `str("P").maybe` here swallowed it and the render lost the P.
762
803
  rule(:ieee_draft_p_identifier) do
763
804
  (str("IEEE").as(:publisher) >> space).maybe >> # Make IEEE prefix optional
764
805
  draft_status.as(:draft_status).maybe >>
765
806
  str("Draft") >> space >>
766
- # `P` is optional — a status-word draft may carry a bare number
767
- # ("IEEE Unapproved Draft 802.1ah/D4.2"), mirroring
768
- # ieee_approved_draft_identifier's str("P").maybe. The `number` rule
807
+ # A status-word draft may carry a bare number
808
+ # ("IEEE Unapproved Draft 802.1ah/D4.2"). The `number` rule
769
809
  # already accepts the bare forms (802.1ah, C57.15, 11073-10471).
770
- str("P").maybe >>
771
810
  number >>
772
811
  (part_subpart_year | edition).maybe >>
773
812
  # Trailing "Month YYYY" date under distinct keys so it never collides
@@ -836,6 +875,11 @@ module Pubid
836
875
  # Year-first pattern: "52 IRE 7.S2" or "60 IRE 28 PS7"
837
876
  ((match("[1-6]") >> digit >> space >> str("IRE")) | # 2-digit year format
838
877
  (str("19") >> digit.repeat(2, 2) >> space >> str("IRE"))) |
878
+ # Prefix-first pattern: "IRE 7.S2-1952" (pubid#316 family 2) —
879
+ # IRE then a designation (digits or a type word). Loose on
880
+ # purpose: the sub-parser is strict, and a failed delegation just
881
+ # falls through to the remaining alternatives.
882
+ (str("IRE") >> space >> (digits | str("Standard") | str("Std") | str("Trans"))) |
839
883
  # IEEE-IRE transitional pattern
840
884
  (str("IEEE-IRE") >> space)
841
885
  ).present? >>
@@ -861,23 +905,47 @@ module Pubid
861
905
  Nesc::Parser.new.nesc_identifier.as(:nesc)
862
906
  end
863
907
 
908
+ # Draft notation for PSI (e.g., /D2, /D3, rawbib's /D-2)
909
+ rule(:psi_draft) do
910
+ slash >> str("D") >> dash.maybe >> digits.as(:draft_version)
911
+ end
912
+
913
+ # Date on an SI/PSI identifier: ", Month Year" or "-YEAR[-MM]". The
914
+ # comma branch also accepts a NUMERIC month and a DASH before the
915
+ # year — the renderer emits ", 05 2010" for a numeric-month draft,
916
+ # and the preprocessing gsub rewrites a trailing " <digits> <year>"
917
+ # to "<digits>-<year>" (the same load-bearing coupling draft_date
918
+ # documents), so the re-parse must read its own rendering back.
919
+ rule(:psi_date) do
920
+ (comma >> (month_name | month_numeric).as(:month) >> (space | dash) >>
921
+ year_digits.as(:year)) |
922
+ (dash >> year_digits.as(:year) >>
923
+ (dash >> digit.repeat(2, 2).as(:month)).maybe)
924
+ end
925
+
864
926
  # IEEE/ASTM SI/PSI (Système International) patterns
865
927
  # SI = Published metric system standard
866
928
  # PSI = Proposed SI (draft)
929
+ # Rawbib spellings (pubid#316 family 3): a bare "IEEE" publisher, a dot
930
+ # separator after the type ("PSI.10", "SI 10.1997"), relaton's
931
+ # hyphenated "/D-<n>" draft, and a dash year-month ("-2010-05").
867
932
  rule(:ieee_astm_si_psi) do
868
- str("IEEE/ASTM").as(:publishers) >>
933
+ (str("IEEE/ASTM") | str("IEEE")).as(:publishers) >>
869
934
  space >>
870
935
  (str("PSI") | str("SI")).as(:si_type) >>
871
- space >>
936
+ (space | str(".")) >>
872
937
  digits.as(:number) >>
873
- # Draft notation for PSI (e.g., /D2, /D3)
874
- (slash >> str("D") >> digits.as(:draft_version)).maybe >>
875
- # Year with optional month
938
+ # Glued ".YEAR" edition date ("IEEE/ASTM SI 10.1997")
939
+ (str(".") >> year_digits.as(:year)).maybe >>
940
+ # The date and the draft appear in EITHER order: the legacy
941
+ # spelling puts the draft first ("PSI 10/D2, October 2015"),
942
+ # while normalize_relaton_suffixes repositions the rawbib
943
+ # hyphenated form onto the number ("PSI 10/D-3-2010" →
944
+ # "PSI 10-2010/D3"). Draft-first is tried first so the legacy
945
+ # comma-date keeps its original match; each side is optional so
946
+ # a date-only or draft-less form still parses.
876
947
  (
877
- # Format: ", Month Year"
878
- (comma >> month_name.as(:month) >> space >> year_digits.as(:year)) |
879
- # Format: "-YEAR"
880
- (dash >> year_digits.as(:year))
948
+ (psi_draft >> psi_date.maybe) | (psi_date >> psi_draft.maybe)
881
949
  ).maybe >>
882
950
  # Optional parenthetical (revision relationships)
883
951
  parenthetical.maybe
@@ -1159,12 +1227,15 @@ module Pubid
1159
1227
  # A "\d+" right after "Rev" both selects the numbered subset and keeps
1160
1228
  # these off the English word "Revision". Three source positions:
1161
1229
  # after a draft : "PC37.30.2/D043 Rev 18" -> ".../D043/R-18"
1230
+ # The draft captures accept relaton's hyphenated "/D-<n>" spelling
1231
+ # (pubid#316): without the "-?" the before-draft regex matched only
1232
+ # "/D" of "/D-3" and emitted the garbage "/D/R-2-3-2008-02".
1162
1233
  cleaned = cleaned.sub(
1163
- %r{(/D[0-9A-Za-z.]*)\s+[Rr][Ee][Vv]\s*(\d+)}, '\1/R-\2'
1234
+ %r{(/D-?[0-9A-Za-z.]+)\s+[Rr][Ee][Vv]\s*(\d+)}, '\1/R-\2'
1164
1235
  )
1165
1236
  # before a draft: "P802.16Rev2/D3" -> "P802.16/D3/R-2"
1166
1237
  cleaned = cleaned.sub(
1167
- %r{[-/_.]?\s?[Rr][Ee][Vv][-\s]?(\d+)(/D[0-9A-Za-z.]*)}, '\2/R-\1'
1238
+ %r{[-/_.]?\s?[Rr][Ee][Vv][-\s]?(\d+)(/D-?[0-9A-Za-z.]+)}, '\2/R-\1'
1168
1239
  )
1169
1240
  # no draft, trailing: "P1722-rev1" -> "P1722/R-1"
1170
1241
  cleaned = cleaned.sub(
@@ -1176,7 +1247,7 @@ module Pubid
1176
1247
  # became "/R-<n>", so these regexes only see the lettered residue.
1177
1248
  # Revision token that PRECEDES a draft: drop it (keep the /D…).
1178
1249
  cleaned = cleaned.sub(
1179
- %r{[-/_.]?\s?[Rr][Ee][Vv][-\s]?[A-Za-z0-9]+(?=/D[0-9])},
1250
+ %r{[-/_.]?\s?[Rr][Ee][Vv][-\s]?[A-Za-z0-9]+(?=/D-?[0-9A-Za-z])},
1180
1251
  "",
1181
1252
  )
1182
1253
  # Trailing revision glued to the number with no draft ("P802.11REVmb");
@@ -1185,6 +1256,105 @@ module Pubid
1185
1256
  cleaned.sub(%r{(\d)[Rr][Ee][Vv][A-Za-z0-9]+\s*\z}, '\1')
1186
1257
  end
1187
1258
 
1259
+ # Rewrite the mechanical spellings of the joint ISO-stage forms that no
1260
+ # grammar branch reaches (pubid#216 residuals). Two safety rules keep
1261
+ # this from stealing inputs other rules already parse:
1262
+ # 1. only BROKEN separators fire — underscore/space glue between
1263
+ # number, draft and stage, a slash-SPACE before the stage, a
1264
+ # dash/underscore month after it. A plain "/FDIS" tail is
1265
+ # natively accepted by ieee_p_identifier (fdraft) and the
1266
+ # stage-LAST embedded rule, so it is never rewritten.
1267
+ # 2. the number carries at most ONE part — joint_development_iso_format
1268
+ # reads a single optional part, while ieee_p_identifier takes
1269
+ # multi-part numbers ("P62271-37-013"); a rewrite that moved a
1270
+ # multi-part number onto the joint rule would break it.
1271
+ def self.normalize_joint_stage_spellings(cleaned)
1272
+ # The separator (slash-optional-space or a space) is INSIDE the
1273
+ # capture so a rewrite re-emits it — an uncaptured separator was
1274
+ # silently eaten, gluing the publisher to the stage.
1275
+ pubs = %r{((?:ISO/IEC/IEEE|IEEE/ISO/IEC|IEEE/IEC/ISO|ISO/IEEE|IEC/IEEE|IEEE/IEC|ISO/IEC|IEEE)(?:/ ?| ))}
1276
+ stage = /(FDIS|FCD|CDV|DIS\d?|CD\d?|WD|PWI|NP)/
1277
+ num = /(P?\d+(?:[.-]\d+)?)/
1278
+ # What the joint grammar can finish reading AFTER the stage: the end
1279
+ # of the string, a (comma-)month-year date, or a dash-year[-month].
1280
+ # Captured and re-emitted so a rewrite never drops the date.
1281
+ tail = /(,? (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec)[a-z]* \d{4}|\z|-\d{4}(?:-\d\d)?)/
1282
+
1283
+ # Draft glued to the number with an underscore: "P15289_D3" →
1284
+ # "P15289/D3" (a digit must precede the underscore; "_FDIS" is the
1285
+ # stage, not a draft, and is already slash-rewritten earlier).
1286
+ cleaned = cleaned.gsub(%r{(\d)_D(\d)}, '\1/D\2')
1287
+
1288
+ # Crawl typo: a comma with no space before a month (",March 2021").
1289
+ # In these identifiers a comma is always a date separator, so
1290
+ # spacing it is safe.
1291
+ cleaned = cleaned.sub(/,(?=[A-Za-z])/, ", ")
1292
+
1293
+ # Stage AFTER the draft, both glued with underscores (the early
1294
+ # stage gsub already turned "_CD" into "/CD"):
1295
+ # "…P24748-3/D3/FDIS, April 2020 (E)" → "…FDIS P24748-3/D3, …"
1296
+ cleaned = cleaned.sub(
1297
+ %r{\A#{pubs}#{num}/(D[\d.]+)/ ?#{stage}\b},
1298
+ '\1\4 \2/\3',
1299
+ )
1300
+
1301
+ # Slash-SPACE before the stage ("…P16085/ FDIS, August 2020" —
1302
+ # ieee_p's fdraft has no inner space, so this spelling has no other
1303
+ # home): → "…FDIS P16085, August 2020"
1304
+ cleaned = cleaned.sub(
1305
+ %r{\A#{pubs}#{num}/ #{stage}#{tail}},
1306
+ '\1\3 \2\4',
1307
+ )
1308
+
1309
+ # A space-year after a slash-stage ("…P26511.2/FDIS 2018" — no rule
1310
+ # reads "/FDIS 2018"): → "…FDIS P26511.2-2018" (dash-year clause).
1311
+ cleaned = cleaned.sub(
1312
+ %r{\A#{pubs}#{num}/#{stage} (\d{4})\b},
1313
+ '\1\3 \2-\4',
1314
+ )
1315
+
1316
+ # Stage separated from the number by a SPACE ("IEC/IEEE P63113 CD4,
1317
+ # April 2019"): → "IEC/IEEE CD4 P63113, April 2019". The tail guard
1318
+ # keeps "…FDIS March 2019" (space-month-year, readable after the
1319
+ # swap) while leaving prose suffixes alone.
1320
+ cleaned = cleaned.sub(
1321
+ %r{\A#{pubs}#{num} #{stage}#{tail}},
1322
+ '\1\3 \2\4',
1323
+ )
1324
+
1325
+ # The same space-stage with a space-DRAFT chained after it
1326
+ # ("IEC/IEEE P60980-344 CDV D1, June 2019"): → stage-first with a
1327
+ # slash draft ("…CDV P60980-344/D1, June 2019").
1328
+ cleaned = cleaned.sub(
1329
+ %r{\A#{pubs}#{num} #{stage} (D\d+)\b},
1330
+ '\1\3 \2/\4',
1331
+ )
1332
+
1333
+ # Slash-stage carrying a dash-date ("…P15288/CD2-2013-09 …" — the
1334
+ # dash-year tail is what the joint grammar reads after the number, so
1335
+ # this cannot steal the plain "/FDIS, June 2021" spellings
1336
+ # ieee_p_identifier finishes): → "…CD2 P15288-2013-09 …"
1337
+ cleaned = cleaned.sub(
1338
+ %r{\A#{pubs}#{num}/ ?#{stage}(-\d{4}(?:-\d\d)?)},
1339
+ '\1\3 \2\4',
1340
+ )
1341
+
1342
+ # Draft separated from a stage-first number by a SPACE:
1343
+ # "…DIS P11073-10418 D13, January 2011" → "…DIS P11073-10418/D13, …"
1344
+ cleaned = cleaned.sub(
1345
+ %r{\A#{pubs}#{stage} #{num}/? ?(D\d+)\b},
1346
+ '\1\2 \3/\4',
1347
+ )
1348
+
1349
+ # NOT rewritten: a month glued to the stage with a dash/underscore
1350
+ # ("…/FDIS_Dec 2012", "…/FDIS-Dec 2015"). Even routed onto the joint
1351
+ # grammar, the joint render of a month+draft is itself not yet
1352
+ # idempotent, and the bare-IEEE fdraft path drops the stage and the
1353
+ # date from to_hash — rewriting would trade a visible parse failure
1354
+ # for silent garbage. Deferred with the pubid#216 semantic residue.
1355
+ cleaned
1356
+ end
1357
+
1188
1358
  def self.parse(string)
1189
1359
  # Strip .pdf extension if present (Pattern 3: File Extensions)
1190
1360
  cleaned = string.sub(/\.pdf$/i, "")
@@ -1265,6 +1435,11 @@ module Pubid
1265
1435
  /\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec)(\d{4})\b/, '\1 \2'
1266
1436
  )
1267
1437
 
1438
+ # Rewrite the joint ISO-stage glued spellings (pubid#216) AFTER the
1439
+ # month unglue above, so "Dec2015" is already spaced when the
1440
+ # dash-month rewrite looks for it.
1441
+ cleaned = normalize_joint_stage_spellings(cleaned)
1442
+
1268
1443
  # NEW: Convert IEC/IEEE space-separated to semicolon format
1269
1444
  # Pattern: "IEC 61523-3 First edition 2004-09; IEEE 1497" → already semicolon
1270
1445
  # Pattern: "IEC 62539 First Edition 2007-07 IEEE 930" → needs semicolon
@@ -0,0 +1,46 @@
1
+ # frozen_string: true
2
+
3
+ module Pubid
4
+ module Ieee
5
+ # Renders the IEEE PROJECT DESIGNATOR — the short form IEEE itself uses
6
+ # for draft documents ("P1201/D0.3", "P10000/D1.2"): the project marker,
7
+ # the number with its parts, and the draft. No publisher, no type word,
8
+ # no date (pubid#18: metanorma-ieee builds these for drafts and had to
9
+ # post-process the full rendering).
10
+ class ProjectRenderer
11
+ def initialize(id)
12
+ @id = id
13
+ end
14
+
15
+ def render(context: nil, **_opts)
16
+ id = @id
17
+ body = if id.respond_to?(:code_obj) && id.code_obj
18
+ id.code_obj.to_s
19
+ else
20
+ id.number.to_s
21
+ end
22
+ body = "P#{body}" if project?(id) && !body.start_with?("P")
23
+
24
+ draft = id.draft_obj if id.respond_to?(:draft_obj)
25
+ body += "/D#{draft.version}" if draft&.version
26
+
27
+ body
28
+ end
29
+
30
+ private
31
+
32
+ # A project identifier carries its marker in one of three places
33
+ # depending on which grammar path built it: the code prefix (the bare
34
+ # "P1201/D0.3" spelling), the "P" type code ("IEEE P802.16/D-3"), or a
35
+ # typed stage with project_status (the D1-D6 draft stages).
36
+ def project?(id)
37
+ return true if id.code_obj&.prefix.to_s == "P"
38
+ return true if id.type.to_s == "P"
39
+
40
+ id.respond_to?(:typed_stage) &&
41
+ id.typed_stage&.respond_to?(:project_status) &&
42
+ id.typed_stage.project_status
43
+ end
44
+ end
45
+ end
46
+ end
@@ -115,17 +115,16 @@ module Pubid
115
115
  parts << id.draft_status if id.draft_status
116
116
 
117
117
  # Type - only render for IEEE/AIEE publishers, and only for non-projects.
118
- # An unapproved draft is not yet a standard, so per IEEE guidance the
119
- # "Std" token is dropped from the type when draft_status is "Unapproved"
120
- # (e.g. "Draft Std" -> "Draft", "Std" -> "").
118
+ # A status word ("Unapproved") already marks the document as a draft,
119
+ # so the WHOLE type word is dropped — not just "Std" (pubid#318: the
120
+ # half-strip left "Draft" in the render, the re-parse dropped it, and
121
+ # to_s needed three rounds to converge while losing the type).
121
122
  should_render_type = id.publisher&.match?(/^(IEEE|AIEE)/)
122
123
 
123
- if should_render_type && !id.typed_stage&.project_status && id.type && !id.type.to_s.strip.empty? && id.type != "P"
124
+ if should_render_type && !id.typed_stage&.project_status && id.type && !id.type.to_s.strip.empty? && id.type != "P" &&
125
+ !id.draft_status.to_s.match?(/unapproved/i)
124
126
  type_str = id.type.dup
125
127
  type_str = type_str.sub(/^P/, "") if type_str.start_with?("P")
126
- if id.draft_status.to_s.match?(/unapproved/i)
127
- type_str = type_str.gsub(/\bStd\b/i, "").squeeze(" ").strip
128
- end
129
128
  parts << type_str unless type_str.strip.empty?
130
129
  end
131
130
 
data/lib/pubid/ieee.rb CHANGED
@@ -20,6 +20,7 @@ module Pubid
20
20
  autoload :Parser, "#{__dir__}/ieee/parser"
21
21
  autoload :PreParser, "#{__dir__}/ieee/pre_parser"
22
22
  autoload :Compaction, "#{__dir__}/ieee/compaction"
23
+ autoload :ProjectRenderer, "#{__dir__}/ieee/project_renderer"
23
24
  autoload :Renderer, "#{__dir__}/ieee/renderer"
24
25
  autoload :TypedStages, "#{__dir__}/ieee/typed_stages"
25
26
  autoload :UrnGenerator, "#{__dir__}/ieee/urn_generator"
@@ -162,3 +163,4 @@ Pubid::Registry.register(:ieee, Pubid::Ieee)
162
163
  # Per-flavor format registry: inherits global formats, overrides :human
163
164
  Pubid::Ieee::Identifier.format_registry = Pubid::FormatRegistry.new(parent: Pubid::Identifier.format_registry)
164
165
  Pubid::Ieee::Identifier.format_registry.register(:human, renderer: Pubid::Ieee::Renderer)
166
+ Pubid::Ieee::Identifier.format_registry.register(:project, renderer: Pubid::Ieee::ProjectRenderer)
@@ -143,7 +143,7 @@ module Pubid
143
143
  return nil unless year_value.between?(1900, 2099)
144
144
 
145
145
  {
146
- number: code_class.new(value: number),
146
+ number: number,
147
147
  date: ::Pubid::Components::Date.new(year: part),
148
148
  }
149
149
  end
@@ -197,7 +197,7 @@ module Pubid
197
197
  # or "105/F" ('F' is part)
198
198
  # or "5843/6" ('6' is part)
199
199
  # LEGACY: "4037-1979" (number-year, year should become date)
200
- parse_number_with_part(value, code_class: Pubid::Iso::Components::Code)
200
+ parse_number_with_part(value)
201
201
 
202
202
  when :directives_type
203
203
  # nothing to do here, just return nil
@@ -240,8 +240,10 @@ module Pubid
240
240
  original_text = value.to_s
241
241
  # Extract just the digit(s) for the number field
242
242
  number_string = original_text.match(/\d+/)&.to_s
243
- number_code = number_string ? Pubid::Iso::Components::Code.new(value: number_string) : nil
244
- Pubid::Components::Edition.new(number: number_code,
243
+ # A plain string: Components::Edition#number is typed Value, so a
244
+ # component here would leak a live Ruby object into to_hash (and a
245
+ # !ruby/object tag into to_yaml).
246
+ Pubid::Components::Edition.new(number: number_string,
245
247
  original_text: original_text)
246
248
 
247
249
  when :languages
@@ -260,8 +262,7 @@ module Pubid
260
262
 
261
263
  when :subgroup
262
264
  # Handle JTC 1 subgroup in directives (ISO/IEC JTC 1 DIR)
263
- # Store as a component for potential use in rendering
264
- Pubid::Iso::Components::Code.new(value: value.to_s)
265
+ value.to_s
265
266
 
266
267
  when :supplements
267
268
  # Handle bundled supplements (+ operator)
@@ -274,12 +275,10 @@ module Pubid
274
275
 
275
276
  # TC Document attributes
276
277
  when :tc_type, :sc_type, :wg_type
277
- # TC, SC, WG types are code components
278
- Pubid::Iso::Components::Code.new(value: value.to_s)
278
+ value.to_s
279
279
 
280
280
  when :tc_number, :sc_number, :wg_number
281
- # TC, SC, WG numbers are code components
282
- Pubid::Iso::Components::Code.new(value: value.to_s)
281
+ value.to_s
283
282
 
284
283
  when :year
285
284
  # For TC documents with year, convert to Date
@@ -290,7 +289,7 @@ module Pubid
290
289
  # For regular identifiers, this is handled in :number_with_part
291
290
  if value.is_a?(Parslet::Slice) || value.is_a?(String) ||
292
291
  value.is_a?(Integer)
293
- Pubid::Iso::Components::Code.new(value: value.to_s)
292
+ value.to_s
294
293
  else
295
294
  value
296
295
  end
@@ -3,7 +3,6 @@
3
3
  module Pubid
4
4
  module Iso
5
5
  module Components
6
- autoload :Code, "#{__dir__}/components/code"
7
6
  autoload :Publisher, "#{__dir__}/components/publisher"
8
7
  end
9
8
  end
@@ -56,9 +56,14 @@ module Pubid
56
56
  ts ? dup.tap { |id| id.typed_stage = ts } : dup
57
57
  end
58
58
 
59
- attribute :number, ::Pubid::Iso::Components::Code
60
- attribute :part, ::Pubid::Iso::Components::Code
61
- attribute :subpart, ::Pubid::Iso::Components::Code
59
+ # Plain strings. Iso::Components::Code existed to join `parts` with
60
+ # "-", a composition no ISO identifier ever reached: `parts` is never
61
+ # written in lib/pubid/iso and no Code in the 7,613-id corpus carries
62
+ # anything but `value`. ISO part and subpart are sibling attributes, not
63
+ # fields of the number.
64
+ attribute :number, :string
65
+ attribute :part, :string
66
+ attribute :subpart, :string
62
67
 
63
68
  # Polymorphic type map for lutaml::Model key_value serialization
64
69
  # Maps polymorphic_name → class name for deserialization
@@ -213,8 +218,10 @@ module Pubid
213
218
  doc.add_child(Lutaml::KeyValue::DataModel::Element.new(key, v.to_s))
214
219
  end
215
220
 
221
+ # number/part/subpart are plain strings; the converter name is kept
222
+ # so the key_value map below does not have to change.
216
223
  def build_code(value)
217
- ::Pubid::Iso::Components::Code.new(value: value.to_s)
224
+ value.to_s
218
225
  end
219
226
 
220
227
  # --- date serialized flat as year/month/day ---
@@ -4,7 +4,10 @@ module Pubid
4
4
  module Iso
5
5
  module Identifiers
6
6
  class Directives < SingleIdentifier
7
- attribute :subgroup, ::Pubid::Components::Code
7
+ # A plain string ("JTC 1"). The serialized form flattens from
8
+ # `{"value" => "JTC 1"}` to `"JTC 1"`, so the 5 directives rows of
9
+ # relaton-data-iso need a re-crawl; no compatibility shim is kept.
10
+ attribute :subgroup, :string
8
11
 
9
12
  # Merges with Identifier's block (base attributes); only adds the
10
13
  # Directives-specific subgroup so it survives to_hash/from_hash.
@@ -46,15 +49,15 @@ module Pubid
46
49
  parts << ([publisher] + copubs).map(&:body).map(&:downcase).join("-")
47
50
 
48
51
  if subgroup
49
- subgroup_parts = subgroup.value.split
52
+ subgroup_parts = subgroup.split
50
53
  parts << subgroup_parts[0].downcase if subgroup_parts[0]
51
54
  parts << subgroup_parts[1] if subgroup_parts[1]
52
55
  end
53
56
 
54
57
  parts << "dir"
55
58
 
56
- parts << number.value if number
57
- parts << part.value.downcase if part
59
+ parts << number.to_s if number
60
+ parts << part.to_s.downcase if part
58
61
  parts << date.year if date
59
62
 
60
63
  parts.join(":")