pubid 2.0.0.pre.alpha.6 → 2.0.0.pre.alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +40 -0
  3. data/lib/pubid/amca.rb +6 -0
  4. data/lib/pubid/ansi.rb +7 -0
  5. data/lib/pubid/api.rb +5 -0
  6. data/lib/pubid/ashrae.rb +6 -0
  7. data/lib/pubid/asme.rb +5 -0
  8. data/lib/pubid/astm.rb +5 -0
  9. data/lib/pubid/bsi.rb +15 -0
  10. data/lib/pubid/ccsds.rb +5 -0
  11. data/lib/pubid/cen_cenelec.rb +7 -0
  12. data/lib/pubid/cie.rb +5 -0
  13. data/lib/pubid/csa.rb +5 -0
  14. data/lib/pubid/etsi.rb +7 -0
  15. data/lib/pubid/iala/builder.rb +62 -6
  16. data/lib/pubid/iala/identifier.rb +11 -7
  17. data/lib/pubid/iala/identifiers/advice.rb +15 -0
  18. data/lib/pubid/iala/identifiers/annex.rb +55 -0
  19. data/lib/pubid/iala/identifiers/general_assembly.rb +16 -0
  20. data/lib/pubid/iala/identifiers/letter.rb +16 -0
  21. data/lib/pubid/iala/identifiers.rb +12 -8
  22. data/lib/pubid/iala/parser.rb +39 -8
  23. data/lib/pubid/iala/renderer.rb +14 -0
  24. data/lib/pubid/iala/urn_generator.rb +17 -0
  25. data/lib/pubid/iala.rb +5 -0
  26. data/lib/pubid/idf.rb +5 -0
  27. data/lib/pubid/iec.rb +8 -0
  28. data/lib/pubid/ieee.rb +8 -0
  29. data/lib/pubid/iho.rb +5 -0
  30. data/lib/pubid/iso.rb +6 -0
  31. data/lib/pubid/itu.rb +5 -0
  32. data/lib/pubid/jcgm.rb +5 -0
  33. data/lib/pubid/jis.rb +6 -0
  34. data/lib/pubid/nist.rb +13 -0
  35. data/lib/pubid/oiml/builder.rb +33 -0
  36. data/lib/pubid/oiml/identifier.rb +1 -0
  37. data/lib/pubid/oiml/identifiers/bulletin.rb +101 -0
  38. data/lib/pubid/oiml/identifiers.rb +1 -0
  39. data/lib/pubid/oiml/parser.rb +43 -1
  40. data/lib/pubid/oiml/renderer.rb +51 -0
  41. data/lib/pubid/oiml/urn_generator.rb +22 -0
  42. data/lib/pubid/oiml.rb +5 -0
  43. data/lib/pubid/plateau.rb +5 -0
  44. data/lib/pubid/prefixes_support.rb +51 -0
  45. data/lib/pubid/sae.rb +6 -0
  46. data/lib/pubid/version.rb +1 -1
  47. data/lib/pubid.rb +68 -0
  48. metadata +7 -1
data/lib/pubid/ieee.rb CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  module Pubid
4
4
  module Ieee
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # Publisher prefix. The private PreParser::PUBLISHERS list also holds adopted
8
+ # co-org names (ANSI, AIEE, ASA, ...) that are not leading routing tokens for
9
+ # an IEEE reference, so they are deliberately excluded. The joint ISO/IEC/IEEE
10
+ # form comes from Pubid::JOINT_PREFIXES.
11
+ PREFIXES = ["IEEE"].freeze
12
+
5
13
  autoload :Aiee, "#{__dir__}/ieee/aiee"
6
14
  autoload :Builder, "#{__dir__}/ieee/builder"
7
15
  autoload :Identifier, "#{__dir__}/ieee/identifier"
data/lib/pubid/iho.rb CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  module Pubid
4
4
  module Iho
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # Sole IHO publisher token (see the parser grammar).
8
+ PREFIXES = ["IHO"].freeze
9
+
5
10
  autoload :Builder, "#{__dir__}/iho/builder"
6
11
  autoload :Identifier, "#{__dir__}/iho/identifier"
7
12
  autoload :Identifiers, "#{__dir__}/iho/identifiers"
data/lib/pubid/iso.rb CHANGED
@@ -4,6 +4,12 @@ require "lutaml/model"
4
4
 
5
5
  module Pubid
6
6
  module Iso
7
+ extend Pubid::PrefixesSupport
8
+
9
+ # Sole ISO publisher token. Joint / co-publication forms (ISO/IEC, IEC/ISO,
10
+ # ISO/IEC/IEEE) are contributed symmetrically via Pubid::JOINT_PREFIXES.
11
+ PREFIXES = ["ISO"].freeze
12
+
7
13
  autoload :Builder, "#{__dir__}/iso/builder"
8
14
  autoload :BundledIdentifier, "#{__dir__}/iso/bundled_identifier"
9
15
  autoload :CombinedIdentifier, "#{__dir__}/iso/combined_identifier"
data/lib/pubid/itu.rb CHANGED
@@ -4,6 +4,11 @@ require "lutaml/model"
4
4
 
5
5
  module Pubid
6
6
  module Itu
7
+ extend Pubid::PrefixesSupport
8
+
9
+ # Sole ITU publisher token (see the parser's `itu_prefix` rule).
10
+ PREFIXES = ["ITU"].freeze
11
+
7
12
  autoload :Builder, "#{__dir__}/itu/builder"
8
13
  autoload :Components, "#{__dir__}/itu/components"
9
14
  autoload :Identifier, "#{__dir__}/itu/identifier"
data/lib/pubid/jcgm.rb CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  module Pubid
4
4
  module Jcgm
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # Sole JCGM publisher token (see the parser's `publisher` rule).
8
+ PREFIXES = ["JCGM"].freeze
9
+
5
10
  autoload :Builder, "#{__dir__}/jcgm/builder"
6
11
  autoload :Components, "#{__dir__}/jcgm/components"
7
12
  autoload :Identifier, "#{__dir__}/jcgm/identifier"
data/lib/pubid/jis.rb CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  module Pubid
4
4
  module Jis
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # Sole JIS publisher token (see the parser's `jis_prefix` rule). Document
8
+ # types (TR/TS) follow the JIS prefix and are excluded.
9
+ PREFIXES = ["JIS"].freeze
10
+
5
11
  autoload :Builder, "#{__dir__}/jis/builder"
6
12
  autoload :Identifier, "#{__dir__}/jis/identifier"
7
13
  autoload :Identifiers, "#{__dir__}/jis/identifiers"
data/lib/pubid/nist.rb CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  module Pubid
4
4
  module Nist
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # Publisher prefixes (NBS/NIST) plus the "simple series" tokens that can
8
+ # *begin* a printed reference on their own — mirrors the parser's
9
+ # `publisher` and `simple_series` rules (lib/pubid/nist/parser.rb). Compound
10
+ # series that only appear glued to a publisher (e.g. "NBS CRPL-F-A") are
11
+ # excluded because they never lead a routable reference by themselves.
12
+ PREFIXES = %w[
13
+ NIST NBS
14
+ AMS VTS BSS BMS BH FIPS GCR HB MONO MP NCSTAR NSRDS IR SP TN CSWP
15
+ AI CIRC CS CSM CRPL LCIRC OWMWP PC RPT SIBS TIBM TTB EAB JPCRD JRES
16
+ ].freeze
17
+
5
18
  autoload :Builder, "#{__dir__}/nist/builder"
6
19
  autoload :Caster, "#{__dir__}/nist/caster"
7
20
  autoload :CircularSupplementBuilder, "#{__dir__}/nist/circular_supplement_builder"
@@ -6,6 +6,7 @@ module Pubid
6
6
  # Type to identifier class mapping (MECE)
7
7
  TYPE_CLASS_MAP = {
8
8
  "B" => "BasicPublication",
9
+ "Bulletin" => "Bulletin",
9
10
  "D" => "Document",
10
11
  "E" => "ExpertReport",
11
12
  "G" => "Guide",
@@ -175,12 +176,26 @@ module Pubid
175
176
  identifier.date = Pubid::Components::Date.new(year: year_value.to_s)
176
177
  end
177
178
 
179
+ # Bulletin-specific locator fields. The structured form captures
180
+ # issue/sequence directly; the citation form captures volume/issue/
181
+ # article_id, which we decode to the same (year, issue, sequence)
182
+ # tuple. Year comes from either the date rule (structured) or the
183
+ # article_id (citation).
184
+ if identifier.is_a?(Identifiers::Bulletin)
185
+ apply_bulletin_locator(identifier, parsed_hash)
186
+ end
187
+
178
188
  # Determine parsed format for round-trip fidelity
179
189
  # If edition_format was captured, it means "Edition" text was present
180
190
  identifier.parsed_format = if parsed_hash[:edition_format]
181
191
  "long"
182
192
  elsif parsed_hash[:space_before_lang]
183
193
  "short_with_space"
194
+ elsif parsed_hash[:article_id]
195
+ # Bulletin citation form — preserve on
196
+ # the identifier so default to_s gives
197
+ # back the citation string verbatim.
198
+ "citation"
184
199
  else
185
200
  "short"
186
201
  end
@@ -195,6 +210,24 @@ module Pubid
195
210
  identifier
196
211
  end
197
212
 
213
+ # Populate issue/sequence on a Bulletin from either parse shape.
214
+ # Structured: :issue and :sequence captured directly as zero-padded
215
+ # strings. Citation: :volume_roman, :issue_arabic, :article_id —
216
+ # decode the 8-digit article_id to recover year+issue+sequence.
217
+ def apply_bulletin_locator(identifier, parsed_hash)
218
+ if parsed_hash[:article_id]
219
+ article_id = parsed_hash[:article_id].to_s
220
+ identifier.date ||= Pubid::Components::Date.new
221
+ identifier.date.year = article_id[0, 4]
222
+ identifier.issue = article_id[4, 2]
223
+ identifier.sequence = article_id[6, 2]
224
+ return
225
+ end
226
+
227
+ identifier.issue = parsed_hash[:issue].to_s if parsed_hash[:issue]
228
+ identifier.sequence = parsed_hash[:sequence].to_s if parsed_hash[:sequence]
229
+ end
230
+
198
231
  def extract_language(lang_data)
199
232
  # Handle both direct string and nested hash from parser
200
233
  case lang_data
@@ -10,6 +10,7 @@ module Pubid
10
10
  OIML_TYPE_MAP = {
11
11
  "pubid:oiml:recommendation" => "Pubid::Oiml::Identifiers::Recommendation",
12
12
  "pubid:oiml:basic-publication" => "Pubid::Oiml::Identifiers::BasicPublication",
13
+ "pubid:oiml:bulletin" => "Pubid::Oiml::Identifiers::Bulletin",
13
14
  "pubid:oiml:document" => "Pubid::Oiml::Identifiers::Document",
14
15
  "pubid:oiml:guide" => "Pubid::Oiml::Identifiers::Guide",
15
16
  "pubid:oiml:vocabulary" => "Pubid::Oiml::Identifiers::Vocabulary",
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Oiml
5
+ module Identifiers
6
+ # OIML Bulletin issues and articles. Carries no code — the locator is
7
+ # the (year, issue, sequence) tuple drawn from the issue's place in the
8
+ # periodical hierarchy.
9
+ #
10
+ # Two input forms are accepted, both referring to the same article:
11
+ #
12
+ # structured: "OIML Bulletin 2026-02-11"
13
+ # citation: "OIML Bulletin LXVII(2) 20260211"
14
+ #
15
+ # The structured form is the dataset's primary docid (sortable,
16
+ # deterministic). The citation form is what OIML prints on the article
17
+ # page (`Citation: AUTHOR YEAR OIML Bulletin VOLUME(ISSUE) ARTID`).
18
+ #
19
+ # Volume tracks year deterministically: 1960 = I, year - 1959 = volume.
20
+ # The 8-digit article id is YYYYNNSS — the same three values concatenated,
21
+ # so the citation form is decoded entirely from the article id; the
22
+ # roman volume and parenthesised issue are redundant display.
23
+ #
24
+ # Shapes in relaton-data-oiml:
25
+ # "OIML Bulletin" (periodical)
26
+ # "OIML Bulletin 1960" (volume / year)
27
+ # "OIML Bulletin 1960-03" (issue)
28
+ # "OIML Bulletin 1960-03-01" (article, structured)
29
+ # "OIML Bulletin LXVII(2) 20260211" (article, citation)
30
+ class Bulletin < SingleIdentifier
31
+ # OIML Bulletin volume I was issued in 1960. Volume N corresponds to
32
+ # year (N + BASE_YEAR_OFFSET). Used to translate between the year
33
+ # carried in the structured form and the roman volume in citations.
34
+ BASE_YEAR_OFFSET = 1959
35
+
36
+ # Zero-padded issue number within the year ("01".."04", plus "07"/"10"
37
+ # for the online-bulletin series). Always 2 digits.
38
+ attribute :issue, :string
39
+ # Zero-padded sequence within the issue ("00" = editorial, "01"+ for
40
+ # articles). Always 2 digits.
41
+ attribute :sequence, :string
42
+
43
+ key_value do
44
+ map "issue", to: :issue
45
+ map "sequence", to: :sequence
46
+ end
47
+
48
+ def type_string
49
+ "Bulletin"
50
+ end
51
+
52
+ # Volume as an arabic string ("67"), derived from the year. nil when
53
+ # the year is absent (bare periodical reference).
54
+ def volume_arabic
55
+ return nil unless date&.year
56
+
57
+ (date.year.to_i - BASE_YEAR_OFFSET).to_s
58
+ end
59
+
60
+ # Volume as a roman-numeral string ("LXVII"), derived from the year.
61
+ # nil when the year is absent.
62
+ def volume_roman
63
+ number = volume_arabic&.to_i
64
+ return nil unless number&.positive?
65
+
66
+ self.class.to_roman(number)
67
+ end
68
+
69
+ # 8-digit oiml.org article id ("20260211"). Composed by concatenating
70
+ # year + issue + sequence. nil unless all three are present.
71
+ def article_id
72
+ return nil unless date&.year && issue && sequence
73
+
74
+ "#{date.year}#{issue}#{sequence}"
75
+ end
76
+
77
+ class << self
78
+ # Convert a positive integer to its roman-numeral representation.
79
+ # Used to render the citation form. The dataset already stores the
80
+ # roman volume verbatim in `extent.locality`, so this conversion is
81
+ # only needed when rendering from the structured form.
82
+ def to_roman(number)
83
+ raise ArgumentError, "volume must be > 0" unless number.positive?
84
+
85
+ ROMAN_TABLE.each_with_object(+"") do |(value, sym), result|
86
+ quotient, number = number.divmod(value)
87
+ result << (sym * quotient)
88
+ end.freeze
89
+ end
90
+
91
+ ROMAN_TABLE = [
92
+ [1000, "M"], [900, "CM"], [500, "D"], [400, "CD"],
93
+ [100, "C"], [90, "XC"], [50, "L"], [40, "XL"],
94
+ [10, "X"], [9, "IX"], [5, "V"], [4, "IV"],
95
+ [1, "I"]
96
+ ].freeze
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -6,6 +6,7 @@ module Pubid
6
6
  autoload :Amendment, "#{__dir__}/identifiers/amendment"
7
7
  autoload :Annex, "#{__dir__}/identifiers/annex"
8
8
  autoload :BasicPublication, "#{__dir__}/identifiers/basic_publication"
9
+ autoload :Bulletin, "#{__dir__}/identifiers/bulletin"
9
10
  autoload :Document, "#{__dir__}/identifiers/document"
10
11
  autoload :Errata, "#{__dir__}/identifiers/errata"
11
12
  autoload :ExpertReport, "#{__dir__}/identifiers/expert_report"
@@ -20,7 +20,7 @@ module Pubid
20
20
  rule(:identifier) do
21
21
  amendment_identifier | amendment_short | annex_letter_identifier |
22
22
  annex_identifier | plus_supplement_identifier |
23
- trailing_supplement_identifier | base_identifier
23
+ trailing_supplement_identifier | bulletin_identifier | base_identifier
24
24
  end
25
25
 
26
26
  # Publisher - always "OIML"
@@ -29,6 +29,48 @@ module Pubid
29
29
  # Document type - single letter
30
30
  rule(:doc_type) { match("[BDEGRSVX]").as(:type) >> space }
31
31
 
32
+ # Bulletin locator — structured form. Year optionally followed by
33
+ # 2-digit issue and 2-digit sequence:
34
+ # "OIML Bulletin", "OIML Bulletin 1960",
35
+ # "OIML Bulletin 1960-03", "OIML Bulletin 1960-03-01"
36
+ # All four shapes appear as primary docids in relaton-data-oiml.
37
+ rule(:bulletin_date) do
38
+ space >> year_digits.as(:year) >>
39
+ (dash >> two_digits.as(:issue)).maybe >>
40
+ (dash >> two_digits.as(:sequence)).maybe
41
+ end
42
+
43
+ # Two-digit zero-padded number (used for issue / sequence).
44
+ rule(:two_digits) { match('\d').repeat(2, 2) }
45
+
46
+ # Roman numeral token composed of I,V,X,L,C,D,M (uppercase, matching
47
+ # OIML's print convention). Consumed but not captured — the 8-digit
48
+ # article id in the same citation carries year/issue/sequence
49
+ # deterministically, so the roman volume is redundant for parsing.
50
+ rule(:roman_numeral) { match("[IVXLCDM]").repeat(1) }
51
+
52
+ # Bulletin locator — citation form. The format OIML prints on the
53
+ # article page is: "LXVII(2) 20260211" where LXVII is the volume in
54
+ # roman numerals, (2) is the issue in arabic without zero padding,
55
+ # and 20260211 is the 8-digit oiml.org article id (YYYYNNSS).
56
+ rule(:bulletin_citation) do
57
+ space >> roman_numeral >>
58
+ lparen >> digits.as(:issue_arabic) >> rparen >>
59
+ space >> match('\d').repeat(8, 8).as(:article_id)
60
+ end
61
+
62
+ # Bulletin identifier — no code; the locator (when present) is the
63
+ # (year, issue, sequence) tuple drawn from either the structured
64
+ # YYYY-II-SS form or the citation VOLUME(ISSUE) ARTID form. Both
65
+ # decode to the same record. Tried before base_identifier because
66
+ # "Bulletin" is a word that the single-letter doc_type rule cannot
67
+ # match.
68
+ rule(:bulletin_identifier) do
69
+ publisher >> str("Bulletin").as(:type) >>
70
+ (bulletin_citation | bulletin_date).maybe >>
71
+ language_portion.maybe.as(:language)
72
+ end
73
+
32
74
  # Number with optional part and subpart
33
75
  rule(:number_only) { digits.as(:number) }
34
76
 
@@ -20,6 +20,8 @@ module Pubid
20
20
  case id
21
21
  when Identifiers::Annex
22
22
  render_annex(id)
23
+ when Identifiers::Bulletin
24
+ render_bulletin(id)
23
25
  when SupplementIdentifier
24
26
  render_supplement(id)
25
27
  when SingleIdentifier
@@ -31,6 +33,55 @@ module Pubid
31
33
 
32
34
  private
33
35
 
36
+ # Render the Bulletin in the requested or parsed form. Default is the
37
+ # structured "YYYY-II-SS" form (the dataset's primary docid). The
38
+ # citation form ("LXVII(2) 20260211") is emitted when the user asks
39
+ # for it via `to_s(format: :citation)` or when the identifier was
40
+ # itself parsed from a citation string (parsed_format == "citation").
41
+ def render_bulletin(id)
42
+ if citation_form?(id)
43
+ render_bulletin_citation(id)
44
+ else
45
+ render_bulletin_structured(id)
46
+ end
47
+ end
48
+
49
+ # User's explicit format request takes precedence over the parsed
50
+ # form, so `parse("LXVII(2) 20260211").to_s(format: :short)` returns
51
+ # the structured form. With no explicit request, the parsed form is
52
+ # preserved for round-trip fidelity.
53
+ def citation_form?(id)
54
+ case id.requested_format
55
+ when :citation then true
56
+ when :short, :structured then false
57
+ else
58
+ id.parsed_format == "citation"
59
+ end
60
+ end
61
+
62
+ # "OIML Bulletin" / "OIML Bulletin 1960" / "OIML Bulletin 1960-03" /
63
+ # "OIML Bulletin 1960-03-01". No code; the locator is space-separated.
64
+ def render_bulletin_structured(id)
65
+ result = "#{id.publisher} Bulletin"
66
+ if id.date&.year
67
+ result += " #{id.date.year}"
68
+ result += "-#{id.issue}" if id.issue
69
+ result += "-#{id.sequence}" if id.sequence
70
+ end
71
+ result += " (#{id.language})" if id.language
72
+ result
73
+ end
74
+
75
+ # "OIML Bulletin LXVII(2) 20260211". Only emitted for full articles
76
+ # (year + issue + sequence all present); lower tiers fall back to
77
+ # structured since OIML's citation format doesn't define volume-only
78
+ # or issue-only variants.
79
+ def render_bulletin_citation(id)
80
+ return render_bulletin_structured(id) unless id.date&.year && id.issue && id.sequence
81
+
82
+ "#{id.publisher} Bulletin #{id.volume_roman}(#{id.issue.to_i}) #{id.article_id}"
83
+ end
84
+
34
85
  def effective_format(id)
35
86
  id.requested_format ||
36
87
  (id.parsed_format == "long" ? :long : :short)
@@ -28,6 +28,17 @@ module Pubid
28
28
  end
29
29
 
30
30
  def generate
31
+ # Bulletin issues carry no code; the (year, issue, sequence) tuple
32
+ # is the locator. URNs are canonical regardless of how the input was
33
+ # formatted, so we always emit the structured YYYY-II-SS form even
34
+ # when the identifier was parsed from a citation string.
35
+ if identifier.is_a?(Identifiers::Bulletin)
36
+ parts = ["urn", "oiml", "bulletin"]
37
+ parts << bulletin_locator if identifier.date&.present?
38
+ parts << urn_language if urn_language
39
+ return parts.join(":")
40
+ end
41
+
31
42
  parts = ["urn", "oiml"]
32
43
  parts << urn_type
33
44
  parts << urn_number if urn_number
@@ -51,6 +62,17 @@ module Pubid
51
62
 
52
63
  parts.join(":")
53
64
  end
65
+
66
+ # Structured locator "YYYY-II-SS" (or "YYYY-II" / "YYYY") for a Bulletin.
67
+ # Built directly from the date year and the issue/sequence attributes so
68
+ # the URN is identical regardless of whether the input was the structured
69
+ # or citation form.
70
+ def bulletin_locator
71
+ locator = identifier.date.year.to_s
72
+ locator += "-#{identifier.issue}" if identifier.issue
73
+ locator += "-#{identifier.sequence}" if identifier.sequence
74
+ locator
75
+ end
54
76
  end
55
77
  end
56
78
  end
data/lib/pubid/oiml.rb CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  module Pubid
4
4
  module Oiml
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # Sole OIML publisher token (see the parser's `publisher` rule).
8
+ PREFIXES = ["OIML"].freeze
9
+
5
10
  autoload :Builder, "#{__dir__}/oiml/builder"
6
11
  autoload :Components, "#{__dir__}/oiml/components"
7
12
  autoload :Identifier, "#{__dir__}/oiml/identifier"
data/lib/pubid/plateau.rb CHANGED
@@ -4,6 +4,11 @@ require "parslet"
4
4
 
5
5
  module Pubid
6
6
  module Plateau
7
+ extend Pubid::PrefixesSupport
8
+
9
+ # Sole PLATEAU publisher token (see the parser's `publisher` rule).
10
+ PREFIXES = ["PLATEAU"].freeze
11
+
7
12
  autoload :Builder, "#{__dir__}/plateau/builder"
8
13
  autoload :Identifier, "#{__dir__}/plateau/identifier"
9
14
  autoload :Identifiers, "#{__dir__}/plateau/identifiers"
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ # Mixin providing the uniform, static +prefixes+ class method that every
5
+ # registered flavor exposes. relaton uses this to build a global prefix
6
+ # register that routes a printed reference string (e.g. +"DD 1234"+,
7
+ # +"ISO/IEC 8802"+) to the flavor(s) that own it.
8
+ #
9
+ # A flavor +extend+s this module and defines a frozen +PREFIXES+ array holding
10
+ # only *its own* leading tokens. Joint / co-publication tokens are injected
11
+ # centrally from {Pubid::JOINT_PREFIXES} so that a co-published prefix
12
+ # (e.g. +"ISO/IEC"+) is listed symmetrically by every co-publisher without
13
+ # being duplicated by hand in two files.
14
+ #
15
+ # == Inclusion policy
16
+ # +prefixes+ returns the set of *leading identifier prefix tokens* — the
17
+ # tokens a printed identifier can start with such that the string belongs to
18
+ # this SDO. Concretely it INCLUDES:
19
+ # * publisher prefixes (e.g. +ISO+, +IEC+, +NIST+, +NBS+, +BS+, +BSI+);
20
+ # * leading series/type tokens that can *begin* a printed reference on their
21
+ # own (e.g. BSI +DD+/+PD+, NIST +FIPS+/+SP+);
22
+ # * joint / co-publication forms (e.g. +ISO/IEC+, +ISO/IEC/IEEE+).
23
+ # It EXCLUDES:
24
+ # * pure sub-series that never appear without a publisher prefix;
25
+ # * dangerously-ambiguous single letters (e.g. BSI aerospace +A+/+B+/+C+),
26
+ # which would cause false routing.
27
+ module PrefixesSupport
28
+ # The static set of leading prefix tokens this flavor recognizes.
29
+ #
30
+ # Static: callable without parsing any identifier string. Strings are
31
+ # returned in canonical case exactly as printed (e.g. +"BS EN ISO"+,
32
+ # +"ISO/IEC"+); consumers fold case themselves.
33
+ #
34
+ # @return [Array<String>] frozen, de-duplicated list of prefix tokens
35
+ def prefixes
36
+ own = const_defined?(:PREFIXES) ? const_get(:PREFIXES) : []
37
+ joint = Pubid::JOINT_PREFIXES.fetch(prefix_flavor_key, [])
38
+ (own + joint).uniq.freeze
39
+ end
40
+
41
+ # The registry key for this flavor, derived from the module name
42
+ # (e.g. +Pubid::CenCenelec+ => +:cen_cenelec+). Used to look this flavor up
43
+ # in {Pubid::JOINT_PREFIXES} and to label it in {Pubid.prefix_flavors}.
44
+ #
45
+ # @return [Symbol]
46
+ def prefix_flavor_key
47
+ name.split("::").last
48
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase.to_sym
49
+ end
50
+ end
51
+ end
data/lib/pubid/sae.rb CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  module Pubid
4
4
  module Sae
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # Sole SAE publisher token (see the parser's `publisher` rule). Document
8
+ # types (AMS/AIR/ARP/AS/J/MA) follow the SAE prefix and are excluded.
9
+ PREFIXES = ["SAE"].freeze
10
+
5
11
  autoload :Builder, "#{__dir__}/sae/builder"
6
12
  autoload :Components, "#{__dir__}/sae/components"
7
13
  autoload :Identifier, "#{__dir__}/sae/identifier"
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.6"
4
+ VERSION = "2.0.0.pre.alpha.7"
5
5
  end
data/lib/pubid.rb CHANGED
@@ -9,6 +9,11 @@ require "parslet"
9
9
  # code runs.
10
10
  require "pubid/lutaml/no_store_registration"
11
11
 
12
+ # Uniform, static per-flavor prefix API (Pubid::<Flavor>.prefixes). Required
13
+ # before the flavor autoloads below so each flavor file can `extend` it and the
14
+ # central JOINT_PREFIXES constant is visible when its PREFIXES is defined.
15
+ require "pubid/prefixes_support"
16
+
12
17
  module Pubid
13
18
  # Upper bound on the length of an identifier string accepted by any +parse+
14
19
  # entry point. Real-world standards identifiers are well under 200 characters;
@@ -61,6 +66,21 @@ module Pubid
61
66
  end
62
67
  end
63
68
 
69
+ # Canonical joint / co-publication leading tokens, injected symmetrically into
70
+ # every participating flavor's +prefixes+ (see {PrefixesSupport}). Single
71
+ # source of truth: editing an entry here updates both sides at once
72
+ # (e.g. +"ISO/IEC"+ appears in +Pubid::Iso.prefixes+ and +Pubid::Iec.prefixes+),
73
+ # so co-publication symmetry can never drift. Keyed by
74
+ # {PrefixesSupport#prefix_flavor_key}.
75
+ JOINT_PREFIXES = {
76
+ iso: ["ISO/IEC", "IEC/ISO", "ISO/IEC/IEEE"],
77
+ iec: ["ISO/IEC", "IEC/ISO", "ISO/IEC/IEEE"],
78
+ ieee: ["ISO/IEC/IEEE"],
79
+ ansi: ["ANSI/ASHRAE", "ANSI/AMCA"],
80
+ ashrae: ["ANSI/ASHRAE"],
81
+ amca: ["ANSI/AMCA"],
82
+ }.freeze
83
+
64
84
  autoload :Parser, "pubid/parser"
65
85
  autoload :Components, "pubid/components"
66
86
  autoload :BundledIdentifier, "pubid/bundled_identifier"
@@ -175,4 +195,52 @@ module Pubid
175
195
  end
176
196
  @flavors_loaded = true
177
197
  end
198
+
199
+ # Static leading prefix tokens for a single flavor.
200
+ #
201
+ # @param flavor [Symbol, String] a registered flavor name (e.g. +:iso+)
202
+ # @return [Array<String>] the flavor's prefixes (see {PrefixesSupport})
203
+ # @raise [ArgumentError] if the flavor is not registered
204
+ def self.prefixes(flavor)
205
+ mod = Registry.get(flavor)
206
+ raise ArgumentError, "unknown flavor: #{flavor.inspect}" unless mod
207
+
208
+ mod.prefixes
209
+ end
210
+
211
+ # Reverse index mapping every canonical prefix token to the flavor(s) that
212
+ # own it — the exact routing table relaton needs. Co-published prefixes list
213
+ # every co-publisher (e.g. +"ISO/IEC" => [:iec, :iso]+); see the inclusion /
214
+ # exclusion policy documented on {PrefixesSupport}.
215
+ #
216
+ # The +:cen+ alias of +:cen_cenelec+ is de-duplicated by module identity, so a
217
+ # prefix is attributed to a flavor's canonical
218
+ # {PrefixesSupport#prefix_flavor_key} exactly once.
219
+ #
220
+ # @return [Hash{String => Array<Symbol>}] prefix token => sorted flavor keys,
221
+ # e.g. +{ "ISO" => [:iso], "ISO/IEC" => [:iec, :iso], ... }+
222
+ def self.prefix_flavors
223
+ index = Hash.new { |h, k| h[k] = [] }
224
+ each_prefix_flavor_module do |mod|
225
+ key = mod.prefix_flavor_key
226
+ mod.prefixes.each { |prefix| index[prefix] |= [key] }
227
+ end
228
+ index.transform_values(&:sort).sort.to_h
229
+ end
230
+
231
+ # Yields each unique flavor module that exposes +prefixes+ exactly once,
232
+ # collapsing alias registrations (e.g. +:cen+ and +:cen_cenelec+ both point to
233
+ # +Pubid::CenCenelec+) by module identity.
234
+ # @yieldparam mod [Module] a flavor module
235
+ def self.each_prefix_flavor_module
236
+ eager_load_flavors!
237
+ seen = {}
238
+ Registry.flavor_names.each do |flavor_name|
239
+ mod = Registry.get(flavor_name)
240
+ next if seen.key?(mod) || !mod.respond_to?(:prefixes)
241
+
242
+ seen[mod] = true
243
+ yield mod
244
+ end
245
+ end
178
246
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pubid
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.pre.alpha.6
4
+ version: 2.0.0.pre.alpha.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -339,8 +339,12 @@ files:
339
339
  - lib/pubid/iala/builder.rb
340
340
  - lib/pubid/iala/identifier.rb
341
341
  - lib/pubid/iala/identifiers.rb
342
+ - lib/pubid/iala/identifiers/advice.rb
343
+ - lib/pubid/iala/identifiers/annex.rb
342
344
  - lib/pubid/iala/identifiers/base.rb
345
+ - lib/pubid/iala/identifiers/general_assembly.rb
343
346
  - lib/pubid/iala/identifiers/guideline.rb
347
+ - lib/pubid/iala/identifiers/letter.rb
344
348
  - lib/pubid/iala/identifiers/manual.rb
345
349
  - lib/pubid/iala/identifiers/model_course.rb
346
350
  - lib/pubid/iala/identifiers/recommendation.rb
@@ -625,6 +629,7 @@ files:
625
629
  - lib/pubid/oiml/identifiers/amendment.rb
626
630
  - lib/pubid/oiml/identifiers/annex.rb
627
631
  - lib/pubid/oiml/identifiers/basic_publication.rb
632
+ - lib/pubid/oiml/identifiers/bulletin.rb
628
633
  - lib/pubid/oiml/identifiers/document.rb
629
634
  - lib/pubid/oiml/identifiers/errata.rb
630
635
  - lib/pubid/oiml/identifiers/expert_report.rb
@@ -656,6 +661,7 @@ files:
656
661
  - lib/pubid/plateau/supplement_identifier.rb
657
662
  - lib/pubid/plateau/urn_generator.rb
658
663
  - lib/pubid/plateau/urn_parser.rb
664
+ - lib/pubid/prefixes_support.rb
659
665
  - lib/pubid/renderers.rb
660
666
  - lib/pubid/renderers/base.rb
661
667
  - lib/pubid/renderers/directives_renderer.rb