pubid 2.0.0.pre.alpha.28 → 2.0.0.pre.alpha.30

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.
@@ -5,6 +5,17 @@ module Pubid
5
5
  # Builder class for constructing IEEE identifier scheme from parsed data
6
6
  # Single Responsibility: Transform parsed data into identifier objects
7
7
  class Builder
8
+ # The month_name vocabulary the REV date embed canonicalizes numeric
9
+ # (the parg rule's exact spellings, dots already stripped).
10
+ REV_MONTH_NUMBERS = {
11
+ "January" => "01", "February" => "02", "March" => "03",
12
+ "April" => "04", "May" => "05", "June" => "06", "July" => "07",
13
+ "August" => "08", "September" => "09", "Sept" => "09",
14
+ "October" => "10", "November" => "11", "December" => "12",
15
+ "Jan" => "01", "Feb" => "02", "Mar" => "03", "Apr" => "04",
16
+ "Jun" => "06", "Jul" => "07", "Aug" => "08", "Sep" => "09",
17
+ "Oct" => "10", "Nov" => "11", "Dec" => "12",
18
+ }.freeze
8
19
  attr_accessor :original_input
9
20
  attr_reader :identifier_class
10
21
 
@@ -287,6 +298,24 @@ module Pubid
287
298
  end
288
299
  dash = parsed_hash[:dash_year] ? parsed_hash : nil
289
300
  dash ||= drafts.find { |d| d.is_a?(Hash) && d[:dash_year] }
301
+ if dash.nil? && parsed_hash[:draft]
302
+ # A numeric month trailing a draft is the draft's own date: it
303
+ # embeds in the designator ("D09, 04 2011" -> "D09-2011-04")
304
+ # rather than printing the long-format comma face (a month name
305
+ # keeps the comma, "D7.2, Nov 2006") — pubid ruling 2026-10-13.
306
+ m = parsed_hash[:month] || parsed_hash[:trailing_month]
307
+ y = parsed_hash[:year] || parsed_hash[:trailing_year]
308
+ if m.to_s.match?(/\A\d{1,2}\z/) && y
309
+ host = drafts.find { |d| d.is_a?(Hash) && d[:draft_version] }
310
+ if host
311
+ host[:draft_version] = [host[:draft_version], y, m].compact.join("-")
312
+ parsed_hash.delete(:month)
313
+ parsed_hash.delete(:year)
314
+ parsed_hash.delete(:trailing_month)
315
+ parsed_hash.delete(:trailing_year)
316
+ end
317
+ end
318
+ end
290
319
  if dash && parsed_hash[:year].nil?
291
320
  joined = drafts.filter_map { |d| d.is_a?(Hash) ? d[:draft_version] : nil }
292
321
  .map { |v| v.is_a?(Hash) ? v[:value] : v.to_s }.join
@@ -297,6 +326,15 @@ module Pubid
297
326
  dash_led = joined.start_with?("-")
298
327
  if bare_joint_draft && dash_led
299
328
  parsed_hash[:part] = { value: dash[:dash_year] }
329
+ elsif dash[:dash_month] &&
330
+ (host = drafts.find { |d| d.is_a?(Hash) && d[:draft_version] })
331
+ # The draft's own numeric date embeds in the designator
332
+ # ("D2-2015-04") — the "D2, 04 2015" comma spelling
333
+ # normalizes onto it (pubid ruling 2026-10-13).
334
+ host[:draft_version] =
335
+ [joined, dash[:dash_year], dash[:dash_month]].compact.join("-")
336
+ dash.delete(:dash_year)
337
+ dash.delete(:dash_month)
300
338
  elsif dash[:dash_month] || dash_led
301
339
  parsed_hash[:year] = dash[:dash_year]
302
340
  parsed_hash[:month] = dash[:dash_month]
@@ -307,9 +345,16 @@ module Pubid
307
345
  # draft version misses the D-registry), no separate year.
308
346
  host = drafts.find { |d| d.is_a?(Hash) && d[:draft_version] }
309
347
  if host
310
- host[:draft_version] = "#{joined}-#{dash[:dash_year]}"
348
+ # The draft's own numeric date embeds in the designator
349
+ # ("D9-2006", and with its month "D2-2015-04") — the
350
+ # "D2, 04 2015" comma spelling normalizes onto it.
351
+ host[:draft_version] =
352
+ [joined, dash[:dash_year], dash[:dash_month]].compact.join("-")
311
353
  dash.delete(:dash_year)
312
354
  dash.delete(:dash_month)
355
+ # the hyphenated "/D-2-2007" spelling's year the spine
356
+ # captured at top level is the draft's own — vacate it
357
+ parsed_hash.delete(:year) if parsed_hash[:year] == dash[:dash_year]
313
358
  else
314
359
  parsed_hash[:year] = dash[:dash_year]
315
360
  parsed_hash[:month] = dash[:dash_month]
@@ -511,8 +556,15 @@ module Pubid
511
556
  # @param parsed_hash [Hash] parsed data with base and supplement info
512
557
  # @return [Identifiers::Corrigendum] corrigendum identifier
513
558
  def build_corrigendum_supplement(parsed_hash)
559
+ # The `corrigendum_identifier` rule's :base subtree binds as a
560
+ # top-level sequence (an Array of capture hashes — the same shape
561
+ # #build's dispatch merges at line 24) whenever the base carries a
562
+ # multi-capture narrative tail; fold it before reconstruction or
563
+ # reconstruct_base_string's Hash indexing crashes with TypeError.
564
+ base_data = parsed_hash[:base]
565
+ base_data = merge_parsed_array(base_data) if base_data.is_a?(Array)
514
566
  # Recursively parse the base identifier reconstructed from the subtree.
515
- base = Identifier.parse(reconstruct_base_string(parsed_hash[:base]))
567
+ base = Identifier.parse(reconstruct_base_string(base_data))
516
568
 
517
569
  # Extract corrigendum attributes (parse-tree keys are cor_*; the
518
570
  # identifier stores them as the uniform `number`/`year`).
@@ -873,6 +925,10 @@ module Pubid
873
925
  end
874
926
  attributes[:parenthetical_content] ||= extract_value(parsed[:edition_marker]) if parsed[:edition_marker]
875
927
 
928
+ # A joint ISO-print redline is a distinct document of the same
929
+ # standard — same flag semantics as the generic path (pubid#215).
930
+ attributes[:redline] = true if parsed[:redline]
931
+
876
932
  # Extract edition, from relaton's "/E-<n>" suffix. The grammar's /E
877
933
  # alternative captures the bare ordinal (nil-residue hand-off item 1).
878
934
  if parsed[:edition]
@@ -1537,6 +1593,8 @@ module Pubid
1537
1593
  # Optional attributes (excluding part/subpart since they're in code)
1538
1594
  extract_optional(parsed, attributes, :edition)
1539
1595
  extract_optional(parsed, attributes, :revision)
1596
+ extract_optional(parsed, attributes, :rev_sep)
1597
+ attributes[:rev_bare] = true if parsed[:rev_bare]
1540
1598
 
1541
1599
  # Month/day - always extract if present (but not if already extracted from year)
1542
1600
  unless attributes[:edition_month]
@@ -1589,9 +1647,29 @@ module Pubid
1589
1647
  # Redline
1590
1648
  attributes[:redline] = true if parsed[:redline]
1591
1649
 
1650
+ mark_glued_month(attributes)
1592
1651
  attributes
1593
1652
  end
1594
1653
 
1654
+ # Glued identity year-month provenance (pubid#517): when the input's
1655
+ # tail is the glued "-YYYY-MM" and the identity carries no draft or
1656
+ # revision suffix, the month belongs to the code's date clause
1657
+ # ("ANSI/IEEE 21-1976-11", "IEEE Std PC2-2016-01") and the renderer
1658
+ # reproduces the glued face. The same month/year attribute shape is
1659
+ # produced by trailing print dates (", 05 2014" — whose numeric form
1660
+ # is its own documented law) and by dash-promoted draft tails
1661
+ # ("/R-x-2011-04"), both of which keep the comma face: the input
1662
+ # shape, checked here at the single choke point every assignment
1663
+ # path converges to, is what distinguishes them (the AIEE
1664
+ # date_separator precedent).
1665
+ def mark_glued_month(attributes)
1666
+ return unless attributes[:month] && attributes[:year] &&
1667
+ attributes[:draft].nil? && attributes[:revision].nil? &&
1668
+ original_input.to_s.match?(/-\d{4}-\d{2}\z/)
1669
+
1670
+ attributes[:month_glued] = true
1671
+ end
1672
+
1595
1673
  # Determine stage abbreviation for TYPED_STAGE lookup
1596
1674
  # @param type_value [String] the type value (e.g., "Std", "P")
1597
1675
  # @param draft_status_value [String] the draft status (e.g., "Unapproved")
@@ -1632,8 +1710,9 @@ module Pubid
1632
1710
 
1633
1711
  if version
1634
1712
  # relaton's hyphenated "/D-2" spelling captures the dash inside
1635
- # draft_version; the stage registry keys on "D2".
1636
- draft_abbr = "D#{version.to_s.sub(/\A-/, "")}"
1713
+ # draft_version; the stage registry keys on "D2". A date-embedded
1714
+ # version ("3-2017-07") keys on its leading ordinal.
1715
+ draft_abbr = "D#{version.to_s.sub(/\A-/, "").sub(/-\d{4}(-\d{2})?\z/, "")}"
1637
1716
  # Check if this specific draft stage is in registry
1638
1717
  stage = Pubid::Ieee.locate_stage(draft_abbr)
1639
1718
  return draft_abbr if stage&.abbr&.include?(draft_abbr)
@@ -1773,6 +1852,18 @@ module Pubid
1773
1852
  year = extract_value(draft_data[:year]) if draft_data[:year]
1774
1853
  day = extract_value(draft_data[:day]) if draft_data[:day]
1775
1854
 
1855
+ # A REV-carrying draft embeds its print date into the designator
1856
+ # with the month canonicalized numeric (pubid ruling 2026-10-14):
1857
+ # "P802.11-REVmc/D8.0, Aug 2016" renders ".../D8.0-2016-08".
1858
+ # Drafts without a REV keep the comma face.
1859
+ if (attributes[:revision] || attributes[:rev_bare]) && month && year
1860
+ number = REV_MONTH_NUMBERS[month.to_s.delete(".")]
1861
+ if number && version
1862
+ version = "#{version}-#{year}-#{number}"
1863
+ month = year = day = nil
1864
+ end
1865
+ end
1866
+
1776
1867
  # Detect comma before month and space before draft by checking the original input
1777
1868
  if original_input
1778
1869
  if month
@@ -36,8 +36,20 @@ module Pubid
36
36
  # "Rev<id>". `::Pubid::Identifier` has no `revision` attribute, so this is
37
37
  # free of the number/stage multi-flavor collision landmine.
38
38
  attribute :revision, :string
39
+ # The lettered revision's printed separator ("P802.11-REVmc" keeps its
40
+ # dash; a glued "REVi" stays glued) and the bare-word marker (no id
41
+ # after "Rev" — "802.1AR-Rev/D2.2" keeps "Rev"). pubid ruling
42
+ # 2026-10-14.
43
+ attribute :rev_sep, :string
44
+ attribute :rev_bare, :boolean
39
45
  attribute :edition, :string # Edition 1.0
40
46
  attribute :month, :string
47
+ # Provenance flag (pubid#517): the month arrived glued to the identity
48
+ # year on the code ("ANSI/IEEE 21-1976-11", grammar
49
+ # date_with_month_numeric) rather than as a trailing ", 11 1976"
50
+ # print date. Serialization round-trips it so to_s reproduces the
51
+ # glued face.
52
+ attribute :month_glued, :boolean, default: -> { false }
41
53
  attribute :day, :string
42
54
  attribute :redline, :boolean, default: -> { false }
43
55
  attribute :amendments, Identifier, collection: true # Amendment identifiers
@@ -210,7 +222,7 @@ module Pubid
210
222
  # (renderer.rb's `"Edition #{edition} #{year}-#{edition_month}"`),
211
223
  # which survived even though its sibling `year` is already stripped.
212
224
  def self.all_parts_edition_keys
213
- super + %i[revision reaffirmed edition_month]
225
+ super + %i[revision rev_sep rev_bare reaffirmed edition_month]
214
226
  end
215
227
 
216
228
  # Inverse of the to_hash compaction: expand a scalar `stage` back into the
@@ -235,6 +247,7 @@ module Pubid
235
247
  Identifiers::Nesc::Draft,
236
248
  Identifiers::Nesc::Edition,
237
249
  Identifiers::Nesc::Handbook,
250
+ Identifiers::Nesc::Preprint,
238
251
  Identifiers::Nesc::Redline,
239
252
  Identifiers::Nesc::Standard,
240
253
  ]
@@ -177,6 +177,9 @@ module Pubid
177
177
  if parenthetical_content&.match?(%r{\A[A-Z](?:\s*[/&]\s*[A-Z])*\z})
178
178
  result += " (#{parenthetical_content})"
179
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
180
183
 
181
184
  result
182
185
  end
@@ -250,6 +253,7 @@ module Pubid
250
253
  result = parts.join(" ")
251
254
  result += "-#{year}" if year && !@designator_carries_year &&
252
255
  !ieee_draft.to_s.end_with?("-#{year}")
256
+ result += " - Redline" if redline
253
257
 
254
258
  result
255
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
@@ -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")
@@ -187,14 +187,26 @@ module Pubid
187
187
  # no draft at all, or a draft that carries its OWN date (the
188
188
  # draft+corrigendum family: "PC37.09-1999/D7.2, Nov 2006" -
189
189
  # the 1999 is the base's year, the Nov 2006 the draft's).
190
- year_on_code = id.year && !id.edition && !id.month &&
190
+ # A numeric identity month is the SHORT format: it keeps the
191
+ # year on the code and appends itself dash-joined, whatever the
192
+ # input spelling ("ANSI/IEEE 21-1976-11" and "IEEE P15026.1,
193
+ # 05 2014" both render glued - pubid#517). The comma face is
194
+ # the LONG format, a month name ("..., May 2014").
195
+ glued_month = id.month.to_s.match?(/\A\d{1,2}\z/)
196
+ year_on_code = id.year && !id.edition &&
197
+ (!id.month || glued_month) &&
191
198
  (!id.draft_obj || id.draft_obj&.year)
192
199
  result += "-#{id.year}" if year_on_code
200
+ result += "-#{id.month}" if glued_month
193
201
 
194
202
  # Append the numbered/lettered revision inline, in IEEE's native
195
203
  # position — right after the code number and before the draft
196
- # ("P802.16Rev2/D3"). Keeps a revision distinct from its base standard.
197
- result += "Rev#{id.revision}" if id.revision
204
+ # ("P802.16Rev2/D3"). Keeps a revision distinct from its base
205
+ # standard. The lettered id keeps its printed separator
206
+ # ("P802.11-REVmc" renders "P802.11-Revmc"); the bare word with
207
+ # no id still prints "Rev" (pubid ruling 2026-10-14).
208
+ result += "#{id.rev_sep}Rev#{id.revision}" if id.revision
209
+ result += "#{id.rev_sep}Rev" if id.rev_bare
198
210
 
199
211
  # Append draft to code - with or without space based on original
200
212
  # format. An exactly-IEC/IEEE co-published reference prints only the
@@ -263,7 +275,8 @@ module Pubid
263
275
  # An exactly-IEC/IEEE co-published reference drops the comma date
264
276
  # entirely (same catalogue-metadata rule as the draft date above):
265
277
  # "IEC/IEEE P60076-16, May 2016" prints as "IEC/IEEE 60076-16".
266
- if id.month && !(id.publisher == "IEC" && id.copublisher == ["IEEE"])
278
+ if id.month && !glued_month &&
279
+ !(id.publisher == "IEC" && id.copublisher == ["IEEE"])
267
280
  result += ", #{id.month}"
268
281
  result += " #{id.day}" if id.day
269
282
  if id.year && !id.edition
@@ -40,6 +40,13 @@ module Pubid
40
40
  parts << "int"
41
41
  end
42
42
 
43
+ # A NESC preprint is a draft-stage document distinct from the
44
+ # published edition it precedes — without this component it would
45
+ # share the edition's URN (urn:ieee:ieee:C2:2012).
46
+ if identifier.is_a?(Identifiers::Nesc::Preprint)
47
+ parts << "preprint"
48
+ end
49
+
43
50
  # Conformance is always a ConformanceIdentifier wrapper (special_type
44
51
  # "conformance"), never inline on a base standard — the base no longer
45
52
  # carries a conf_number attribute, so there is no inline conf part.
@@ -138,7 +138,11 @@ module Pubid
138
138
  # Two-letter suffixes: Ur (Unclassified Revised), Ua (Unclassified Amended), Ub-Uj (series variants)
139
139
  # Single letter: any letter not followed by excluded keywords
140
140
  rule(:number_suffix) do
141
- (str("U") >> lower_letter) | (match("[a-zA-Z]") >> (
141
+ (str("U") >> lower_letter) |
142
+ # An "r" directly followed by a digit is the revision marker (pubid#170),
143
+ # not a suffix — leave it for the second_number/edition rules.
144
+ # The guard sits before the letter so it tests the suffix start itself.
145
+ ((str("r") >> digit).absent? >> match("[a-zA-Z]") >> (
142
146
  # Match suffixes
143
147
  str("ec") |
144
148
  str("ndex") |
@@ -265,7 +269,9 @@ module Pubid
265
269
  (digits >> str("r") >> digits >> match("[a-zA-Z]")) |
266
270
  # NEW: Revision pattern with year (e.g., "126r2013")
267
271
  # This handles SP revision format where revision is attached to second_number
268
- (digits.as(:number_only) >> str("r") >> digits.as(:edition_id)) |
272
+ # Dotted minor accepted atomically (pubid#170): "53r4.1" binds id "4.1"
273
+ (digits.as(:number_only) >> str("r") >>
274
+ (digits >> (str(".") >> digits).maybe).as(:edition_id)) |
269
275
  # CRPL range with underscore (e.g., "2_3-1A")
270
276
  (digits >> str("_") >> digits >> dash >> digits >> upper_letter.maybe) |
271
277
  # Letter followed by dash and digits (e.g., "m-5")
@@ -283,7 +289,9 @@ module Pubid
283
289
  # For patterns like NIST SP 800-27ra where revision 'ra' is attached directly to number
284
290
  (str("r") >> match("[a-zA-Z]")).as(:revision_letter_suffix) |
285
291
  # NEW: Simple revision pattern r followed by digits (e.g., "r1", "r2") for trailing revision
286
- (str("r") >> digits.as(:edition_id)).as(:revision_simple) |
292
+ # Dotted minor accepted atomically (pubid#170): "r4.1" binds id "4.1"
293
+ (str("r") >>
294
+ (digits >> (str(".") >> digits).maybe).as(:edition_id)).as(:revision_simple) |
287
295
  # Special patterns like "NCNR", "PERMIS", "BFRL"
288
296
  str("NCNR") | str("PERMIS") | str("BFRL") |
289
297
  # Just capital letters (e.g., "A", "B", "C") - standalone
@@ -303,15 +311,22 @@ module Pubid
303
311
  # Enhanced: Support space-separated format from preprocessing (r1 separated from number)
304
312
  rule(:edition) do
305
313
  # Edition with "e" prefix: e2, e3, e2021 (1-4 digits for ID)
306
- (space.maybe >> str("e") >> digits.as(:edition_id)).as(:edition_e) |
314
+ # Dotted minor accepted atomically (pubid#170): "e2006.1" binds id "2006.1"
315
+ (space.maybe >> str("e") >>
316
+ (digits >> (str(".") >> digits).maybe).as(:edition_id)).as(:edition_e) |
307
317
  # Revision with "r" prefix and SPACE, with letter: r 5A (preserve format)
308
- (space >> str("r") >> digits.as(:edition_id) >> match("[a-zA-Z]").as(:edition_letter)).as(:edition_r_with_space_letter) |
318
+ # Dotted minor accepted atomically (pubid#170)
319
+ (space >> str("r") >> (digits >> (str(".") >> digits).maybe).as(:edition_id) >>
320
+ match("[a-zA-Z]").as(:edition_letter)).as(:edition_r_with_space_letter) |
309
321
  # Revision with "r" prefix and SPACE: r 5 (preserve format)
310
- (space >> str("r") >> digits.as(:edition_id)).as(:edition_r_with_space) |
322
+ (space >> str("r") >>
323
+ (digits >> (str(".") >> digits).maybe).as(:edition_id)).as(:edition_r_with_space) |
311
324
  # Revision with "r" prefix NO space, with letter: r5A (compact format)
312
- (str("r") >> digits.as(:edition_id) >> match("[a-zA-Z]").as(:edition_letter)).as(:edition_r_no_space_letter) |
325
+ (str("r") >> (digits >> (str(".") >> digits).maybe).as(:edition_id) >>
326
+ match("[a-zA-Z]").as(:edition_letter)).as(:edition_r_no_space_letter) |
313
327
  # Revision with "r" prefix NO space: r5 (compact format)
314
- (str("r") >> digits.as(:edition_id)).as(:edition_r_no_space) |
328
+ (str("r") >>
329
+ (digits >> (str(".") >> digits).maybe).as(:edition_id)).as(:edition_r_no_space) |
315
330
  # Revision with "rev" prefix (verbose): rev2013, rev 2013
316
331
  (space.maybe >> str("rev") >> space.maybe >> digits.as(:edition_id)).as(:edition_rev) |
317
332
  # Historical with "-" prefix: -2, -3 (ONLY if followed by non-digit or end)
@@ -76,6 +76,7 @@ module Pubid
76
76
  normalize_supplement_and_part!
77
77
  normalize_version_notation!
78
78
  normalize_edition_year_suffix!
79
+ normalize_glued_dotted_edition!
79
80
  normalize_revision_with_letter!
80
81
  normalize_version_dotted_spaces!
81
82
  normalize_update_markers!
@@ -264,10 +265,24 @@ module Pubid
264
265
  end
265
266
 
266
267
  # Edition year suffix shorthand: "2006ed." → "e2006".
268
+ # A dotted minor after the dot stays attached (pubid#170): "2006ed.1" → "e2006.1",
269
+ # so the edition id binds atomically instead of corrupting into "e20061".
267
270
  def normalize_edition_year_suffix!
271
+ @cleaned = @cleaned.gsub(/(\d{4})ed\.(\d+)/, 'e\1.\2')
268
272
  @cleaned = @cleaned.gsub(/(\d{4})ed\./, 'e\1')
269
273
  end
270
274
 
275
+ # A glued dotted e-edition needs the space the spaced form has (pubid#170):
276
+ # "800-53e2.1" → "800-53 e2.1", so the edition rule binds "2.1" atomically.
277
+ # The minor is 1-2 digits — a 4-digit tail is the additional-text year of
278
+ # forms like "NBS CIRC 11e2.1915", which must keep its existing tree.
279
+ # Only dotted e-editions are unglued; the dot-less glued form "800-53e2"
280
+ # keeps its number-bound tree. Mirrors the "v1.1" unglue in
281
+ # normalize_draft_and_volume!.
282
+ def normalize_glued_dotted_edition!
283
+ @cleaned = @cleaned.gsub(/(\d)e(\d+\.\d{1,2})(?!\d)/, '\1 e\2')
284
+ end
285
+
271
286
  # Revision attached to a number with optional letter suffix. When
272
287
  # a letter suffix is present, keep them together for the
273
288
  # second_number grammar rule; otherwise insert a space before
data/lib/pubid/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pubid
4
- VERSION = "2.0.0.pre.alpha.28"
4
+ VERSION = "2.0.0.pre.alpha.30"
5
5
  end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pubid
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.pre.alpha.28
4
+ version: 2.0.0.pre.alpha.30
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-08 00:00:00.000000000 Z
11
+ date: 2026-10-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lutaml-model
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '0.8'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: '0.8'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: parslet
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -484,6 +484,8 @@ files:
484
484
  - lib/pubid/evs/builder.rb
485
485
  - lib/pubid/evs/identifier.rb
486
486
  - lib/pubid/evs/identifiers.rb
487
+ - lib/pubid/evs/identifiers/estonian_standard.rb
488
+ - lib/pubid/evs/identifiers/international_adoption.rb
487
489
  - lib/pubid/evs/identifiers/national_adoption.rb
488
490
  - lib/pubid/evs/parser.rb
489
491
  - lib/pubid/evs/renderer.rb
@@ -640,6 +642,7 @@ files:
640
642
  - lib/pubid/ieee/identifiers/nesc/draft.rb
641
643
  - lib/pubid/ieee/identifiers/nesc/edition.rb
642
644
  - lib/pubid/ieee/identifiers/nesc/handbook.rb
645
+ - lib/pubid/ieee/identifiers/nesc/preprint.rb
643
646
  - lib/pubid/ieee/identifiers/nesc/redline.rb
644
647
  - lib/pubid/ieee/identifiers/nesc/standard.rb
645
648
  - lib/pubid/ieee/identifiers/parenthetical_identifier.rb