pubid 2.0.0.pre.alpha.29 → 2.0.0.pre.alpha.31

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,7 +298,29 @@ 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] }
290
- if dash && parsed_hash[:year].nil?
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
319
+ # The draft's own dash date embeds onto the draft even when a
320
+ # base identity year is present ("…-2011/D4-2015-07": the 2011
321
+ # is the base's year, the 2015-07 the draft's); only the
322
+ # root-year assignment waits for an unoccupied year slot.
323
+ if dash
291
324
  joined = drafts.filter_map { |d| d.is_a?(Hash) ? d[:draft_version] : nil }
292
325
  .map { |v| v.is_a?(Hash) ? v[:value] : v.to_s }.join
293
326
  bare_joint_draft = parsed_hash[:joint_publishers] &&
@@ -297,7 +330,16 @@ module Pubid
297
330
  dash_led = joined.start_with?("-")
298
331
  if bare_joint_draft && dash_led
299
332
  parsed_hash[:part] = { value: dash[:dash_year] }
300
- elsif dash[:dash_month] || dash_led
333
+ elsif dash[:dash_month] &&
334
+ (host = drafts.find { |d| d.is_a?(Hash) && d[:draft_version] })
335
+ # The draft's own numeric date embeds in the designator
336
+ # ("D2-2015-04") — the "D2, 04 2015" comma spelling
337
+ # normalizes onto it (pubid ruling 2026-10-13).
338
+ host[:draft_version] =
339
+ [joined, dash[:dash_year], dash[:dash_month]].compact.join("-")
340
+ dash.delete(:dash_year)
341
+ dash.delete(:dash_month)
342
+ elsif (dash[:dash_month] || dash_led) && parsed_hash[:year].nil?
301
343
  parsed_hash[:year] = dash[:dash_year]
302
344
  parsed_hash[:month] = dash[:dash_month]
303
345
  else
@@ -307,9 +349,16 @@ module Pubid
307
349
  # draft version misses the D-registry), no separate year.
308
350
  host = drafts.find { |d| d.is_a?(Hash) && d[:draft_version] }
309
351
  if host
310
- host[:draft_version] = "#{joined}-#{dash[:dash_year]}"
352
+ # The draft's own numeric date embeds in the designator
353
+ # ("D9-2006", and with its month "D2-2015-04") — the
354
+ # "D2, 04 2015" comma spelling normalizes onto it.
355
+ host[:draft_version] =
356
+ [joined, dash[:dash_year], dash[:dash_month]].compact.join("-")
311
357
  dash.delete(:dash_year)
312
358
  dash.delete(:dash_month)
359
+ # the hyphenated "/D-2-2007" spelling's year the spine
360
+ # captured at top level is the draft's own — vacate it
361
+ parsed_hash.delete(:year) if parsed_hash[:year] == dash[:dash_year]
313
362
  else
314
363
  parsed_hash[:year] = dash[:dash_year]
315
364
  parsed_hash[:month] = dash[:dash_month]
@@ -910,12 +959,23 @@ module Pubid
910
959
  # exempt: their canonical is the ISO face, which prints the
911
960
  # date from the identity slots (", March 2017", pubid#216).
912
961
  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/}, "")
962
+ dm = extract_value(parsed[:draft_month]).to_s
963
+ dy = extract_value(parsed[:draft_year]).to_s
964
+ num = REV_MONTH_NUMBERS[dm.delete(".")]
965
+ if attributes[:publishers] == %w[IEEE CSA] && num && !dy.empty?
966
+ # The joint IEEE/CSA drafts glue their date into the
967
+ # designator with the month numeric (pubid ruling
968
+ # 2026-10-14: no comma dates on these rows).
969
+ draft = Components::Draft.new(version: "#{draft_ver}-#{dy}-#{num}")
970
+ attributes[:draft] = draft.to_s.sub(%r{\A/}, "")
971
+ else
972
+ draft = Components::Draft.new(
973
+ version: draft_ver,
974
+ month: dm,
975
+ year: dy)
976
+ draft.comma_before_month = true
977
+ attributes[:draft] = draft.to_s.sub(%r{\A/}, "")
978
+ end
919
979
  attributes.delete(:year) if parsed[:year].nil?
920
980
  attributes.delete(:month) if parsed[:month].nil?
921
981
  elsif parsed[:draft_year] && parsed[:iso_stage].nil?
@@ -926,6 +986,19 @@ module Pubid
926
986
  draft_year = extract_value(parsed[:draft_year]).to_s.sub(/\A[, ]+/, "")
927
987
  attributes[:draft] = "D#{draft_ver}, #{draft_year}"
928
988
  attributes.delete(:year) if parsed[:year].nil?
989
+ elsif attributes[:publishers] == %w[IEEE CSA] &&
990
+ parsed[:iso_stage].nil? &&
991
+ extract_value(parsed[:year]).to_s.match?(/\A(19|20)\d\d\z/) &&
992
+ extract_value(parsed[:month]).to_s.match?(/\A\d{1,2}\z/)
993
+ # The glued face re-parses with the date in the identity
994
+ # slots ("D2-2016-09" -> year/month + a bare D2): it
995
+ # normalizes back onto the draft-embedded date so the
996
+ # round trip closes on the CSA canonical.
997
+ draft = Components::Draft.new(
998
+ version: "#{draft_ver}-#{extract_value(parsed[:year])}-#{extract_value(parsed[:month])}")
999
+ attributes[:draft] = draft.to_s.sub(%r{\A/}, "")
1000
+ attributes.delete(:year)
1001
+ attributes.delete(:month)
929
1002
  else
930
1003
  attributes[:ieee_draft] = "D#{draft_ver}"
931
1004
  end
@@ -1548,6 +1621,8 @@ module Pubid
1548
1621
  # Optional attributes (excluding part/subpart since they're in code)
1549
1622
  extract_optional(parsed, attributes, :edition)
1550
1623
  extract_optional(parsed, attributes, :revision)
1624
+ extract_optional(parsed, attributes, :rev_sep)
1625
+ attributes[:rev_bare] = true if parsed[:rev_bare]
1551
1626
 
1552
1627
  # Month/day - always extract if present (but not if already extracted from year)
1553
1628
  unless attributes[:edition_month]
@@ -1663,8 +1738,9 @@ module Pubid
1663
1738
 
1664
1739
  if version
1665
1740
  # relaton's hyphenated "/D-2" spelling captures the dash inside
1666
- # draft_version; the stage registry keys on "D2".
1667
- draft_abbr = "D#{version.to_s.sub(/\A-/, "")}"
1741
+ # draft_version; the stage registry keys on "D2". A date-embedded
1742
+ # version ("3-2017-07") keys on its leading ordinal.
1743
+ draft_abbr = "D#{version.to_s.sub(/\A-/, "").sub(/-\d{4}(-\d{2})?\z/, "")}"
1668
1744
  # Check if this specific draft stage is in registry
1669
1745
  stage = Pubid::Ieee.locate_stage(draft_abbr)
1670
1746
  return draft_abbr if stage&.abbr&.include?(draft_abbr)
@@ -1802,8 +1878,25 @@ module Pubid
1802
1878
  month_slice = draft_data[:month]
1803
1879
  month = extract_value(month_slice) if month_slice
1804
1880
  year = extract_value(draft_data[:year]) if draft_data[:year]
1881
+ # The apostrophe-year catalogue convention ("P1484.20.1/D7,
1882
+ # May 07"): the 2-digit year expands with the <30 pivot so the
1883
+ # hash carries an unambiguous year.
1884
+ year = "#{year.to_i < 30 ? 20 : 19}#{year}" if year.to_s.match?(/\A\d{2}\z/)
1805
1885
  day = extract_value(draft_data[:day]) if draft_data[:day]
1806
1886
 
1887
+ # A REV-carrying draft embeds its print date into the designator
1888
+ # with the month canonicalized numeric (pubid ruling 2026-10-14):
1889
+ # "P802.11-REVmc/D8.0, Aug 2016" renders ".../D8.0-2016-08".
1890
+ # Drafts without a REV keep the comma face.
1891
+ if (attributes[:revision] || attributes[:rev_bare] ||
1892
+ attributes[:cor_number] || attributes[:cor_year]) && month && year
1893
+ number = REV_MONTH_NUMBERS[month.to_s.delete(".")]
1894
+ if number && version
1895
+ version = "#{version}-#{year}-#{number}"
1896
+ month = year = day = nil
1897
+ end
1898
+ end
1899
+
1807
1900
  # Detect comma before month and space before draft by checking the original input
1808
1901
  if original_input
1809
1902
  if month
@@ -1931,8 +2024,17 @@ module Pubid
1931
2024
  end
1932
2025
 
1933
2026
  if param_data[:revision_of]
1934
- attributes[:revision_of] =
1935
- extract_value(param_data[:revision_of])
2027
+ target = extract_value(param_data[:revision_of]).to_s.strip
2028
+ begin
2029
+ # The model types revision_of as an Identifier (the printed
2030
+ # target parses as one); feeding the raw string crashes
2031
+ # to_hash with Lutaml::Model::IncorrectModelError.
2032
+ attributes[:revision_of] = Identifier.parse(target)
2033
+ rescue Pubid::Errors::ParseError, StandardError
2034
+ # An unparseable printed target stays on the parenthetical
2035
+ # surface instead of poisoning the hash.
2036
+ attributes[:parenthetical_content] ||= target
2037
+ end
1936
2038
  end
1937
2039
  if param_data[:amendment_to]
1938
2040
  attributes[:amendment_to] =
@@ -36,6 +36,12 @@ 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
41
47
  # Provenance flag (pubid#517): the month arrived glued to the identity
@@ -216,7 +222,7 @@ module Pubid
216
222
  # (renderer.rb's `"Edition #{edition} #{year}-#{edition_month}"`),
217
223
  # which survived even though its sibling `year` is already stripped.
218
224
  def self.all_parts_edition_keys
219
- super + %i[revision reaffirmed edition_month]
225
+ super + %i[revision rev_sep rev_bare reaffirmed edition_month]
220
226
  end
221
227
 
222
228
  # Inverse of the to_hash compaction: expand a scalar `stage` back into the
@@ -1273,6 +1273,43 @@ module Pubid
1273
1273
  cleaned = cleaned.gsub("&amp;amp;", "&") # Double-encoded ampersand
1274
1274
  cleaned = cleaned.gsub("&amp;", "&") # Single-encoded ampersand
1275
1275
 
1276
+ # The joint IEEE/CSA catalogue spells the type word before the
1277
+ # project number ("IEEE/CSA Draft P844.1/D2, Sept, 2016"): the P
1278
+ # already carries the project semantics (docs/IEEE-DRAFT-STAGES.md
1279
+ # §1.1), so the redundant word drops and the joint route parses.
1280
+ cleaned = cleaned.gsub(%r{IEEE/CSA Draft (P[A-Z0-9])}, 'IEEE/CSA \1')
1281
+
1282
+ # The corrigendum's own draft follows the ordinal after a comma
1283
+ # ("P11073-10418-2011/Cor 1, D4, July 2015"): re-spell the comma as
1284
+ # the draft separator so the cor route's draft slot reaches it.
1285
+ cleaned = cleaned.gsub(%r{/Cor(?:rigendum)? (\d+), (D[A-Z0-9])}, '/Cor \1/\2')
1286
+
1287
+ # The corrigendum's own draft rides before the ordinal in the
1288
+ # house order ("PC37.101-2006/D2, 2007/Cor. 1"): the catalogue
1289
+ # spellings "…/Cor 1, D4, July 2015" and "…/Cor 1-201x/D5,
1290
+ # April 2012" re-order into it, with the draft's date glued
1291
+ # -YYYY-MM (pubid ruling 2026-10-14: no comma dates).
1292
+ cleaned = cleaned.gsub(
1293
+ %r{/Cor(?:rigendum)? (\d+(?:-[0-9xX]{4})?)/(D[A-Z0-9][^,]*), ([A-Za-z]+) (\d{4})}
1294
+ ) do
1295
+ month = ::Pubid::Ieee::Builder::REV_MONTH_NUMBERS[Regexp.last_match(3).capitalize]
1296
+ if month
1297
+ "/#{Regexp.last_match(2)}-#{Regexp.last_match(4)}-#{month}/Cor. #{Regexp.last_match(1)}"
1298
+ else
1299
+ Regexp.last_match(0)
1300
+ end
1301
+ end
1302
+ cleaned = cleaned.gsub(
1303
+ %r{/Cor(?:rigendum)? (\d+), (D[A-Z0-9][^,]*), ([A-Za-z]+) (\d{4})}
1304
+ ) do
1305
+ month = ::Pubid::Ieee::Builder::REV_MONTH_NUMBERS[Regexp.last_match(3).capitalize]
1306
+ if month
1307
+ "/#{Regexp.last_match(2)}-#{Regexp.last_match(4)}-#{month}/Cor. #{Regexp.last_match(1)}"
1308
+ else
1309
+ Regexp.last_match(0)
1310
+ end
1311
+ end
1312
+
1276
1313
  # NEW Phase 1: Fix number spacing issues (e.g., "C57.1 2.25" → "C57.12.25")
1277
1314
  # This handles cases where a space appears in the middle of a number
1278
1315
  cleaned = cleaned.gsub(/(\d+\.\d+)\s+(\d+\.)/, '\1\2')
@@ -195,14 +195,19 @@ module Pubid
195
195
  glued_month = id.month.to_s.match?(/\A\d{1,2}\z/)
196
196
  year_on_code = id.year && !id.edition &&
197
197
  (!id.month || glued_month) &&
198
- (!id.draft_obj || id.draft_obj&.year)
198
+ (!id.draft_obj || id.draft_obj&.year ||
199
+ id.draft_obj&.version.to_s.match?(/-\d{4}-\d{2}\z/))
199
200
  result += "-#{id.year}" if year_on_code
200
201
  result += "-#{id.month}" if glued_month
201
202
 
202
203
  # Append the numbered/lettered revision inline, in IEEE's native
203
204
  # position — right after the code number and before the draft
204
- # ("P802.16Rev2/D3"). Keeps a revision distinct from its base standard.
205
- result += "Rev#{id.revision}" if id.revision
205
+ # ("P802.16Rev2/D3"). Keeps a revision distinct from its base
206
+ # standard. The lettered id keeps its printed separator
207
+ # ("P802.11-REVmc" renders "P802.11-Revmc"); the bare word with
208
+ # no id still prints "Rev" (pubid ruling 2026-10-14).
209
+ result += "#{id.rev_sep}Rev#{id.revision}" if id.revision
210
+ result += "#{id.rev_sep}Rev" if id.rev_bare
206
211
 
207
212
  # Append draft to code - with or without space based on original
208
213
  # format. An exactly-IEC/IEEE co-published reference prints only the
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.29"
4
+ VERSION = "2.0.0.pre.alpha.31"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pubid
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.pre.alpha.29
4
+ version: 2.0.0.pre.alpha.31
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-12 00:00:00.000000000 Z
11
+ date: 2026-10-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lutaml-model
@@ -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