pubid 2.0.0.pre.alpha.27 → 2.0.0.pre.alpha.28

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
 
@@ -771,6 +771,11 @@ module Pubid
771
771
  def build_joint_development(parsed)
772
772
  attributes = {}
773
773
 
774
+ # The status-draft spelling ("IEEE Unapproved P15288/DFDIS") keeps
775
+ # its status word on the designator face (pubid#318: the word marks
776
+ # the draft).
777
+ attributes[:draft_status] = extract_value(parsed[:draft_status]) if parsed[:draft_status]
778
+
774
779
  # Extract publishers from joint_publishers
775
780
  if parsed[:joint_publishers]
776
781
  joint_pub_str = extract_value(parsed[:joint_publishers])
@@ -788,6 +793,17 @@ module Pubid
788
793
  # date ("P42010/D-4-2019") rides the code as a year-part
789
794
  # ("P42010.2019/D4") — the face the relaton fixtures pin — while
790
795
  # stage-first joints keep the top-level year ("FDIS P15289:2017").
796
+ # A part whose whole value is an ISO/IEC stage token is the
797
+ # stage-last spelling ("29119.FDIS"), not a part: promote it to
798
+ # the stage slot (the #311 embedded rule covers "29119.4.FDIS";
799
+ # this covers the part-less form the shared part capture claims
800
+ # first).
801
+ if parsed[:iso_stage].nil? && parsed[:part] &&
802
+ extract_value(parsed[:part]).to_s.match?(/\A[._-]?(FDIS|FCD|CDV|DIS|CD\d?|WD|PWI|NP)\z/)
803
+ # the stage is the bare token; the capture carries its separator
804
+ parsed[:iso_stage] = extract_value(parsed[:part]).to_s.sub(/\A[._-]/, "")
805
+ parsed[:part] = nil
806
+ end
791
807
  code_parts = []
792
808
  if parsed[:part].nil? && parsed[:iso_stage].nil? && parsed[:draft_version] &&
793
809
  parsed[:printed_dash_year] && parsed[:year] &&
@@ -805,15 +821,23 @@ module Pubid
805
821
  "#{extract_value(parsed[:draft_version])}-#{extract_value(parsed[:year])}"
806
822
  end
807
823
  end
808
- code_parts << extract_value(parsed[:part]) if parsed[:part]
824
+ # The part/subpart captures carry their printed separator
825
+ # (C1: the part binds before the draft status and the year,
826
+ # separator-in-capture), so they concatenate verbatim - a
827
+ # synthetic separator here is what produced "61850..9".
828
+ # The 8802-family IEC label binds its part with a dash the
829
+ # grammar consumes separately (iec_label_dash): "8802-9", not
830
+ # the dot join a bare part would get.
831
+ if parsed[:part] && parsed[:iec_label_dash]
832
+ code_parts << "-#{extract_value(parsed[:part])}"
833
+ elsif parsed[:part]
834
+ code_parts << extract_value(parsed[:part])
835
+ end
836
+ code_parts << extract_value(parsed[:subpart]) if parsed[:subpart]
809
837
 
810
838
  code_str = extract_value(parsed[:number])
811
839
  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)}"
840
+ code_str += code_parts.map { |p| p.to_s.start_with?(".", "-", "_") ? p : ".#{p}" }.join
817
841
  end
818
842
  # P = project (a draft): identity-bearing, preserved as spelled.
819
843
  code_str = "P#{code_str}" if parsed[:project_marker] && code_str
@@ -867,7 +891,34 @@ module Pubid
867
891
  draft_ver = extract_value(parsed[:draft_version])
868
892
  # Remove leading 'D' if present since draft_version already has it
869
893
  draft_ver = draft_ver.sub(/^D/, "") if draft_ver
870
- attributes[:ieee_draft] = "D#{draft_ver}" if draft_ver
894
+ if draft_ver
895
+ # A date trailing the draft ("/D8, June 2010") belongs to the
896
+ # draft component (canonical month, comma form), and the
897
+ # identity year/month the earlier promotion filled from the
898
+ # same captures are vacated. Stage-first ISO rows are
899
+ # exempt: their canonical is the ISO face, which prints the
900
+ # date from the identity slots (", March 2017", pubid#216).
901
+ if parsed[:draft_month] && parsed[:iso_stage].nil?
902
+ draft = Components::Draft.new(
903
+ version: draft_ver,
904
+ month: extract_value(parsed[:draft_month]),
905
+ year: extract_value(parsed[:draft_year]))
906
+ draft.comma_before_month = true
907
+ attributes[:draft] = draft.to_s.sub(%r{\A/}, "")
908
+ attributes.delete(:year) if parsed[:year].nil?
909
+ attributes.delete(:month) if parsed[:month].nil?
910
+ elsif parsed[:draft_year] && parsed[:iso_stage].nil?
911
+ # The artifact's comma-year tail captures its separator
912
+ # with the value (", 2015") — strip it (the #471
913
+ # separator-in-capture rule: every capture a renderer
914
+ # re-joins gets checked).
915
+ draft_year = extract_value(parsed[:draft_year]).to_s.sub(/\A[, ]+/, "")
916
+ attributes[:draft] = "D#{draft_ver}, #{draft_year}"
917
+ attributes.delete(:year) if parsed[:year].nil?
918
+ else
919
+ attributes[:ieee_draft] = "D#{draft_ver}"
920
+ end
921
+ end
871
922
  end
872
923
 
873
924
  # The joint stage-draft clause (docs/IEEE-DRAFT-STAGES.md §1.3):
@@ -909,7 +960,9 @@ module Pubid
909
960
  # must not become a printed Standard (which keeps the code's P).
910
961
  numeric_draft = parsed[:draft_version] &&
911
962
  extract_value(parsed[:draft_version]).to_s.match?(/\A\d/)
912
- printed_joint = parsed[:iso_published] &&
963
+ # A promoted stage ("29119.FDIS") is printed identity - the joint
964
+ # model carries it, never the printed Standard face.
965
+ printed_joint = parsed[:iso_published] && parsed[:iso_stage].nil? &&
913
966
  (parsed[:printed_dash_year] || parsed[:printed_month_year] ||
914
967
  (numeric_draft && parsed[:draft_month]) ||
915
968
  # A date-less stage-less joint reference carrying only a
@@ -919,9 +972,18 @@ module Pubid
919
972
  (parsed[:year].nil? && parsed[:parameters].is_a?(Hash) &&
920
973
  parsed[:parameters][:parenthetical_content]))
921
974
  if printed_joint
922
- sep = parsed[:part_dash] ? "-" : "."
923
- printed_code = [extract_value(parsed[:number]),
924
- extract_value(parsed[:part])].compact.join(sep)
975
+ # The part capture carries its own separator (C1
976
+ # separator-in-capture); join verbatim, synthesizing one only
977
+ # for a bare capture.
978
+ part_val = extract_value(parsed[:part])
979
+ printed_code = extract_value(parsed[:number]).to_s
980
+ if part_val
981
+ printed_code += if part_val.start_with?(".", "-", "_")
982
+ part_val
983
+ else
984
+ "#{parsed[:part_dash] ? "-" : "."}#{part_val}"
985
+ end
986
+ end
925
987
  # P = project: the marker is identity, preserved as spelled.
926
988
  printed_code = "P#{printed_code}" if parsed[:project_marker]
927
989
  printed_attrs = { publisher: attributes[:publisher],
@@ -1049,6 +1111,24 @@ module Pubid
1049
1111
  Pubid::Ieee.locate_stage("P")
1050
1112
  end
1051
1113
 
1114
+ # A plain IEEE-designator draft on joint publishers ("/D8,
1115
+ # June 2010") carries no ISO stage machinery: it is the same
1116
+ # document family as its bare-IEEE twin and the D= designator
1117
+ # rows, so it builds as the project draft they pin (pubid#430)
1118
+ # rather than the joint-development model.
1119
+ if attributes[:draft] && parsed[:iso_stage].nil? &&
1120
+ parsed[:iec_stage].nil? && parsed[:amd_number].nil?
1121
+ draft_attrs = { code: attributes[:code],
1122
+ draft: attributes[:draft] }
1123
+ if attributes[:publishers]
1124
+ draft_attrs[:publisher] = attributes[:publishers].first
1125
+ rest = attributes[:publishers][1..]
1126
+ draft_attrs[:copublisher] = rest if rest
1127
+ end
1128
+ draft_attrs[:draft_status] = attributes[:draft_status] if attributes[:draft_status]
1129
+ return Identifiers::ProjectDraftIdentifier.new(**draft_attrs)
1130
+ end
1131
+
1052
1132
  joint = Identifiers::JointDevelopment.new(**attributes)
1053
1133
 
1054
1134
  # Amendment tail on the joint ISO-format form (pubid#317:
@@ -1322,6 +1402,11 @@ module Pubid
1322
1402
  # standards ("IEEE No148" prints "IEEE Std 148").
1323
1403
  type_value = "Std" if type_value&.match?(/\ANo\.?\z/) &&
1324
1404
  original_input.to_s.match?(/\AIEEE\s/)
1405
+ # The canonical type word is the short form (C2 ruling):
1406
+ # "IEEE Standard 495-2007" and "IEEE Std 495-2007" are the same
1407
+ # identifier, so the model normalizes to Std and matches? unifies
1408
+ # the spellings; the long word is a render option (:long).
1409
+ type_value = "Std" if type_value == "Standard"
1325
1410
  draft_status_value = extract_value(parsed[:draft_status])
1326
1411
 
1327
1412
  # Handle case where parser captured number without "P" prefix
@@ -1335,6 +1420,10 @@ module Pubid
1335
1420
  if !type_value && original_input&.match?(/ANSI\s+P/)
1336
1421
  # Extract P as type since it was in the original but parser stripped it
1337
1422
  type_value = "P"
1423
+ # The P is identity on a non-IEEE-led publisher too (the C1
1424
+ # ruling): record the marker so the render prepends it —
1425
+ # "ANSI PN42.34/D9a, 2015" round-trips.
1426
+ attributes[:project_marker] = true
1338
1427
  end
1339
1428
 
1340
1429
  # NOTE: "P" is a project/draft stage indicator, NOT a code prefix
@@ -1388,6 +1477,15 @@ module Pubid
1388
1477
  if parsed[:year]
1389
1478
  year_str = extract_value(parsed[:year])
1390
1479
  attributes[:year] = year_str
1480
+ # AIEE dash-glued date ("No 15-1928-05"): the tail binds as the
1481
+ # year/month clause with no separator capture, and the AIEE
1482
+ # long-form render is the dash form (a plain numeric month alone
1483
+ # would render ", 05 1928").
1484
+ if attributes[:publisher] == "AIEE" &&
1485
+ original_input.to_s.match?(/-\d{4}(?:-\d{2})?\z/) &&
1486
+ parsed[:month] && year_str.match?(/\A(19|20)\d\d\z/)
1487
+ attributes[:date_separator] = "-"
1488
+ end
1391
1489
  elsif parsed[:edition_year]
1392
1490
  # The `edition` rule captures its own year under :edition_year (not
1393
1491
  # :year) so it never collides with the base -YYYY (see parser.rb).
@@ -1418,7 +1516,13 @@ module Pubid
1418
1516
  # Set type attribute for backward compatibility (after P extraction)
1419
1517
  # NOTE: "P" IS a type code that maps to ProjectDraftIdentifier via flavor module
1420
1518
  # Type can be: "Std", "No", "P" (project draft), etc.
1421
- attributes[:type] = type_value if type_value
1519
+ # A DRAFT type word never reaches the hash: the draft-ness is
1520
+ # carried by draft_status and the draft component (the C2 ruling
1521
+ # - a draft is a Draft, "Draft Std" is banned from canonical
1522
+ # output), so an alias spelling and its canonical hash equal.
1523
+ # The value still feeds determine_stage_abbr below (the
1524
+ # version-less D1 fabrication, pubid#318).
1525
+ attributes[:type] = type_value if type_value && type_value !~ /\ADraft/
1422
1526
 
1423
1527
  # Lookup typed_stage from registry
1424
1528
  typed_stage_abbr = determine_stage_abbr(type_value, draft_status_value,
@@ -1638,6 +1742,17 @@ module Pubid
1638
1742
  # Normalize a leading hyphen ("/D-3.0" -> "3.0"); the draft
1639
1743
  # designator is otherwise held verbatim so no digit is lost.
1640
1744
  version = version.sub(/\A-/, "") if version
1745
+
1746
+ # FDIS/CDV/… are ISO/IEC stage tokens, not draft ordinals (C1
1747
+ # ruling, testsuite#2). "/DDIS" and "/DFDIS" are hallucinated
1748
+ # concatenations — the designator D does not merge onto a
1749
+ # stage word, and IEEE speak has no such token. The draft of
1750
+ # a stage rides the iso_stage slot, which Draft#to_s renders
1751
+ # as the D= designator face ("…/D=DIS-2020").
1752
+ if version.match?(/\A(FDIS|FCD|CDV|DIS|CD|WD|PWI|NP)\z/)
1753
+ iso_stage = version
1754
+ version = nil
1755
+ end
1641
1756
  end
1642
1757
 
1643
1758
  # The PARG capture carries its separator (".12", the #471
@@ -1649,7 +1764,7 @@ module Pubid
1649
1764
 
1650
1765
  # The compound both-systems form (docs/IEEE-DRAFT-STAGES.md
1651
1766
  # §1.3): the stage half of "=DDIS.3".
1652
- iso_stage = extract_value(draft_data[:draft_iso_stage]) if draft_data[:draft_iso_stage]
1767
+ iso_stage ||= extract_value(draft_data[:draft_iso_stage]) if draft_data[:draft_iso_stage]
1653
1768
  iso_iteration = extract_value(draft_data[:draft_iso_iteration]) if draft_data[:draft_iso_iteration]
1654
1769
 
1655
1770
  # Extract date information from draft data
@@ -1674,9 +1789,12 @@ module Pubid
1674
1789
  version = extract_value(draft_data)
1675
1790
  end
1676
1791
 
1792
+ # A stage-token draft's month rides the D= designator zero-padded
1793
+ # ("-202007", the pubid#203 convention) — it must not drop.
1794
+
1677
1795
  # Create Draft component object if we have version info (or a
1678
1796
  # compound stage half without an ordinal, "D=CDV:2020")
1679
- if version || draft_data.is_a?(Hash) && draft_data[:draft_iso_stage]
1797
+ if version || iso_stage || draft_data.is_a?(Hash) && draft_data[:draft_iso_stage]
1680
1798
  draft_obj = Components::Draft.new(
1681
1799
  version: version,
1682
1800
  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
 
@@ -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
 
@@ -200,6 +205,9 @@ module Pubid
200
205
  # Publishers (slash-separated)
201
206
  parts << publishers.join("/") if publishers && !publishers.empty?
202
207
 
208
+ # A status word ("Unapproved") marks the draft (pubid#318)
209
+ parts << draft_status if draft_status
210
+
203
211
  # Build code part — the P-state prints as spelled (P = project
204
212
  # draft; no P = standard). Never added or stripped.
205
213
  code_str = code.to_s
@@ -230,7 +238,8 @@ module Pubid
230
238
  # ("D=CD.2" — docs/IEEE-DRAFT-STAGES.md §1.3).
231
239
  stage = iso_stage.match(/\A([A-Z]+?)(\d+)\z/)
232
240
  code_str += stage ? "/D=#{stage[1]}.#{stage[2]}" : "/D=#{iso_stage}"
233
- elsif typed_stage&.ieee_draft_equivalent
241
+ elsif typed_stage&.ieee_draft_equivalent &&
242
+ !code_str.start_with?(typed_stage.ieee_draft_equivalent)
234
243
  code_str += "/#{typed_stage.ieee_draft_equivalent}"
235
244
  end
236
245
 
@@ -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
@@ -24,7 +24,29 @@ module Pubid
24
24
  # (Pubid::Identifier#render), so the flag is safe as instance state.
25
25
  @trademark = opts[:trademark]
26
26
 
27
- render_id(@id)
27
+ result = render_id(@id)
28
+ # The long face spells draft months out ("Aug" → "August") over the
29
+ # bounded identifier — no title prose is ever in the string.
30
+ result = expand_months(result) if @long
31
+ result
32
+ end
33
+
34
+ # The long-form month names, longest key first ("Sept" before "Sep").
35
+ LONG_MONTHS = {
36
+ "Sept" => "September", "Jan" => "January", "Feb" => "February",
37
+ "Mar" => "March", "Apr" => "April", "Jun" => "June",
38
+ "Jul" => "July", "Aug" => "August", "Sep" => "September",
39
+ "Oct" => "October", "Nov" => "November", "Dec" => "December",
40
+ }.freeze
41
+ private_constant :LONG_MONTHS
42
+
43
+ # Expand abbreviated draft months to their long names (\b-bounded, so
44
+ # only whole month tokens move).
45
+ def expand_months(str)
46
+ LONG_MONTHS.each do |abbr, full|
47
+ str = str.gsub(/\b#{abbr}\b/, full)
48
+ end
49
+ str
28
50
  end
29
51
 
30
52
  private
@@ -125,6 +147,15 @@ module Pubid
125
147
  !id.draft_status.to_s.match?(/unapproved/i)
126
148
  type_str = id.type.dup
127
149
  type_str = type_str.sub(/^P/, "") if type_str.start_with?("P")
150
+ # A draft carries the word Draft alone in every face (the C2
151
+ # ruling): Std/Standard never renders on a draft, whatever
152
+ # spelling the source used — a status-less draft ("IEEE Draft
153
+ # Std P802.3/D2.0") renders "IEEE Draft P802.3/D2.0".
154
+ type_str = "Draft" if id.draft_obj && !id.draft_status
155
+ # The long face expands the type word ("IEEE Standard 495-2007");
156
+ # a draft never takes Std/Standard in any face, so the expansion
157
+ # only ever sees the published word.
158
+ type_str = "Standard" if @long && type_str == "Std"
128
159
  parts << type_str unless type_str.strip.empty?
129
160
  end
130
161
 
@@ -151,8 +182,14 @@ module Pubid
151
182
  publishers: publishers_of(id))
152
183
 
153
184
 
154
- # Only attach year to code if there's no edition, no month, and no draft
155
- result += "-#{id.year}" if id.year && !id.draft_obj && !id.edition && !id.month
185
+ # A base year dash-attaches to the code when nothing else
186
+ # carries it: no edition, no identity-month date - and either
187
+ # no draft at all, or a draft that carries its OWN date (the
188
+ # draft+corrigendum family: "PC37.09-1999/D7.2, Nov 2006" -
189
+ # the 1999 is the base's year, the Nov 2006 the draft's).
190
+ year_on_code = id.year && !id.edition && !id.month &&
191
+ (!id.draft_obj || id.draft_obj&.year)
192
+ result += "-#{id.year}" if year_on_code
156
193
 
157
194
  # Append the numbered/lettered revision inline, in IEEE's native
158
195
  # position — right after the code number and before the draft
@@ -232,6 +269,14 @@ module Pubid
232
269
  if id.year && !id.edition
233
270
  result += " #{id.year}"
234
271
  end
272
+ elsif id.year && id.draft_obj && !id.edition && !year_on_code &&
273
+ !(id.publisher == "IEC" && id.copublisher == ["IEEE"])
274
+ # A year-only date trailing the draft ("ANSI N42.34/D9a,
275
+ # 2015") keeps its comma form - the grammar's comma-year
276
+ # draft tail - instead of being silently dropped. A year
277
+ # that dash-attached to the code above (the base year of a
278
+ # dated-draft family) does not repeat here.
279
+ result += ", #{id.year}"
235
280
  end
236
281
 
237
282
  # Bounded parenthetical markers only. The unbounded relationship/
data/lib/pubid/ieee.rb CHANGED
@@ -21,6 +21,7 @@ module Pubid
21
21
  autoload :PreParser, "#{__dir__}/ieee/pre_parser"
22
22
  autoload :Compaction, "#{__dir__}/ieee/compaction"
23
23
  autoload :ProjectRenderer, "#{__dir__}/ieee/project_renderer"
24
+ autoload :LongRenderer, "#{__dir__}/ieee/long_renderer"
24
25
  autoload :Renderer, "#{__dir__}/ieee/renderer"
25
26
  autoload :TypedStages, "#{__dir__}/ieee/typed_stages"
26
27
  autoload :UrnGenerator, "#{__dir__}/ieee/urn_generator"
@@ -164,3 +165,4 @@ Pubid::Registry.register(:ieee, Pubid::Ieee)
164
165
  Pubid::Ieee::Identifier.format_registry = Pubid::FormatRegistry.new(parent: Pubid::Identifier.format_registry)
165
166
  Pubid::Ieee::Identifier.format_registry.register(:human, renderer: Pubid::Ieee::Renderer)
166
167
  Pubid::Ieee::Identifier.format_registry.register(:project, renderer: Pubid::Ieee::ProjectRenderer)
168
+ Pubid::Ieee::Identifier.format_registry.register(:long, renderer: Pubid::Ieee::LongRenderer)
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "date"
3
4
  require "lutaml/model"
4
5
 
5
6
  module Pubid
@@ -81,6 +82,10 @@ module Pubid
81
82
  # For letter suffixes, append uppercase letter directly: " r5A" or "r5A"
82
83
  if additional_text.match?(/^[A-Za-z]$/)
83
84
  result += additional_text.upcase
85
+ elsif additional_text.match?(/\Ar\d+\z/)
86
+ # A revision marker rides attached ("e2001r3"), the 1.x
87
+ # corpus form
88
+ result += additional_text
84
89
  else
85
90
  # For other additional text (years, etc.), use dot separator
86
91
  result += ".#{additional_text}"
@@ -106,6 +111,10 @@ module Pubid
106
111
  # Use UPPERCASE for consistency: "r5A" for SP patterns like 800-53r5a
107
112
  elsif additional_text.match?(/^[A-Za-z]$/)
108
113
  result += additional_text.upcase # Normalize to UPPERCASE: r5A
114
+ elsif additional_text.match?(/\Ar\d+\z/)
115
+ # A revision marker rides attached ("e2001r3"), the 1.x
116
+ # corpus form
117
+ result += additional_text
109
118
  else
110
119
  # For ALL years (2-digit or 4-digit) and month+year, use DOT separator
111
120
  # Examples: e2.50, e2.1915, e2.June1908
@@ -119,8 +128,20 @@ module Pubid
119
128
  # Build long format: "Edition 2021", "Revision 5", etc.
120
129
  def build_long_format
121
130
  case type
131
+ # An edition date renders as the parenthesised date
132
+ # ("1000e2021" long: "... (2021)", "ABCe20130101" long:
133
+ # "... (January 01, 2013)"), the 1.x corpus form; a numbered
134
+ # edition keeps its worded form ("Edition 2")
122
135
  when "e"
123
- "Edition #{id}"
136
+ case id.to_s
137
+ when /\A\d{4}\z/ then "(#{id})"
138
+ when /\A(\d{4})(\d{2})(\d{2})\z/
139
+ "(#{::Date.new(Regexp.last_match[1].to_i,
140
+ Regexp.last_match[2].to_i,
141
+ Regexp.last_match[3].to_i)
142
+ .strftime('%B %d, %Y')})"
143
+ else "Edition #{id}"
144
+ end
124
145
  when "r"
125
146
  "Revision #{id}"
126
147
  when "-"
@@ -31,11 +31,13 @@ module Pubid
31
31
 
32
32
  case format
33
33
  when :short, :long
34
- " #{code}"
34
+ # 1.x corpus shape: the trailing parenthesised suffix
35
+ # "(en)" (pubid#497), not a leading segment.
36
+ "(#{code})"
35
37
  when :mr
36
38
  ".#{code}"
37
39
  else
38
- " #{code}"
40
+ "(#{code})"
39
41
  end
40
42
  end
41
43
  end