pubid 2.0.0.pre.alpha.27 → 2.0.0.pre.alpha.29

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.
@@ -38,9 +38,12 @@ module Pubid
38
38
 
39
39
  # Extract the long-form date separator ("," or ".") for rendering.
40
40
  # Named `date_separator` on the identifier to avoid colliding with the
41
- # CodeNumber code-part `separator`.
41
+ # CodeNumber code-part `separator`. The dash-glued form ("-1928-05")
42
+ # carries no separator capture (its tail binds as the year/month
43
+ # clause itself), so a month without a separator capture IS the
44
+ # dash form.
42
45
  separator_str = extract_value(parsed[:separator])
43
- attributes[:date_separator] = separator_str if separator_str
46
+ attributes[:date_separator] = separator_str || "-" if separator_str || month_str
44
47
 
45
48
  # Set original format based on parsed data
46
49
  attributes[:original_format] = if separator_str || month_str
@@ -59,20 +59,25 @@ module Pubid
59
59
  format ||= month || date_separator ? "long" : "short"
60
60
 
61
61
  if year
62
- case format
63
- when "short", :short
64
- base += "-#{year}"
65
- when "long", :long
66
- sep = date_separator || "," # Default to comma for long form
67
- base += "#{sep} #{"#{month} " if month}#{year}"
62
+ if date_separator == "-" && month
63
+ # The dash-glued numeric form renders whole ("No 15-1928-05")
64
+ base += "-#{year}-#{month}"
68
65
  else
69
- base += if date_separator
70
- "#{date_separator} #{"#{month} " if month}#{year}"
71
- elsif month
72
- ", #{month} #{year}"
73
- else
74
- "-#{year}"
75
- end
66
+ case format
67
+ when "short", :short
68
+ base += "-#{year}"
69
+ when "long", :long
70
+ sep = date_separator || "," # Default to comma for long form
71
+ base += "#{sep} #{"#{month} " if month}#{year}"
72
+ else
73
+ base += if date_separator
74
+ "#{date_separator} #{"#{month} " if month}#{year}"
75
+ elsif month
76
+ ", #{month} #{year}"
77
+ else
78
+ "-#{year}"
79
+ end
80
+ end
76
81
  end
77
82
  end
78
83
 
@@ -511,8 +511,15 @@ module Pubid
511
511
  # @param parsed_hash [Hash] parsed data with base and supplement info
512
512
  # @return [Identifiers::Corrigendum] corrigendum identifier
513
513
  def build_corrigendum_supplement(parsed_hash)
514
+ # The `corrigendum_identifier` rule's :base subtree binds as a
515
+ # top-level sequence (an Array of capture hashes — the same shape
516
+ # #build's dispatch merges at line 24) whenever the base carries a
517
+ # multi-capture narrative tail; fold it before reconstruction or
518
+ # reconstruct_base_string's Hash indexing crashes with TypeError.
519
+ base_data = parsed_hash[:base]
520
+ base_data = merge_parsed_array(base_data) if base_data.is_a?(Array)
514
521
  # Recursively parse the base identifier reconstructed from the subtree.
515
- base = Identifier.parse(reconstruct_base_string(parsed_hash[:base]))
522
+ base = Identifier.parse(reconstruct_base_string(base_data))
516
523
 
517
524
  # Extract corrigendum attributes (parse-tree keys are cor_*; the
518
525
  # identifier stores them as the uniform `number`/`year`).
@@ -771,6 +778,11 @@ module Pubid
771
778
  def build_joint_development(parsed)
772
779
  attributes = {}
773
780
 
781
+ # The status-draft spelling ("IEEE Unapproved P15288/DFDIS") keeps
782
+ # its status word on the designator face (pubid#318: the word marks
783
+ # the draft).
784
+ attributes[:draft_status] = extract_value(parsed[:draft_status]) if parsed[:draft_status]
785
+
774
786
  # Extract publishers from joint_publishers
775
787
  if parsed[:joint_publishers]
776
788
  joint_pub_str = extract_value(parsed[:joint_publishers])
@@ -788,6 +800,17 @@ module Pubid
788
800
  # date ("P42010/D-4-2019") rides the code as a year-part
789
801
  # ("P42010.2019/D4") — the face the relaton fixtures pin — while
790
802
  # stage-first joints keep the top-level year ("FDIS P15289:2017").
803
+ # A part whose whole value is an ISO/IEC stage token is the
804
+ # stage-last spelling ("29119.FDIS"), not a part: promote it to
805
+ # the stage slot (the #311 embedded rule covers "29119.4.FDIS";
806
+ # this covers the part-less form the shared part capture claims
807
+ # first).
808
+ if parsed[:iso_stage].nil? && parsed[:part] &&
809
+ extract_value(parsed[:part]).to_s.match?(/\A[._-]?(FDIS|FCD|CDV|DIS|CD\d?|WD|PWI|NP)\z/)
810
+ # the stage is the bare token; the capture carries its separator
811
+ parsed[:iso_stage] = extract_value(parsed[:part]).to_s.sub(/\A[._-]/, "")
812
+ parsed[:part] = nil
813
+ end
791
814
  code_parts = []
792
815
  if parsed[:part].nil? && parsed[:iso_stage].nil? && parsed[:draft_version] &&
793
816
  parsed[:printed_dash_year] && parsed[:year] &&
@@ -805,15 +828,23 @@ module Pubid
805
828
  "#{extract_value(parsed[:draft_version])}-#{extract_value(parsed[:year])}"
806
829
  end
807
830
  end
808
- code_parts << extract_value(parsed[:part]) if parsed[:part]
831
+ # The part/subpart captures carry their printed separator
832
+ # (C1: the part binds before the draft status and the year,
833
+ # separator-in-capture), so they concatenate verbatim - a
834
+ # synthetic separator here is what produced "61850..9".
835
+ # The 8802-family IEC label binds its part with a dash the
836
+ # grammar consumes separately (iec_label_dash): "8802-9", not
837
+ # the dot join a bare part would get.
838
+ if parsed[:part] && parsed[:iec_label_dash]
839
+ code_parts << "-#{extract_value(parsed[:part])}"
840
+ elsif parsed[:part]
841
+ code_parts << extract_value(parsed[:part])
842
+ end
843
+ code_parts << extract_value(parsed[:subpart]) if parsed[:subpart]
809
844
 
810
845
  code_str = extract_value(parsed[:number])
811
846
  if code_str && !code_parts.empty?
812
- # The ISO/IEC label's part may be dash-joined ("8802-9") - keep
813
- # the printed separator (the iso-format route's :part_dash
814
- # prints as a dot in the joint code).
815
- sep = parsed[:iec_label_dash] ? "-" : "."
816
- code_str += "#{sep}#{code_parts.join(sep)}"
847
+ code_str += code_parts.map { |p| p.to_s.start_with?(".", "-", "_") ? p : ".#{p}" }.join
817
848
  end
818
849
  # P = project (a draft): identity-bearing, preserved as spelled.
819
850
  code_str = "P#{code_str}" if parsed[:project_marker] && code_str
@@ -849,6 +880,10 @@ module Pubid
849
880
  end
850
881
  attributes[:parenthetical_content] ||= extract_value(parsed[:edition_marker]) if parsed[:edition_marker]
851
882
 
883
+ # A joint ISO-print redline is a distinct document of the same
884
+ # standard — same flag semantics as the generic path (pubid#215).
885
+ attributes[:redline] = true if parsed[:redline]
886
+
852
887
  # Extract edition, from relaton's "/E-<n>" suffix. The grammar's /E
853
888
  # alternative captures the bare ordinal (nil-residue hand-off item 1).
854
889
  if parsed[:edition]
@@ -867,7 +902,34 @@ module Pubid
867
902
  draft_ver = extract_value(parsed[:draft_version])
868
903
  # Remove leading 'D' if present since draft_version already has it
869
904
  draft_ver = draft_ver.sub(/^D/, "") if draft_ver
870
- attributes[:ieee_draft] = "D#{draft_ver}" if draft_ver
905
+ if draft_ver
906
+ # A date trailing the draft ("/D8, June 2010") belongs to the
907
+ # draft component (canonical month, comma form), and the
908
+ # identity year/month the earlier promotion filled from the
909
+ # same captures are vacated. Stage-first ISO rows are
910
+ # exempt: their canonical is the ISO face, which prints the
911
+ # date from the identity slots (", March 2017", pubid#216).
912
+ if parsed[:draft_month] && parsed[:iso_stage].nil?
913
+ draft = Components::Draft.new(
914
+ version: draft_ver,
915
+ month: extract_value(parsed[:draft_month]),
916
+ year: extract_value(parsed[:draft_year]))
917
+ draft.comma_before_month = true
918
+ attributes[:draft] = draft.to_s.sub(%r{\A/}, "")
919
+ attributes.delete(:year) if parsed[:year].nil?
920
+ attributes.delete(:month) if parsed[:month].nil?
921
+ elsif parsed[:draft_year] && parsed[:iso_stage].nil?
922
+ # The artifact's comma-year tail captures its separator
923
+ # with the value (", 2015") — strip it (the #471
924
+ # separator-in-capture rule: every capture a renderer
925
+ # re-joins gets checked).
926
+ draft_year = extract_value(parsed[:draft_year]).to_s.sub(/\A[, ]+/, "")
927
+ attributes[:draft] = "D#{draft_ver}, #{draft_year}"
928
+ attributes.delete(:year) if parsed[:year].nil?
929
+ else
930
+ attributes[:ieee_draft] = "D#{draft_ver}"
931
+ end
932
+ end
871
933
  end
872
934
 
873
935
  # The joint stage-draft clause (docs/IEEE-DRAFT-STAGES.md §1.3):
@@ -909,7 +971,9 @@ module Pubid
909
971
  # must not become a printed Standard (which keeps the code's P).
910
972
  numeric_draft = parsed[:draft_version] &&
911
973
  extract_value(parsed[:draft_version]).to_s.match?(/\A\d/)
912
- printed_joint = parsed[:iso_published] &&
974
+ # A promoted stage ("29119.FDIS") is printed identity - the joint
975
+ # model carries it, never the printed Standard face.
976
+ printed_joint = parsed[:iso_published] && parsed[:iso_stage].nil? &&
913
977
  (parsed[:printed_dash_year] || parsed[:printed_month_year] ||
914
978
  (numeric_draft && parsed[:draft_month]) ||
915
979
  # A date-less stage-less joint reference carrying only a
@@ -919,9 +983,18 @@ module Pubid
919
983
  (parsed[:year].nil? && parsed[:parameters].is_a?(Hash) &&
920
984
  parsed[:parameters][:parenthetical_content]))
921
985
  if printed_joint
922
- sep = parsed[:part_dash] ? "-" : "."
923
- printed_code = [extract_value(parsed[:number]),
924
- extract_value(parsed[:part])].compact.join(sep)
986
+ # The part capture carries its own separator (C1
987
+ # separator-in-capture); join verbatim, synthesizing one only
988
+ # for a bare capture.
989
+ part_val = extract_value(parsed[:part])
990
+ printed_code = extract_value(parsed[:number]).to_s
991
+ if part_val
992
+ printed_code += if part_val.start_with?(".", "-", "_")
993
+ part_val
994
+ else
995
+ "#{parsed[:part_dash] ? "-" : "."}#{part_val}"
996
+ end
997
+ end
925
998
  # P = project: the marker is identity, preserved as spelled.
926
999
  printed_code = "P#{printed_code}" if parsed[:project_marker]
927
1000
  printed_attrs = { publisher: attributes[:publisher],
@@ -1049,6 +1122,24 @@ module Pubid
1049
1122
  Pubid::Ieee.locate_stage("P")
1050
1123
  end
1051
1124
 
1125
+ # A plain IEEE-designator draft on joint publishers ("/D8,
1126
+ # June 2010") carries no ISO stage machinery: it is the same
1127
+ # document family as its bare-IEEE twin and the D= designator
1128
+ # rows, so it builds as the project draft they pin (pubid#430)
1129
+ # rather than the joint-development model.
1130
+ if attributes[:draft] && parsed[:iso_stage].nil? &&
1131
+ parsed[:iec_stage].nil? && parsed[:amd_number].nil?
1132
+ draft_attrs = { code: attributes[:code],
1133
+ draft: attributes[:draft] }
1134
+ if attributes[:publishers]
1135
+ draft_attrs[:publisher] = attributes[:publishers].first
1136
+ rest = attributes[:publishers][1..]
1137
+ draft_attrs[:copublisher] = rest if rest
1138
+ end
1139
+ draft_attrs[:draft_status] = attributes[:draft_status] if attributes[:draft_status]
1140
+ return Identifiers::ProjectDraftIdentifier.new(**draft_attrs)
1141
+ end
1142
+
1052
1143
  joint = Identifiers::JointDevelopment.new(**attributes)
1053
1144
 
1054
1145
  # Amendment tail on the joint ISO-format form (pubid#317:
@@ -1322,6 +1413,11 @@ module Pubid
1322
1413
  # standards ("IEEE No148" prints "IEEE Std 148").
1323
1414
  type_value = "Std" if type_value&.match?(/\ANo\.?\z/) &&
1324
1415
  original_input.to_s.match?(/\AIEEE\s/)
1416
+ # The canonical type word is the short form (C2 ruling):
1417
+ # "IEEE Standard 495-2007" and "IEEE Std 495-2007" are the same
1418
+ # identifier, so the model normalizes to Std and matches? unifies
1419
+ # the spellings; the long word is a render option (:long).
1420
+ type_value = "Std" if type_value == "Standard"
1325
1421
  draft_status_value = extract_value(parsed[:draft_status])
1326
1422
 
1327
1423
  # Handle case where parser captured number without "P" prefix
@@ -1335,6 +1431,10 @@ module Pubid
1335
1431
  if !type_value && original_input&.match?(/ANSI\s+P/)
1336
1432
  # Extract P as type since it was in the original but parser stripped it
1337
1433
  type_value = "P"
1434
+ # The P is identity on a non-IEEE-led publisher too (the C1
1435
+ # ruling): record the marker so the render prepends it —
1436
+ # "ANSI PN42.34/D9a, 2015" round-trips.
1437
+ attributes[:project_marker] = true
1338
1438
  end
1339
1439
 
1340
1440
  # NOTE: "P" is a project/draft stage indicator, NOT a code prefix
@@ -1388,6 +1488,15 @@ module Pubid
1388
1488
  if parsed[:year]
1389
1489
  year_str = extract_value(parsed[:year])
1390
1490
  attributes[:year] = year_str
1491
+ # AIEE dash-glued date ("No 15-1928-05"): the tail binds as the
1492
+ # year/month clause with no separator capture, and the AIEE
1493
+ # long-form render is the dash form (a plain numeric month alone
1494
+ # would render ", 05 1928").
1495
+ if attributes[:publisher] == "AIEE" &&
1496
+ original_input.to_s.match?(/-\d{4}(?:-\d{2})?\z/) &&
1497
+ parsed[:month] && year_str.match?(/\A(19|20)\d\d\z/)
1498
+ attributes[:date_separator] = "-"
1499
+ end
1391
1500
  elsif parsed[:edition_year]
1392
1501
  # The `edition` rule captures its own year under :edition_year (not
1393
1502
  # :year) so it never collides with the base -YYYY (see parser.rb).
@@ -1418,7 +1527,13 @@ module Pubid
1418
1527
  # Set type attribute for backward compatibility (after P extraction)
1419
1528
  # NOTE: "P" IS a type code that maps to ProjectDraftIdentifier via flavor module
1420
1529
  # Type can be: "Std", "No", "P" (project draft), etc.
1421
- attributes[:type] = type_value if type_value
1530
+ # A DRAFT type word never reaches the hash: the draft-ness is
1531
+ # carried by draft_status and the draft component (the C2 ruling
1532
+ # - a draft is a Draft, "Draft Std" is banned from canonical
1533
+ # output), so an alias spelling and its canonical hash equal.
1534
+ # The value still feeds determine_stage_abbr below (the
1535
+ # version-less D1 fabrication, pubid#318).
1536
+ attributes[:type] = type_value if type_value && type_value !~ /\ADraft/
1422
1537
 
1423
1538
  # Lookup typed_stage from registry
1424
1539
  typed_stage_abbr = determine_stage_abbr(type_value, draft_status_value,
@@ -1485,9 +1600,29 @@ module Pubid
1485
1600
  # Redline
1486
1601
  attributes[:redline] = true if parsed[:redline]
1487
1602
 
1603
+ mark_glued_month(attributes)
1488
1604
  attributes
1489
1605
  end
1490
1606
 
1607
+ # Glued identity year-month provenance (pubid#517): when the input's
1608
+ # tail is the glued "-YYYY-MM" and the identity carries no draft or
1609
+ # revision suffix, the month belongs to the code's date clause
1610
+ # ("ANSI/IEEE 21-1976-11", "IEEE Std PC2-2016-01") and the renderer
1611
+ # reproduces the glued face. The same month/year attribute shape is
1612
+ # produced by trailing print dates (", 05 2014" — whose numeric form
1613
+ # is its own documented law) and by dash-promoted draft tails
1614
+ # ("/R-x-2011-04"), both of which keep the comma face: the input
1615
+ # shape, checked here at the single choke point every assignment
1616
+ # path converges to, is what distinguishes them (the AIEE
1617
+ # date_separator precedent).
1618
+ def mark_glued_month(attributes)
1619
+ return unless attributes[:month] && attributes[:year] &&
1620
+ attributes[:draft].nil? && attributes[:revision].nil? &&
1621
+ original_input.to_s.match?(/-\d{4}-\d{2}\z/)
1622
+
1623
+ attributes[:month_glued] = true
1624
+ end
1625
+
1491
1626
  # Determine stage abbreviation for TYPED_STAGE lookup
1492
1627
  # @param type_value [String] the type value (e.g., "Std", "P")
1493
1628
  # @param draft_status_value [String] the draft status (e.g., "Unapproved")
@@ -1638,6 +1773,17 @@ module Pubid
1638
1773
  # Normalize a leading hyphen ("/D-3.0" -> "3.0"); the draft
1639
1774
  # designator is otherwise held verbatim so no digit is lost.
1640
1775
  version = version.sub(/\A-/, "") if version
1776
+
1777
+ # FDIS/CDV/… are ISO/IEC stage tokens, not draft ordinals (C1
1778
+ # ruling, testsuite#2). "/DDIS" and "/DFDIS" are hallucinated
1779
+ # concatenations — the designator D does not merge onto a
1780
+ # stage word, and IEEE speak has no such token. The draft of
1781
+ # a stage rides the iso_stage slot, which Draft#to_s renders
1782
+ # as the D= designator face ("…/D=DIS-2020").
1783
+ if version.match?(/\A(FDIS|FCD|CDV|DIS|CD|WD|PWI|NP)\z/)
1784
+ iso_stage = version
1785
+ version = nil
1786
+ end
1641
1787
  end
1642
1788
 
1643
1789
  # The PARG capture carries its separator (".12", the #471
@@ -1649,7 +1795,7 @@ module Pubid
1649
1795
 
1650
1796
  # The compound both-systems form (docs/IEEE-DRAFT-STAGES.md
1651
1797
  # §1.3): the stage half of "=DDIS.3".
1652
- iso_stage = extract_value(draft_data[:draft_iso_stage]) if draft_data[:draft_iso_stage]
1798
+ iso_stage ||= extract_value(draft_data[:draft_iso_stage]) if draft_data[:draft_iso_stage]
1653
1799
  iso_iteration = extract_value(draft_data[:draft_iso_iteration]) if draft_data[:draft_iso_iteration]
1654
1800
 
1655
1801
  # Extract date information from draft data
@@ -1674,9 +1820,12 @@ module Pubid
1674
1820
  version = extract_value(draft_data)
1675
1821
  end
1676
1822
 
1823
+ # A stage-token draft's month rides the D= designator zero-padded
1824
+ # ("-202007", the pubid#203 convention) — it must not drop.
1825
+
1677
1826
  # Create Draft component object if we have version info (or a
1678
1827
  # compound stage half without an ordinal, "D=CDV:2020")
1679
- if version || draft_data.is_a?(Hash) && draft_data[:draft_iso_stage]
1828
+ if version || iso_stage || draft_data.is_a?(Hash) && draft_data[:draft_iso_stage]
1680
1829
  draft_obj = Components::Draft.new(
1681
1830
  version: version,
1682
1831
  revision: revision,
@@ -44,7 +44,11 @@ day: nil, iso_stage: nil, iso_iteration: nil)
44
44
  self.version = version
45
45
  self.revision = revision
46
46
  self.year = year
47
- self.original_month = month # Store original format
47
+ # The canonical month form is the IEEE crawl abbreviation
48
+ # (C2 ruling: "Aug 2007" and "August 2007" are the same
49
+ # document, so the model normalizes and matches? unifies the
50
+ # spellings; the long face spells the name back out).
51
+ self.original_month = month && CANONICAL_ABBREVIATIONS[month.sub(/\.\z/, "")] || month
48
52
  self.month = convert_month(month)
49
53
  self.day = day
50
54
  # The grammar's ordered alternation captures the stage WITH its
@@ -58,6 +62,16 @@ day: nil, iso_stage: nil, iso_iteration: nil)
58
62
  self.iso_iteration = iso_iteration
59
63
  end
60
64
 
65
+ # Canonical month spelling: the crawl abbreviation. Full names
66
+ # normalize onto these ("August" → "Aug"); "September" keeps its
67
+ # IEEE "Sept" form, so the 3-letter "Sep" folds onto it too.
68
+ CANONICAL_ABBREVIATIONS = {
69
+ "January" => "Jan", "February" => "Feb", "March" => "Mar",
70
+ "April" => "Apr", "June" => "Jun", "July" => "Jul",
71
+ "August" => "Aug", "September" => "Sept", "October" => "Oct",
72
+ "November" => "Nov", "December" => "Dec", "Sep" => "Sept",
73
+ }.freeze
74
+
61
75
  # Month names as an alternation, longest-first so "September" wins over
62
76
  # "Sep"; each may carry a trailing period ("Sept."). Used to split a
63
77
  # rendered draft's trailing date back out.
@@ -67,7 +81,7 @@ day: nil, iso_stage: nil, iso_iteration: nil)
67
81
  private_constant :MONTH_ALTERNATION
68
82
 
69
83
  DATE_SUFFIX =
70
- /(?:(, | )(#{MONTH_ALTERNATION})(?: (\d{1,2}))?(?:, | )| )((?:19|20)\d{2}[a-z]{0,2})\z/
84
+ /(?:(, | )(#{MONTH_ALTERNATION})(?: (\d{1,2}))?(?:, | )|, | )((?:19|20)\d{2}[a-z]{0,2})\z/
71
85
  private_constant :DATE_SUFFIX
72
86
 
73
87
  # Parse a rendered draft such as "/D3", "/D3.4", "/D7 Jul 2019", or a
@@ -151,6 +165,15 @@ day: nil, iso_stage: nil, iso_iteration: nil)
151
165
  end if revision
152
166
 
153
167
  if year
168
+ if iso_stage
169
+ # The D= designator face dash-joins its date in IEEE's
170
+ # convention: the year, and a draft month rides the
171
+ # designator zero-padded ("D=FDIS-201008", pubid#203).
172
+ result += "-#{year}"
173
+ result += format("%02d", month.to_i) if month
174
+ return result
175
+ end
176
+
154
177
  # Use original month format to preserve abbreviated vs full names
155
178
  display_month = original_month
156
179
 
@@ -173,7 +196,10 @@ day: nil, iso_stage: nil, iso_iteration: nil)
173
196
  ", #{year}"
174
197
  end
175
198
  else
176
- result += " #{year}"
199
+ # The year-only date joins with its comma - the grammar's
200
+ # comma-year draft tail (pubid-grammar 17244fc) - so the
201
+ # render re-parses through the same clause.
202
+ result += ", #{year}"
177
203
  end
178
204
  end
179
205
 
@@ -38,6 +38,12 @@ module Pubid
38
38
  attribute :revision, :string
39
39
  attribute :edition, :string # Edition 1.0
40
40
  attribute :month, :string
41
+ # Provenance flag (pubid#517): the month arrived glued to the identity
42
+ # year on the code ("ANSI/IEEE 21-1976-11", grammar
43
+ # date_with_month_numeric) rather than as a trailing ", 11 1976"
44
+ # print date. Serialization round-trips it so to_s reproduces the
45
+ # glued face.
46
+ attribute :month_glued, :boolean, default: -> { false }
41
47
  attribute :day, :string
42
48
  attribute :redline, :boolean, default: -> { false }
43
49
  attribute :amendments, Identifier, collection: true # Amendment identifiers
@@ -235,6 +241,7 @@ module Pubid
235
241
  Identifiers::Nesc::Draft,
236
242
  Identifiers::Nesc::Edition,
237
243
  Identifiers::Nesc::Handbook,
244
+ Identifiers::Nesc::Preprint,
238
245
  Identifiers::Nesc::Redline,
239
246
  Identifiers::Nesc::Standard,
240
247
  ]
@@ -159,8 +159,13 @@ module Pubid
159
159
 
160
160
  # The P project marker is identity-bearing and prints on both
161
161
  # faces (the standing trademark_leaf_to_s_spec contract:
162
- # "ISO/IEC/IEEE P26511:2018").
162
+ # "ISO/IEC/IEEE P26511:2018"). A P-prefixed joint project
163
+ # prints its parts dot-joined on the ISO face ("ISO/IEEE DIS
164
+ # P11073.10418", #216); a plain ISO/IEC number keeps the
165
+ # printed separator ("IEC/IEEE FDIS 60079-30-2", "ISO/IEC
166
+ # 8802-3").
163
167
  code_str = code.to_s
168
+ code_str = code_str.tr("-", ".") if code_str.start_with?("P")
164
169
  code_str += mark unless code_str.empty?
165
170
  parts << code_str if code_str && !code_str.empty?
166
171
 
@@ -172,6 +177,9 @@ module Pubid
172
177
  if parenthetical_content&.match?(%r{\A[A-Z](?:\s*[/&]\s*[A-Z])*\z})
173
178
  result += " (#{parenthetical_content})"
174
179
  end
180
+ # The redline marker is identity — the joint ISO print spells it
181
+ # with the space-dash form ("15289:2015(E) - Redline", pubid#215).
182
+ result += " - Redline" if redline
175
183
 
176
184
  result
177
185
  end
@@ -200,6 +208,9 @@ module Pubid
200
208
  # Publishers (slash-separated)
201
209
  parts << publishers.join("/") if publishers && !publishers.empty?
202
210
 
211
+ # A status word ("Unapproved") marks the draft (pubid#318)
212
+ parts << draft_status if draft_status
213
+
203
214
  # Build code part — the P-state prints as spelled (P = project
204
215
  # draft; no P = standard). Never added or stripped.
205
216
  code_str = code.to_s
@@ -230,7 +241,8 @@ module Pubid
230
241
  # ("D=CD.2" — docs/IEEE-DRAFT-STAGES.md §1.3).
231
242
  stage = iso_stage.match(/\A([A-Z]+?)(\d+)\z/)
232
243
  code_str += stage ? "/D=#{stage[1]}.#{stage[2]}" : "/D=#{iso_stage}"
233
- elsif typed_stage&.ieee_draft_equivalent
244
+ elsif typed_stage&.ieee_draft_equivalent &&
245
+ !code_str.start_with?(typed_stage.ieee_draft_equivalent)
234
246
  code_str += "/#{typed_stage.ieee_draft_equivalent}"
235
247
  end
236
248
 
@@ -241,6 +253,7 @@ module Pubid
241
253
  result = parts.join(" ")
242
254
  result += "-#{year}" if year && !@designator_carries_year &&
243
255
  !ieee_draft.to_s.end_with?("-#{year}")
256
+ result += " - Redline" if redline
244
257
 
245
258
  result
246
259
  end
@@ -79,7 +79,7 @@ module Pubid
79
79
  #
80
80
  # @return [Boolean]
81
81
  def draft?
82
- is_a?(Nesc::Draft)
82
+ is_a?(Nesc::Draft) || is_a?(Nesc::Preprint)
83
83
  end
84
84
 
85
85
  # Rendering for year-first NESC identifiers (the C2-code standard form
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Ieee
5
+ module Identifiers
6
+ module Nesc
7
+ # Preprint NESC identifier — the draft stage preceding a C2 edition.
8
+ # "Preprint" is a draft stage (the NESC's own terminology for its
9
+ # pre-edition drafts), not a variant marker: the document IS a draft
10
+ # of the C2 code, distinct from the published edition.
11
+ #
12
+ # @example
13
+ # nesc = Pubid::Ieee.parse("IEEE Std C2.2012.Preprint")
14
+ # nesc.to_s # => "IEEE Std C2.2012.Preprint"
15
+ class Preprint < Base
16
+ include Pubid::Ieee::Identifiers::CodeNumber
17
+
18
+ def self.polymorphic_name
19
+ "pubid:ieee:nesc-preprint"
20
+ end
21
+
22
+ def draft?
23
+ true
24
+ end
25
+
26
+ # Render preprint identifier. The canonical spelling is dotted
27
+ # (C2.2012.Preprint); the dash spelling is a non-normalized alias.
28
+ #
29
+ # @param trademark [Boolean] append the IEEE trademark symbol (™/®)
30
+ # @return [String] IEEE Std C2.YYYY.Preprint
31
+ def to_s(trademark: false, **opts)
32
+ body = ["C2", year, "Preprint"].compact.join(".")
33
+ result = ["IEEE Std", body].join(" ")
34
+ result += trademark_symbol if trademark
35
+ annotate_plain_render(result, **opts)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -8,6 +8,7 @@ module Pubid
8
8
  autoload :Draft, "#{__dir__}/nesc/draft"
9
9
  autoload :Edition, "#{__dir__}/nesc/edition"
10
10
  autoload :Handbook, "#{__dir__}/nesc/handbook"
11
+ autoload :Preprint, "#{__dir__}/nesc/preprint"
11
12
  autoload :Redline, "#{__dir__}/nesc/redline"
12
13
  autoload :Standard, "#{__dir__}/nesc/standard"
13
14
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Ieee
5
+ # The long face (C2 ruling): one uniform format dimension for every
6
+ # IEEE identifier. Expands the type words an identifier actually
7
+ # carries — Std → Standard on a published standard — and spells
8
+ # draft months out ("August 2007"). A draft carries no Std/Standard
9
+ # word, so both faces render `Draft`; there are no draft-specific
10
+ # render rules.
11
+ class LongRenderer < Renderer
12
+ def render(context: nil, **opts)
13
+ @long = true
14
+ super
15
+ end
16
+ end
17
+ end
18
+ end
@@ -88,6 +88,11 @@ module Pubid
88
88
  # @param parsed_hash [Hash] Parsed attributes
89
89
  # @return [Class] Identifier class to instantiate
90
90
  def determine_identifier_class(parsed_hash)
91
+ # Preprint — a draft STAGE of the C2 code (checked before the
92
+ # Standard route below: the c2-preprint tree carries :code "C2"
93
+ # too, and the verbose proposal forms carry none).
94
+ return Identifiers::Nesc::Preprint if parsed_hash[:preprint]
95
+
91
96
  # Draft identifiers
92
97
  return Identifiers::Nesc::Draft if parsed_hash[:draft]
93
98
 
@@ -735,10 +735,19 @@ module Pubid
735
735
  (slash >> str("Amd") >> (dot | space).maybe >>
736
736
  digits.as(:amd_number) >>
737
737
  ((str(":") | dash) >> year_digits.as(:amd_year)).maybe).maybe >>
738
+ # Optional corrigendum tail, the ISO print's dotted spelling
739
+ # (pubid#215): "8802-1AC:2018/Cor.1:2020(E)". The shared
740
+ # corrigendum rule accepts the slash/dot/colon separators; the
741
+ # flat tree routes through build_flat_corrigendum.
742
+ corrigendum.maybe >>
738
743
  # Optional edition, from relaton's "/E-<n>" suffix normalized to
739
744
  # "Edition <n>.0[ YYYY]" (nil-residue hand-off item 1).
740
745
  edition.maybe >>
741
746
  revision_suffix.maybe >>
747
+ # A redline is a distinct document of the joint ISO print
748
+ # (pubid#215): "15289:2015(E) - Redline". Same flag semantics as
749
+ # the generic rules - the builder sets redline: true.
750
+ redline.maybe >>
742
751
  # Published joint docs print a language marker, after either the
743
752
  # year or the amendment tail: "9945:2009(E)", "...Amd.1:2014(E)".
744
753
  # The crawl sometimes spaces it ("…(E), January 2017" family,
@@ -1465,10 +1474,6 @@ module Pubid
1465
1474
  cleaned = "IEEE P#{cleaned}"
1466
1475
  end
1467
1476
 
1468
- # Part D: Suffix Normalization
1469
- # 5. "/Preprint" -> remove (data quality - not standard suffix)
1470
- cleaned = cleaned.gsub(/\/Preprint\b/, "")
1471
-
1472
1477
  # Part E: Relationship Text Normalization
1473
1478
  # 6. "Proposed Revision of" -> "Revision of"
1474
1479
  cleaned = cleaned.gsub("Proposed Revision of", "Revision of")