pubid 2.0.0.pre.alpha.3 → 2.0.0.pre.alpha.4

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 (151) hide show
  1. checksums.yaml +4 -4
  2. data/lib/pubid/amca/identifier.rb +4 -23
  3. data/lib/pubid/amca/identifiers/base.rb +67 -30
  4. data/lib/pubid/amca/identifiers/interpretation.rb +0 -4
  5. data/lib/pubid/amca/identifiers/publication.rb +0 -4
  6. data/lib/pubid/amca/identifiers/standard.rb +0 -4
  7. data/lib/pubid/amca/renderer.rb +1 -1
  8. data/lib/pubid/amca/urn_generator.rb +2 -1
  9. data/lib/pubid/api/builder.rb +4 -0
  10. data/lib/pubid/api/identifier.rb +5 -1
  11. data/lib/pubid/api/identifiers/base.rb +2 -12
  12. data/lib/pubid/api/identifiers/mpms.rb +0 -4
  13. data/lib/pubid/api/identifiers/typeless_standard.rb +0 -3
  14. data/lib/pubid/api/single_identifier.rb +5 -8
  15. data/lib/pubid/ashrae/identifier.rb +7 -22
  16. data/lib/pubid/ashrae/identifiers/base.rb +28 -17
  17. data/lib/pubid/ashrae/parser.rb +4 -0
  18. data/lib/pubid/ashrae.rb +4 -0
  19. data/lib/pubid/asme/single_identifier.rb +3 -1
  20. data/lib/pubid/astm/single_identifier.rb +3 -1
  21. data/lib/pubid/bsi/identifier.rb +5 -31
  22. data/lib/pubid/bsi/identifiers/addendum_document.rb +0 -4
  23. data/lib/pubid/bsi/identifiers/adopted_european_norm.rb +0 -4
  24. data/lib/pubid/bsi/identifiers/adopted_international_standard.rb +0 -4
  25. data/lib/pubid/bsi/identifiers/aerospace_standard.rb +0 -3
  26. data/lib/pubid/bsi/identifiers/amendment.rb +0 -4
  27. data/lib/pubid/bsi/identifiers/british_industrial_practice.rb +0 -3
  28. data/lib/pubid/bsi/identifiers/bundled_identifier.rb +2 -5
  29. data/lib/pubid/bsi/identifiers/committee_document.rb +0 -3
  30. data/lib/pubid/bsi/identifiers/consolidated_identifier.rb +2 -5
  31. data/lib/pubid/bsi/identifiers/corrigendum.rb +0 -4
  32. data/lib/pubid/bsi/identifiers/detailed_specification.rb +0 -3
  33. data/lib/pubid/bsi/identifiers/disc.rb +0 -3
  34. data/lib/pubid/bsi/identifiers/draft_document.rb +0 -3
  35. data/lib/pubid/bsi/identifiers/electronic_book.rb +0 -3
  36. data/lib/pubid/bsi/identifiers/expert_commentary.rb +0 -4
  37. data/lib/pubid/bsi/identifiers/explanatory_supplement.rb +0 -3
  38. data/lib/pubid/bsi/identifiers/flex.rb +0 -3
  39. data/lib/pubid/bsi/identifiers/handbook.rb +0 -3
  40. data/lib/pubid/bsi/identifiers/index.rb +0 -3
  41. data/lib/pubid/bsi/identifiers/method.rb +0 -3
  42. data/lib/pubid/bsi/identifiers/national_annex.rb +0 -3
  43. data/lib/pubid/bsi/identifiers/practice_guide.rb +0 -3
  44. data/lib/pubid/bsi/identifiers/publicly_available_specification.rb +0 -3
  45. data/lib/pubid/bsi/identifiers/published_document.rb +0 -3
  46. data/lib/pubid/bsi/identifiers/section.rb +0 -3
  47. data/lib/pubid/bsi/identifiers/set.rb +2 -5
  48. data/lib/pubid/bsi/identifiers/standalone_amendment.rb +0 -3
  49. data/lib/pubid/bsi/identifiers/supplement_document.rb +0 -4
  50. data/lib/pubid/bsi/identifiers/supplementary_index.rb +0 -3
  51. data/lib/pubid/bsi/identifiers/technical_specification.rb +0 -3
  52. data/lib/pubid/bsi/identifiers/test_method.rb +0 -3
  53. data/lib/pubid/bsi/identifiers/value_added_publication.rb +0 -4
  54. data/lib/pubid/bsi/single_identifier.rb +32 -13
  55. data/lib/pubid/bsi.rb +8 -1
  56. data/lib/pubid/ccsds/identifier.rb +2 -14
  57. data/lib/pubid/cen_cenelec/identifier.rb +10 -8
  58. data/lib/pubid/cen_cenelec/identifiers/adopted_european_norm.rb +0 -4
  59. data/lib/pubid/cen_cenelec/identifiers/amendment.rb +0 -4
  60. data/lib/pubid/cen_cenelec/identifiers/base.rb +2 -9
  61. data/lib/pubid/cen_cenelec/identifiers/consolidated_identifier.rb +0 -4
  62. data/lib/pubid/cen_cenelec/identifiers/corrigendum.rb +0 -4
  63. data/lib/pubid/cen_cenelec/identifiers/european_prestandard.rb +0 -3
  64. data/lib/pubid/cen_cenelec/identifiers/fragment.rb +0 -4
  65. data/lib/pubid/cen_cenelec/single_identifier.rb +4 -7
  66. data/lib/pubid/cen_cenelec.rb +4 -0
  67. data/lib/pubid/cie/identifier.rb +4 -0
  68. data/lib/pubid/cie/single_identifier.rb +4 -4
  69. data/lib/pubid/cie.rb +4 -0
  70. data/lib/pubid/csa/identifier.rb +10 -1
  71. data/lib/pubid/csa/identifiers/base.rb +0 -3
  72. data/lib/pubid/csa/identifiers/cec.rb +0 -4
  73. data/lib/pubid/csa/identifiers/series.rb +0 -4
  74. data/lib/pubid/csa/single_identifier.rb +1 -1
  75. data/lib/pubid/csa.rb +4 -0
  76. data/lib/pubid/etsi/components/code.rb +1 -7
  77. data/lib/pubid/etsi/components/version.rb +0 -5
  78. data/lib/pubid/etsi/identifier.rb +4 -12
  79. data/lib/pubid/etsi/identifiers/base.rb +31 -24
  80. data/lib/pubid/etsi/identifiers/supplement_identifier.rb +0 -3
  81. data/lib/pubid/etsi.rb +3 -1
  82. data/lib/pubid/identifier.rb +90 -1
  83. data/lib/pubid/idf/identifier.rb +4 -0
  84. data/lib/pubid/idf.rb +4 -0
  85. data/lib/pubid/iec/identifier.rb +14 -14
  86. data/lib/pubid/iec/identifiers/base.rb +0 -4
  87. data/lib/pubid/iec/identifiers/consolidated_identifier.rb +0 -4
  88. data/lib/pubid/iec/identifiers/fragment_identifier.rb +1 -5
  89. data/lib/pubid/iec/identifiers/sheet_identifier.rb +1 -5
  90. data/lib/pubid/iec/identifiers/test_report_form.rb +0 -3
  91. data/lib/pubid/iec/identifiers/vap_identifier.rb +1 -5
  92. data/lib/pubid/iec/identifiers/working_document.rb +0 -4
  93. data/lib/pubid/iec.rb +4 -0
  94. data/lib/pubid/ieee/identifier.rb +5 -17
  95. data/lib/pubid/ieee/identifiers/adopted_standard.rb +0 -4
  96. data/lib/pubid/ieee/identifiers/base.rb +223 -201
  97. data/lib/pubid/ieee/identifiers/conformance_identifier.rb +0 -4
  98. data/lib/pubid/ieee/identifiers/corrigendum.rb +0 -4
  99. data/lib/pubid/ieee/identifiers/csa_dual_published.rb +0 -4
  100. data/lib/pubid/ieee/identifiers/dual_identifier.rb +0 -4
  101. data/lib/pubid/ieee/identifiers/dual_published.rb +0 -4
  102. data/lib/pubid/ieee/identifiers/iec_ieee_copublished.rb +0 -4
  103. data/lib/pubid/ieee/identifiers/interpretation_identifier.rb +0 -4
  104. data/lib/pubid/ieee/identifiers/multi_numbered_identifier.rb +0 -4
  105. data/lib/pubid/ieee/identifiers/parenthetical_identifier.rb +0 -4
  106. data/lib/pubid/ieee/identifiers/redlined_standard.rb +0 -4
  107. data/lib/pubid/ieee/identifiers/si_standard.rb +0 -4
  108. data/lib/pubid/ieee.rb +4 -0
  109. data/lib/pubid/iho/identifier.rb +7 -25
  110. data/lib/pubid/iho/identifiers/base.rb +62 -68
  111. data/lib/pubid/iho.rb +3 -1
  112. data/lib/pubid/iso/identifier.rb +15 -14
  113. data/lib/pubid/iso/single_identifier.rb +0 -4
  114. data/lib/pubid/iso.rb +4 -0
  115. data/lib/pubid/itu/components/code.rb +1 -7
  116. data/lib/pubid/itu/components/sector.rb +15 -4
  117. data/lib/pubid/itu/components/series.rb +0 -4
  118. data/lib/pubid/itu/identifier.rb +4 -18
  119. data/lib/pubid/itu/identifiers/base.rb +124 -122
  120. data/lib/pubid/itu.rb +3 -1
  121. data/lib/pubid/jcgm/identifiers/amendment.rb +0 -4
  122. data/lib/pubid/jcgm/identifiers/gum_guide.rb +0 -4
  123. data/lib/pubid/jcgm/single_identifier.rb +0 -4
  124. data/lib/pubid/jis/identifier.rb +2 -14
  125. data/lib/pubid/nist/identifier.rb +8 -23
  126. data/lib/pubid/nist/identifiers/base.rb +553 -544
  127. data/lib/pubid/nist/preprocessor.rb +12 -0
  128. data/lib/pubid/nist.rb +4 -0
  129. data/lib/pubid/oiml/builder.rb +21 -3
  130. data/lib/pubid/oiml/components/code.rb +7 -0
  131. data/lib/pubid/oiml/identifier.rb +44 -0
  132. data/lib/pubid/oiml/identifiers/annex.rb +10 -1
  133. data/lib/pubid/oiml/identifiers/errata.rb +15 -0
  134. data/lib/pubid/oiml/identifiers.rb +1 -1
  135. data/lib/pubid/oiml/parser.rb +59 -12
  136. data/lib/pubid/oiml/renderer.rb +27 -17
  137. data/lib/pubid/oiml/single_identifier.rb +58 -0
  138. data/lib/pubid/oiml/supplement_identifier.rb +30 -1
  139. data/lib/pubid/plateau/identifier.rb +4 -21
  140. data/lib/pubid/plateau/identifiers/base.rb +45 -36
  141. data/lib/pubid/plateau/identifiers/handbook.rb +0 -4
  142. data/lib/pubid/plateau/identifiers/technical_report.rb +0 -4
  143. data/lib/pubid/plateau/supplement_identifier.rb +0 -4
  144. data/lib/pubid/sae/identifier.rb +4 -12
  145. data/lib/pubid/sae/identifiers/base.rb +26 -17
  146. data/lib/pubid/version.rb +1 -1
  147. data/lib/pubid.rb +18 -2
  148. data/lib/tasks/export.rake +6 -5
  149. metadata +3 -4
  150. data/lib/pubid/identifier_facade.rb +0 -114
  151. data/lib/pubid/oiml/identifiers/base.rb +0 -21
@@ -2,640 +2,649 @@
2
2
 
3
3
  module Pubid
4
4
  module Nist
5
- module Identifiers
6
- # Base NIST/NBS identifier class
7
- # Each series type inherits from this and overrides series_code
8
- class Base < Pubid::Identifier
9
- # Mark every NIST identifier as a member of the Pubid::Nist::Identifier
10
- # facade module so `id.is_a?(Pubid::Nist::Identifier)` and
11
- # `Pubid::Nist::Identifier === id` hold. The module carries only
12
- # singleton methods (parse/from_hash), so this adds ancestry for the
13
- # identity check without injecting any instance behavior.
14
- include Pubid::Nist::Identifier
15
-
16
- # Default: no typed stages. Subclasses override as needed.
17
- def self.typed_stages
18
- []
19
- end
5
+ # NIST/NBS flavor base class. Canonical name Pubid::Nist::Identifier; every
6
+ # concrete NIST identifier (Identifiers::*) descends from it, and
7
+ # Identifiers::Base aliased at the foot of this file points back to it
8
+ # for backward compatibility. Being a real Pubid::Identifier subclass gives
9
+ # native `is_a?` identity and the shared polymorphic `from_hash`.
10
+ class Identifier < ::Pubid::Identifier
11
+ # Delegate to the flavor module so callers can use
12
+ # `Pubid::Nist::Identifier.parse` consistently with other flavors.
13
+ def self.parse(identifier)
14
+ Pubid::Nist.parse(identifier)
15
+ end
20
16
 
21
- # Generate URN for this identifier
22
- #
23
- # @return [String] URN representation
24
-
25
- # Plain string ("NIST"/"NBS"), not a Components::Publisher wrapper: the
26
- # value is a single token, so a string serializes flat
27
- # (`publisher: NBS`, not `publisher: {publisher: NBS}`) and accepts the
28
- # raw string the circular/supplement builders pass straight through.
29
- attribute :publisher, :string
30
- attribute :series, Components::Code # Set by Builder from parsed data
31
- attribute :number, Components::Code
32
-
33
- # V2 COMPONENTS (Lutaml::Model objects) - PROPER SEPARATION
34
- attribute :edition, Components::Edition # Edition (type + id): e2, e2021, r5, -3
35
- attribute :edition_component, Components::Edition # V2 edition component (alias)
36
- attribute :volume, Components::Volume # Volume component (v6)
37
- attribute :part, Components::Part # Part component (n1 or pt1)
38
- attribute :stage, Components::Stage
39
- attribute :version_component, Components::Version
40
- attribute :update_component, Components::Update
41
- attribute :translation_component, Components::Translation
42
- attribute :issue_number, Components::IssueNumber
43
- attribute :parsed_format, :string # :mr, :short, :long, :abbrev
44
- # Whether the publisher prefix (NIST/NBS) should be rendered. Defaults
45
- # to true so the common publisher-bearing id need not serialize the
46
- # flag at all — the Builder only assigns it (false) for prefix-less
47
- # inputs, so `to_hash` carries `publisher_was_parsed: false` only in
48
- # that case and omits it otherwise. (lutaml emits a boolean iff it was
49
- # explicitly assigned; an omitted key loads as this default.)
50
- attribute :publisher_was_parsed, :boolean, default: -> { true }
51
-
52
- # LEGACY attributes (keep for backward compatibility during migration)
53
- attribute :parts, Components::Code, collection: true
54
- attribute :revision, :string
55
- attribute :revision_year, :string # Year for revision (e.g., r6/1925, r1963, rJun1992)
56
- attribute :revision_month, :string # Month for revision (e.g., rJun1992)
57
- attribute :edition_year, :string # Legacy edition year for backward compatibility
58
- attribute :version, :string
59
- attribute :update, Components::Update
60
- attribute :year, :integer
61
- attribute :month, :integer
62
-
63
- # Additional attributes for complex patterns
64
- attribute :first_number, Components::Code
65
- attribute :second_number, Components::Code
66
- attribute :update_number, :string
67
- attribute :update_year, :string
68
- attribute :addendum, :string
69
- attribute :addendum_number, :string
70
- # Single source of truth for the supplement: a structured component with
71
- # isolated parts (number / year / month / date-range / revision), so a
72
- # supplement's year is queryable independently of its number. Presence
73
- # (non-nil) means "is a supplement"; an all-empty component is a bare
74
- # "sup" marker. Replaces the former flat :supplement string plus the
75
- # separate date-range/has_revision fields.
76
- attribute :supplement, Components::Supplement
77
- attribute :errata, :string
78
- attribute :index, :string
79
- attribute :insert, :string
80
- attribute :section, :string
81
- attribute :appendix, :string
82
- attribute :translation, :string
83
- attribute :draft, :string
84
- attribute :draft_number, :string # For -draft N → N pd rendering
85
-
86
- # Default series_code — subclasses override to provide a normalized series name.
87
- def series_code
88
- nil
89
- end
17
+ # Default: no typed stages. Subclasses override as needed.
18
+ def self.typed_stages
19
+ []
20
+ end
90
21
 
91
- def initialize(**attributes)
92
- super()
22
+ # Generate URN for this identifier
23
+ #
24
+ # @return [String] URN representation
25
+
26
+ # Plain string ("NIST"/"NBS"), not a Components::Publisher wrapper: the
27
+ # value is a single token, so a string serializes flat
28
+ # (`publisher: NBS`, not `publisher: {publisher: NBS}`) and accepts the
29
+ # raw string the circular/supplement builders pass straight through.
30
+ attribute :publisher, :string
31
+ attribute :series, Components::Code # Set by Builder from parsed data
32
+ attribute :number, Components::Code
33
+
34
+ # V2 COMPONENTS (Lutaml::Model objects) - PROPER SEPARATION
35
+ attribute :edition, Components::Edition # Edition (type + id): e2, e2021, r5, -3
36
+ attribute :edition_component, Components::Edition # V2 edition component (alias)
37
+ attribute :volume, Components::Volume # Volume component (v6)
38
+ attribute :part, Components::Part # Part component (n1 or pt1)
39
+ attribute :stage, Components::Stage
40
+ attribute :version_component, Components::Version
41
+ attribute :update_component, Components::Update
42
+ attribute :translation_component, Components::Translation
43
+ attribute :issue_number, Components::IssueNumber
44
+ attribute :parsed_format, :string # :mr, :short, :long, :abbrev
45
+ # Whether the publisher prefix (NIST/NBS) should be rendered. Defaults
46
+ # to true so the common publisher-bearing id need not serialize the
47
+ # flag at all — the Builder only assigns it (false) for prefix-less
48
+ # inputs, so `to_hash` carries `publisher_was_parsed: false` only in
49
+ # that case and omits it otherwise. (lutaml emits a boolean iff it was
50
+ # explicitly assigned; an omitted key loads as this default.)
51
+ attribute :publisher_was_parsed, :boolean, default: -> { true }
52
+
53
+ # LEGACY attributes (keep for backward compatibility during migration)
54
+ attribute :parts, Components::Code, collection: true
55
+ attribute :revision, :string
56
+ attribute :revision_year, :string # Year for revision (e.g., r6/1925, r1963, rJun1992)
57
+ attribute :revision_month, :string # Month for revision (e.g., rJun1992)
58
+ attribute :edition_year, :string # Legacy edition year for backward compatibility
59
+ attribute :version, :string
60
+ attribute :update, Components::Update
61
+ attribute :year, :integer
62
+ attribute :month, :integer
63
+
64
+ # Additional attributes for complex patterns
65
+ attribute :first_number, Components::Code
66
+ attribute :second_number, Components::Code
67
+ attribute :update_number, :string
68
+ attribute :update_year, :string
69
+ attribute :addendum, :string
70
+ attribute :addendum_number, :string
71
+ # Single source of truth for the supplement: a structured component with
72
+ # isolated parts (number / year / month / date-range / revision), so a
73
+ # supplement's year is queryable independently of its number. Presence
74
+ # (non-nil) means "is a supplement"; an all-empty component is a bare
75
+ # "sup" marker. Replaces the former flat :supplement string plus the
76
+ # separate date-range/has_revision fields.
77
+ attribute :supplement, Components::Supplement
78
+ attribute :errata, :string
79
+ attribute :index, :string
80
+ attribute :insert, :string
81
+ attribute :section, :string
82
+ attribute :appendix, :string
83
+ attribute :translation, :string
84
+ attribute :draft, :string
85
+ attribute :draft_number, :string # For -draft N → N pd rendering
86
+
87
+ # Default series_code — subclasses override to provide a normalized series name.
88
+ def series_code
89
+ nil
90
+ end
93
91
 
94
- attrs = self.class.attributes
95
- attributes.each do |key, value|
96
- next if value.nil?
92
+ def initialize(**attributes)
93
+ super()
97
94
 
98
- setter = :"#{key}="
99
- public_send(setter, value) if attrs.key?(key)
100
- end
95
+ attrs = self.class.attributes
96
+ attributes.each do |key, value|
97
+ next if value.nil?
101
98
 
102
- # NOTE: Compound number building is handled by the Builder class
103
- # Do NOT build compound numbers here - let the builder apply special patterns first
104
- # See lib/pubid/nist/builder.rb lines 368-472 for compound number logic
99
+ setter = :"#{key}="
100
+ public_send(setter, value) if attrs.key?(key)
105
101
  end
106
102
 
107
- # Attributes that are build artifacts or rendering aliases, not part
108
- # of an identifier's logical identity. They diverge between equally-
109
- # valid spellings of the same id (e.g. long "Rev. 1" vs short "r1"):
110
- # - edition_component: redundant alias of :edition
111
- # - first_number/second_number: decomposed parts of the canonical
112
- # :number, retained from the parse for building
113
- # - parsed_format: records the input format for round-trip rendering
114
- EQUALITY_IGNORED_ATTRS = %i[
115
- edition_component first_number second_number parsed_format
116
- ].freeze
117
-
118
- # Logical identity comparison: equal when every attribute except the
119
- # build artifacts/aliases above matches. (Edition#== already ignores
120
- # its rendering-only original_prefix.)
121
- def ==(other)
122
- return false unless other.instance_of?(self.class)
123
-
124
- self.class.attributes.each_key.all? do |name|
125
- EQUALITY_IGNORED_ATTRS.include?(name) || public_send(name) == other.public_send(name)
126
- end
103
+ # NOTE: Compound number building is handled by the Builder class
104
+ # Do NOT build compound numbers here - let the builder apply special patterns first
105
+ # See lib/pubid/nist/builder.rb lines 368-472 for compound number logic
106
+ end
107
+
108
+ # Attributes that are build artifacts or rendering aliases, not part
109
+ # of an identifier's logical identity. They diverge between equally-
110
+ # valid spellings of the same id (e.g. long "Rev. 1" vs short "r1"):
111
+ # - edition_component: redundant alias of :edition
112
+ # - first_number/second_number: decomposed parts of the canonical
113
+ # :number, retained from the parse for building
114
+ # - parsed_format: records the input format for round-trip rendering
115
+ EQUALITY_IGNORED_ATTRS = %i[
116
+ edition_component first_number second_number parsed_format
117
+ ].freeze
118
+
119
+ # Logical identity comparison: equal when every attribute except the
120
+ # build artifacts/aliases above matches. (Edition#== already ignores
121
+ # its rendering-only original_prefix.)
122
+ def ==(other)
123
+ return false unless other.instance_of?(self.class)
124
+
125
+ self.class.attributes.each_key.all? do |name|
126
+ EQUALITY_IGNORED_ATTRS.include?(name) || public_send(name) == other.public_send(name)
127
127
  end
128
+ end
128
129
 
129
- alias eql? ==
130
+ alias eql? ==
130
131
 
131
- def hash
132
- vals = self.class.attributes.each_key.reject do |name|
133
- EQUALITY_IGNORED_ATTRS.include?(name)
134
- end.map { |name| public_send(name) }
135
- [self.class, *vals].hash
136
- end
132
+ def hash
133
+ vals = self.class.attributes.each_key.reject do |name|
134
+ EQUALITY_IGNORED_ATTRS.include?(name)
135
+ end.map { |name| public_send(name) }
136
+ [self.class, *vals].hash
137
+ end
137
138
 
138
- # Wildcard / partial-identifier match. Treats +self+ as a QUERY pattern
139
- # and +candidate+ as a concrete document: every ID part SET on the query
140
- # must equal the candidate's, while parts left unset (nil/empty) are
141
- # wildcards that match any value. So a query carrying no edition and no
142
- # supplement matches that document across ALL editions, years, and
143
- # supplements — the basis for "select docs by ID parts".
144
- #
145
- # Asymmetric (unlike ==): "NBS CIRC 25".matches?("NBS CIRC 25sup1924")
146
- # is true, but not the reverse. The candidate must be the same class or
147
- # a subclass so series-level identity still holds.
148
- def matches?(candidate)
149
- return false unless candidate.is_a?(self.class)
150
-
151
- self.class.attributes.each_key.all? do |name|
152
- next true if EQUALITY_IGNORED_ATTRS.include?(name)
153
-
154
- query_val = public_send(name)
155
- next true if query_val.nil?
156
- next true if query_val.is_a?(String) && query_val.empty?
157
- next true if query_val.is_a?(Array) && query_val.empty?
158
-
159
- query_val == candidate.public_send(name)
160
- end
139
+ # Wildcard / partial-identifier match. Treats +self+ as a QUERY pattern
140
+ # and +candidate+ as a concrete document: every ID part SET on the query
141
+ # must equal the candidate's, while parts left unset (nil/empty) are
142
+ # wildcards that match any value. So a query carrying no edition and no
143
+ # supplement matches that document across ALL editions, years, and
144
+ # supplements — the basis for "select docs by ID parts".
145
+ #
146
+ # Asymmetric (unlike ==): "NBS CIRC 25".matches?("NBS CIRC 25sup1924")
147
+ # is true, but not the reverse. The candidate must be the same class or
148
+ # a subclass so series-level identity still holds.
149
+ def matches?(candidate)
150
+ return false unless candidate.is_a?(self.class)
151
+
152
+ self.class.attributes.each_key.all? do |name|
153
+ next true if EQUALITY_IGNORED_ATTRS.include?(name)
154
+
155
+ query_val = public_send(name)
156
+ next true if query_val.nil?
157
+ next true if query_val.is_a?(String) && query_val.empty?
158
+ next true if query_val.is_a?(Array) && query_val.empty?
159
+
160
+ query_val == candidate.public_send(name)
161
161
  end
162
+ end
162
163
 
163
- # Return a copy with the named attributes nil'd. Overrides
164
- # Pubid::Identifier#exclude because NIST's initialize is keyword-only
165
- # (initialize(**attributes)) while the inherited exclude rebuilds via
166
- # the positional self.class.new(attrs) form — passing a positional
167
- # hash to a keyword-only initializer raises ArgumentError. Rebuild
168
- # with the keyword splat instead.
169
- def exclude(*args)
170
- excluded_args = args.dup
171
- excluded_args << :date if excluded_args.delete(:year)
172
-
173
- attrs = self.class.attributes.each_with_object({}) do |(name, _), h|
174
- h[name] = excluded_args.include?(name) ? nil : public_send(name)
175
- end
176
- self.class.new(**attrs)
164
+ # Return a copy with the named attributes nil'd. Overrides
165
+ # Pubid::Identifier#exclude because NIST's initialize is keyword-only
166
+ # (initialize(**attributes)) while the inherited exclude rebuilds via
167
+ # the positional self.class.new(attrs) form — passing a positional
168
+ # hash to a keyword-only initializer raises ArgumentError. Rebuild
169
+ # with the keyword splat instead.
170
+ def exclude(*args)
171
+ excluded_args = args.dup
172
+ excluded_args << :date if excluded_args.delete(:year)
173
+
174
+ attrs = self.class.attributes.each_with_object({}) do |(name, _), h|
175
+ h[name] = excluded_args.include?(name) ? nil : public_send(name)
177
176
  end
177
+ self.class.new(**attrs)
178
+ end
178
179
 
179
- # Short-form supplement fragment ("sup", "sup1924", "supJan1924",
180
- # "suprev", " supJun1925-Jun1926"), rendered from the structured
181
- # component. A present-but-empty component is the bare "sup" marker; a
182
- # number-less date range gets the leading space the number would have
183
- # supplied. Shared by base and the per-series to_short_style overrides.
184
- def supplement_short
185
- return "" unless supplement
186
-
187
- prefix = supplement.range? && !number ? " " : ""
188
- rendered = supplement.to_s(:short)
189
- prefix + (rendered.empty? ? "sup" : rendered)
190
- end
180
+ # Short-form supplement fragment ("sup", "sup1924", "supJan1924",
181
+ # "suprev", " supJun1925-Jun1926"), rendered from the structured
182
+ # component. A present-but-empty component is the bare "sup" marker; a
183
+ # number-less date range gets the leading space the number would have
184
+ # supplied. Shared by base and the per-series to_short_style overrides.
185
+ def supplement_short
186
+ return "" unless supplement
187
+
188
+ prefix = supplement.range? && !number ? " " : ""
189
+ rendered = supplement.to_s(:short)
190
+ prefix + (rendered.empty? ? "sup" : rendered)
191
+ end
191
192
 
192
- # Compute revision from edition component for backward compatibility
193
- # @return [String, nil] revision string (e.g., "r5") or nil
194
- def revision
195
- return @revision if @revision
193
+ # Compute revision from edition component for backward compatibility
194
+ # @return [String, nil] revision string (e.g., "r5") or nil
195
+ def revision
196
+ return @revision if @revision
196
197
 
197
- # Compute from edition component if available
198
- if edition&.type && edition.id
199
- "#{edition.type}#{edition.id}"
200
- end
198
+ # Compute from edition component if available
199
+ if edition&.type && edition.id
200
+ "#{edition.type}#{edition.id}"
201
201
  end
202
+ end
203
+
204
+ # Backward compatibility: translation method returns translation_component
205
+ # This allows tests to use parsed.translation.language instead of parsed.translation_component.language
206
+ def translation
207
+ translation_component
208
+ end
209
+
210
+ # Backward compatibility: language method returns translation_component
211
+ # This allows tests to use parsed.language instead of parsed.translation_component
212
+ def language
213
+ translation_component
214
+ end
202
215
 
203
- # Backward compatibility: translation method returns translation_component
204
- # This allows tests to use parsed.translation.language instead of parsed.translation_component.language
205
- def translation
206
- translation_component
216
+ # Override parent render to pass NIST-specific format option through
217
+ # to the renderer. Pubid::Identifier#render only forwards
218
+ # :with_edition; NIST needs :nist_format for multi-format support.
219
+ def render(format: :human, **opts)
220
+ registry = self.class.format_registry
221
+ unless registry
222
+ raise ArgumentError, "No format registry configured on #{self.class}"
207
223
  end
208
224
 
209
- # Backward compatibility: language method returns translation_component
210
- # This allows tests to use parsed.language instead of parsed.translation_component
211
- def language
212
- translation_component
225
+ renderer = registry.renderer_for(format)
226
+ unless renderer
227
+ raise ArgumentError, "No renderer registered for format: #{format}"
213
228
  end
214
229
 
215
- # Override parent render to pass NIST-specific format option through
216
- # to the renderer. Pubid::Identifier#render only forwards
217
- # :with_edition; NIST needs :nist_format for multi-format support.
218
- def render(format: :human, **opts)
219
- registry = self.class.format_registry
220
- unless registry
221
- raise ArgumentError, "No format registry configured on #{self.class}"
222
- end
230
+ renderer.new(self).render(**opts)
231
+ end
223
232
 
224
- renderer = registry.renderer_for(format)
225
- unless renderer
226
- raise ArgumentError, "No renderer registered for format: #{format}"
227
- end
233
+ # Generate identifier string in specified format
234
+ # @param format [:full, :long, :abbreviated, :short, :mr] output format
235
+ def to_s(format = nil)
236
+ # Handle both keyword argument (hash) and positional argument (symbol/string)
237
+ format = format[:format] if format.is_a?(Hash)
238
+
239
+ # Default to parsed_format if available (preserves input format on round-trip)
240
+ # Falls back to :short format for output (normalization)
241
+ # Explicit format parameter always overrides parsed_format
242
+ render(format: :human,
243
+ nist_format: format || parsed_format&.to_sym || :short)
244
+ end
228
245
 
229
- renderer.new(self).render(**opts)
246
+ # Returns weight based on amount of defined attributes
247
+ # Used for ranking identifiers by specificity for conflict resolution
248
+ # @return [Integer] weight score (higher = more specific)
249
+ def weight
250
+ self.class.attributes.keys.inject(0) do |sum, key|
251
+ val = public_send(key)
252
+ val && !val.to_s.empty? ? sum + 1 : sum
230
253
  end
254
+ end
231
255
 
232
- # Generate identifier string in specified format
233
- # @param format [:full, :long, :abbreviated, :short, :mr] output format
234
- def to_s(format = nil)
235
- # Handle both keyword argument (hash) and positional argument (symbol/string)
236
- format = format[:format] if format.is_a?(Hash)
237
-
238
- # Default to parsed_format if available (preserves input format on round-trip)
239
- # Falls back to :short format for output (normalization)
240
- # Explicit format parameter always overrides parsed_format
241
- render(format: :human, nist_format: format || parsed_format&.to_sym || :short)
256
+ # Merge another document into this one
257
+ # Used for combining document data, preferring more specific values
258
+ # @param document [Base] another NIST document to merge
259
+ # @return [Base] self with merged attributes
260
+ def merge(document)
261
+ return self unless document.is_a?(Pubid::Nist::Identifier)
262
+
263
+ attrs = self.class.attributes
264
+ attrs.each_key do |var_name|
265
+ next if var_name == :rendering_style
266
+ next if var_name == :parsed_format
267
+
268
+ current_val = public_send(var_name)
269
+ new_val = document.public_send(var_name)
270
+ next unless new_val
271
+
272
+ should_merge = case var_name
273
+ when :publisher, :series, :number
274
+ true
275
+ when :edition
276
+ current_val.nil? || edition_greater?(new_val,
277
+ current_val)
278
+ when :volume, :part, :version, :revision
279
+ current_val.nil? || (new_val.to_s.length > current_val.to_s.length)
280
+ when :supplement, :errata, :index, :insert, :section, :appendix, :translation
281
+ true
282
+ when :year, :month, :update, :draft
283
+ true
284
+ else
285
+ false
286
+ end
287
+
288
+ public_send(:"#{var_name}=", new_val) if should_merge
242
289
  end
243
290
 
244
- # Returns weight based on amount of defined attributes
245
- # Used for ranking identifiers by specificity for conflict resolution
246
- # @return [Integer] weight score (higher = more specific)
247
- def weight
248
- self.class.attributes.keys.inject(0) do |sum, key|
249
- val = public_send(key)
250
- val && !val.to_s.empty? ? sum + 1 : sum
251
- end
252
- end
291
+ self
292
+ end
253
293
 
254
- # Merge another document into this one
255
- # Used for combining document data, preferring more specific values
256
- # @param document [Base] another NIST document to merge
257
- # @return [Base] self with merged attributes
258
- def merge(document)
259
- return self unless document.is_a?(Base)
260
-
261
- attrs = self.class.attributes
262
- attrs.each_key do |var_name|
263
- next if var_name == :rendering_style
264
- next if var_name == :parsed_format
265
-
266
- current_val = public_send(var_name)
267
- new_val = document.public_send(var_name)
268
- next unless new_val
269
-
270
- should_merge = case var_name
271
- when :publisher, :series, :number
272
- true
273
- when :edition
274
- current_val.nil? || edition_greater?(new_val,
275
- current_val)
276
- when :volume, :part, :version, :revision
277
- current_val.nil? || (new_val.to_s.length > current_val.to_s.length)
278
- when :supplement, :errata, :index, :insert, :section, :appendix, :translation
279
- true
280
- when :year, :month, :update, :draft
281
- true
282
- else
283
- false
284
- end
294
+ # Helper to compare edition values numerically
295
+ # @return [Boolean] true if edition1 is greater than edition2
296
+ def edition_greater?(edition1, edition2)
297
+ num1 = extract_edition_number(edition1)
298
+ num2 = extract_edition_number(edition2)
299
+ num1 && num2 && num1 > num2
300
+ end
285
301
 
286
- public_send(:"#{var_name}=", new_val) if should_merge
287
- end
302
+ # Extract numeric value from edition (r3 -> 3, r5 -> 5, e2 -> 2)
303
+ # @return [Integer, nil] the edition number or nil if not extractable
304
+ def extract_edition_number(edition)
305
+ # Handle both String and Edition component
306
+ edition_str = edition.to_s
307
+ # Match patterns like r3, r5, e2, etc.
308
+ match = edition_str.match(/^[er]?(\d+)$/)
309
+ match ? match[1].to_i : nil
310
+ end
288
311
 
289
- self
290
- end
312
+ public
291
313
 
292
- # Helper to compare edition values numerically
293
- # @return [Boolean] true if edition1 is greater than edition2
294
- def edition_greater?(edition1, edition2)
295
- num1 = extract_edition_number(edition1)
296
- num2 = extract_edition_number(edition2)
297
- num1 && num2 && num1 > num2
298
- end
314
+ def to_full_style
315
+ # "National Institute of Standards and Technology Special Publication 800-27, Revision A"
316
+ result = publisher_full_name
317
+ result += " #{series_full_name}" if series
318
+ result += " #{number.value}" if number
319
+ result += parts.map { |p| "-#{p}" }.join if parts&.any?
299
320
 
300
- # Extract numeric value from edition (r3 -> 3, r5 -> 5, e2 -> 2)
301
- # @return [Integer, nil] the edition number or nil if not extractable
302
- def extract_edition_number(edition)
303
- # Handle both String and Edition component
304
- edition_str = edition.to_s
305
- # Match patterns like r3, r5, e2, etc.
306
- match = edition_str.match(/^[er]?(\d+)$/)
307
- match ? match[1].to_i : nil
321
+ # Render volume and issue number in long form: "Vol. 6, No. 12"
322
+ if volume && issue_number
323
+ result += " Vol. #{volume}, #{issue_number.to_s(:long)}"
324
+ elsif volume
325
+ result += " Vol. #{volume}"
308
326
  end
309
327
 
310
- public
311
-
312
- def to_full_style
313
- # "National Institute of Standards and Technology Special Publication 800-27, Revision A"
314
- result = publisher_full_name
315
- result += " #{series_full_name}" if series
316
- result += " #{number.value}" if number
317
- result += parts.map { |p| "-#{p}" }.join if parts&.any?
328
+ # NEW: Use edition component properly
329
+ result += " #{edition.to_s(:long)}" if edition
318
330
 
319
- # Render volume and issue number in long form: "Vol. 6, No. 12"
320
- if volume && issue_number
321
- result += " Vol. #{volume}, #{issue_number.to_s(:long)}"
322
- elsif volume
323
- result += " Vol. #{volume}"
324
- end
331
+ result += ", Revision #{revision.sub(/^r/, '')}" if revision
325
332
 
326
- # NEW: Use edition component properly
327
- result += " #{edition.to_s(:long)}" if edition
333
+ # V2: Use version_component
334
+ result += " #{version_component.to_s(:long)}" if version_component
328
335
 
329
- result += ", Revision #{revision.sub(/^r/, '')}" if revision
336
+ # V2: Use update_component
337
+ result += " #{update_component.to_s(:long)}" if update_component
330
338
 
331
- # V2: Use version_component
332
- result += " #{version_component.to_s(:long)}" if version_component
339
+ # V2: Use stage
340
+ result += " #{stage.to_s(:long)}" if stage
333
341
 
334
- # V2: Use update_component
335
- result += " #{update_component.to_s(:long)}" if update_component
342
+ # V2: Use translation_component (already includes space)
343
+ result += translation_component.to_s(:long) if translation_component
336
344
 
337
- # V2: Use stage
338
- result += " #{stage.to_s(:long)}" if stage
345
+ result
346
+ end
339
347
 
340
- # V2: Use translation_component (already includes space)
341
- result += translation_component.to_s(:long) if translation_component
348
+ def to_abbreviated_style
349
+ # "Natl. Inst. Stand. Technol. Spec. Publ. 800-57 Part 1, Revision 4"
350
+ result = publisher_abbreviated_name
351
+ result += " #{series_abbreviated_name}" if series
352
+ result += " #{number}" if number
353
+ result += " Part #{parts.first}" if parts&.any?
342
354
 
343
- result
344
- end
355
+ # NEW: Use edition component properly
356
+ result += " #{edition.to_s(:abbrev)}" if edition
345
357
 
346
- def to_abbreviated_style
347
- # "Natl. Inst. Stand. Technol. Spec. Publ. 800-57 Part 1, Revision 4"
348
- result = publisher_abbreviated_name
349
- result += " #{series_abbreviated_name}" if series
350
- result += " #{number}" if number
351
- result += " Part #{parts.first}" if parts&.any?
358
+ result += ", Revision #{revision}" if revision
352
359
 
353
- # NEW: Use edition component properly
354
- result += " #{edition.to_s(:abbrev)}" if edition
360
+ # V2: Use version_component
361
+ result += " #{version_component.to_s(:abbrev)}" if version_component
355
362
 
356
- result += ", Revision #{revision}" if revision
363
+ # V2: Use update_component
364
+ result += " #{update_component.to_s(:abbrev)}" if update_component
357
365
 
358
- # V2: Use version_component
359
- result += " #{version_component.to_s(:abbrev)}" if version_component
366
+ # V2: Use stage
367
+ result += " #{stage.to_s(:abbrev)}" if stage
360
368
 
361
- # V2: Use update_component
362
- result += " #{update_component.to_s(:abbrev)}" if update_component
369
+ # V2: Use translation_component
370
+ result += ", #{translation_component.to_s(:abbrev)}" if translation_component
363
371
 
364
- # V2: Use stage
365
- result += " #{stage.to_s(:abbrev)}" if stage
372
+ result
373
+ end
366
374
 
367
- # V2: Use translation_component
368
- result += ", #{translation_component.to_s(:abbrev)}" if translation_component
375
+ def to_short_style
376
+ # "SP 800-187" or "NIST SP 800-187" - handle compound series properly
377
+ result = ""
378
+
379
+ # Determine effective publisher
380
+ # Only show publisher if it was explicitly parsed (either directly or from series prefix)
381
+ effective_publisher = if publisher && publisher_was_parsed
382
+ # Publisher was in input (either as separate field or extracted from "NBS CS" series)
383
+ publisher.to_s
384
+ else
385
+ # No publisher in input, don't show default
386
+ nil
387
+ end
388
+
389
+ # Determine effective series - PREFER series_code if subclass defines it
390
+ # This allows normalization (e.g., LCIRC → LC in LetterCircular)
391
+ effective_series = if series_code
392
+ series_code
393
+ elsif series
394
+ series.to_s
395
+ end
369
396
 
370
- result
397
+ # Special handling for compound series that include publisher prefix
398
+ # If series starts with "NBS " (like "NBS CIRC"), use it as-is
399
+ if effective_series&.start_with?("NBS ")
400
+ result += effective_series
401
+ elsif effective_publisher && effective_series
402
+ result += "#{effective_publisher} #{effective_series}"
403
+ elsif effective_series && publisher_was_parsed
404
+ # Only add "NIST" prefix if publisher was explicitly in the input
405
+ result += "NIST #{effective_series}"
406
+ elsif effective_series
407
+ # No publisher in input, just show series without prefix
408
+ result += effective_series
371
409
  end
372
410
 
373
- def to_short_style
374
- # "SP 800-187" or "NIST SP 800-187" - handle compound series properly
375
- result = ""
376
-
377
- # Determine effective publisher
378
- # Only show publisher if it was explicitly parsed (either directly or from series prefix)
379
- effective_publisher = if publisher && publisher_was_parsed
380
- # Publisher was in input (either as separate field or extracted from "NBS CS" series)
381
- publisher.to_s
382
- else
383
- # No publisher in input, don't show default
384
- nil
385
- end
386
-
387
- # Determine effective series - PREFER series_code if subclass defines it
388
- # This allows normalization (e.g., LCIRC → LC in LetterCircular)
389
- effective_series = if series_code
390
- series_code
391
- elsif series
392
- series.to_s
393
- end
394
-
395
- # Special handling for compound series that include publisher prefix
396
- # If series starts with "NBS " (like "NBS CIRC"), use it as-is
397
- if effective_series&.start_with?("NBS ")
398
- result += effective_series
399
- elsif effective_publisher && effective_series
400
- result += "#{effective_publisher} #{effective_series}"
401
- elsif effective_series && publisher_was_parsed
402
- # Only add "NIST" prefix if publisher was explicitly in the input
403
- result += "NIST #{effective_series}"
404
- elsif effective_series
405
- # No publisher in input, just show series without prefix
406
- result += effective_series
407
- end
411
+ result += " #{number}" if number
412
+ result += parts.map { |p| "-#{p}" }.join if parts&.any?
408
413
 
409
- result += " #{number}" if number
410
- result += parts.map { |p| "-#{p}" }.join if parts&.any?
414
+ # Append every optional component the parser may have attached
415
+ # (volume, part, edition, version, supplement, update, stage, ...).
416
+ # Shared with the lossy subclasses so they cannot silently drop a
417
+ # distinguishing component and collide with another document.
418
+ result += append_short_components
411
419
 
412
- # Append every optional component the parser may have attached
413
- # (volume, part, edition, version, supplement, update, stage, ...).
414
- # Shared with the lossy subclasses so they cannot silently drop a
415
- # distinguishing component and collide with another document.
416
- result += append_short_components
420
+ result
421
+ end
417
422
 
418
- result
423
+ # Render the optional component "tail" that follows
424
+ # publisher/series/number in short (human) form. Extracted from
425
+ # #to_short_style so subclasses that build a series-specific prefix can
426
+ # reuse it instead of hand-listing components (and forgetting some).
427
+ #
428
+ # skip_part: true lets a series that renders its part differently
429
+ # (e.g. FIPS uses dash "-1", not "pt1") emit the part itself and skip
430
+ # the generic part rendering here, while still getting volume/edition/
431
+ # supplement/etc.
432
+ def append_short_components(skip_part: false)
433
+ result = ""
434
+ effective_part = skip_part ? nil : part
435
+
436
+ # Volume and Part components (v6n1 notation for CSM, pt1 for SP)
437
+ if volume.is_a?(Components::Volume) && effective_part.is_a?(Components::Part)
438
+ # CSM series: v#n# notation
439
+ result += " #{volume}#{effective_part}"
440
+ elsif effective_part.is_a?(Components::Part)
441
+ # SP and other series: use Part.type to determine format
442
+ result += effective_part.to_s
443
+ # Legacy: Render standalone volume (not part of v#n#)
444
+ elsif volume && !issue_number && !effective_part
445
+ vol_str = volume.is_a?(Components::Volume) ? volume.to_s : "v#{volume}"
446
+ result += vol_str
447
+ elsif volume && issue_number
448
+ # Render volume and issue number in short form: "v6n12"
449
+ vol_str = volume.is_a?(Components::Volume) ? volume.to_s : "v#{volume}"
450
+ result += "#{vol_str}n#{issue_number.number}"
419
451
  end
420
452
 
421
- # Render the optional component "tail" that follows
422
- # publisher/series/number in short (human) form. Extracted from
423
- # #to_short_style so subclasses that build a series-specific prefix can
424
- # reuse it instead of hand-listing components (and forgetting some).
425
- #
426
- # skip_part: true lets a series that renders its part differently
427
- # (e.g. FIPS uses dash "-1", not "pt1") emit the part itself and skip
428
- # the generic part rendering here, while still getting volume/edition/
429
- # supplement/etc.
430
- def append_short_components(skip_part: false)
431
- result = ""
432
- effective_part = skip_part ? nil : part
433
-
434
- # Volume and Part components (v6n1 notation for CSM, pt1 for SP)
435
- if volume.is_a?(Components::Volume) && effective_part.is_a?(Components::Part)
436
- # CSM series: v#n# notation
437
- result += " #{volume}#{effective_part}"
438
- elsif effective_part.is_a?(Components::Part)
439
- # SP and other series: use Part.type to determine format
440
- result += effective_part.to_s
441
- # Legacy: Render standalone volume (not part of v#n#)
442
- elsif volume && !issue_number && !effective_part
443
- vol_str = volume.is_a?(Components::Volume) ? volume.to_s : "v#{volume}"
444
- result += vol_str
445
- elsif volume && issue_number
446
- # Render volume and issue number in short form: "v6n12"
447
- vol_str = volume.is_a?(Components::Volume) ? volume.to_s : "v#{volume}"
448
- result += "#{vol_str}n#{issue_number.number}"
453
+ # Use edition component properly (e2, e2021, r5, -3)
454
+ # NO space before edition when number present (per NIST spec)
455
+ # Only add space for bare edition (no number case) or if original_prefix has specific format
456
+ if edition
457
+ if edition.original_prefix && !edition.original_prefix.empty?
458
+ # original_prefix includes the full prefix (e.g., " Rev. " for verbose format)
459
+ result += edition.to_s
460
+ elsif number
461
+ # Number present, NO space: "800-53r5"
462
+ result += edition.to_s
463
+ else
464
+ # Bare edition, add space: " r5"
465
+ result += " #{edition}"
449
466
  end
467
+ end
450
468
 
451
- # Use edition component properly (e2, e2021, r5, -3)
452
- # NO space before edition when number present (per NIST spec)
453
- # Only add space for bare edition (no number case) or if original_prefix has specific format
454
- if edition
455
- if edition.original_prefix && !edition.original_prefix.empty?
456
- # original_prefix includes the full prefix (e.g., " Rev. " for verbose format)
457
- result += edition.to_s
458
- elsif number
459
- # Number present, NO space: "800-53r5"
460
- result += edition.to_s
461
- else
462
- # Bare edition, add space: " r5"
463
- result += " #{edition}"
464
- end
465
- end
469
+ # Use version_component if available, else use version string.
470
+ # Attach directly (no leading space) to match edition rendering
471
+ # (e.g. "800-53r5"), so version reads "800-45ver2" not
472
+ # "800-45 ver2".
473
+ if version_component
474
+ result += version_component.to_s(:short)
475
+ elsif version
476
+ result += "ver#{version}"
477
+ end
466
478
 
467
- # Use version_component if available, else use version string.
468
- # Attach directly (no leading space) to match edition rendering
469
- # (e.g. "800-53r5"), so version reads "800-45ver2" not
470
- # "800-45 ver2".
471
- if version_component
472
- result += version_component.to_s(:short)
473
- elsif version
474
- result += "ver#{version}"
475
- end
479
+ # Add supplement. NIST/NBS canonical short form is single-p "sup"
480
+ # with the suffix attached directly, no dash (relaton-data-nist
481
+ # uses "sup2", "sup1940", "supA"); date-range keeps its inner dash.
482
+ # Rendered from the structured component; a present-but-empty
483
+ # component is the bare "sup" marker.
484
+ result += supplement_short
485
+
486
+ # Add other attributes
487
+ result += errata.to_s if errata
488
+ result += "index" if index
489
+ result += "insert" if insert
490
+ result += "sec#{section}" if section
491
+ result += "app" if appendix
492
+
493
+ # Add addendum - render as " Add." suffix
494
+ if addendum || addendum_number
495
+ result += " Add."
496
+ end
476
497
 
477
- # Add supplement. NIST/NBS canonical short form is single-p "sup"
478
- # with the suffix attached directly, no dash (relaton-data-nist
479
- # uses "sup2", "sup1940", "supA"); date-range keeps its inner dash.
480
- # Rendered from the structured component; a present-but-empty
481
- # component is the bare "sup" marker.
482
- result += supplement_short
483
-
484
- # Add other attributes
485
- result += errata.to_s if errata
486
- result += "index" if index
487
- result += "insert" if insert
488
- result += "sec#{section}" if section
489
- result += "app" if appendix
490
-
491
- # Add addendum - render as " Add." suffix
492
- if addendum || addendum_number
493
- result += " Add."
494
- end
498
+ # Use update_component if available, else use update string
499
+ if update_component
500
+ result += update_component.to_s(:short)
501
+ elsif update
502
+ result += "-upd#{update}"
503
+ end
495
504
 
496
- # Use update_component if available, else use update string
497
- if update_component
498
- result += update_component.to_s(:short)
499
- elsif update
500
- result += "-upd#{update}"
501
- end
505
+ # Add draft - render as {N}pd if draft_number present
506
+ if draft_number
507
+ result += " #{draft_number}pd"
508
+ elsif draft&.to_s&.include?("draft") && !draft.to_s.include?("Draft)")
509
+ result += "-draft"
510
+ end
502
511
 
503
- # Add draft - render as {N}pd if draft_number present
504
- if draft_number
505
- result += " #{draft_number}pd"
506
- elsif draft&.to_s&.include?("draft") && !draft.to_s.include?("Draft)")
507
- result += "-draft"
508
- end
512
+ # Add stage component (at end, before translation)
513
+ if stage
514
+ result += " #{stage.to_s(:short)}"
515
+ end
509
516
 
510
- # Add stage component (at end, before translation)
511
- if stage
512
- result += " #{stage.to_s(:short)}"
513
- end
517
+ # Use translation_component if available, else use translation string
518
+ # Note: translation_component.to_s already includes the space prefix
519
+ if translation_component
520
+ result += translation_component.to_s(:short)
521
+ elsif translation
522
+ result += " #{translation}"
523
+ end
514
524
 
515
- # Use translation_component if available, else use translation string
516
- # Note: translation_component.to_s already includes the space prefix
517
- if translation_component
518
- result += translation_component.to_s(:short)
519
- elsif translation
520
- result += " #{translation}"
521
- end
525
+ result
526
+ end
522
527
 
523
- result
524
- end
528
+ def to_mr_style
529
+ # "NIST.SP.800-116r1.ipd" (machine-readable with dots)
530
+ result = (publisher || "NIST").to_s
525
531
 
526
- def to_mr_style
527
- # "NIST.SP.800-116r1.ipd" (machine-readable with dots)
528
- result = (publisher || "NIST").to_s
532
+ # Determine effective series - PREFER series_code if subclass defines it
533
+ # This allows normalization (e.g., LCIRC → LC in LetterCircular)
534
+ effective_series = if series_code
535
+ series_code
536
+ elsif series
537
+ series.to_s
538
+ end
529
539
 
530
- # Determine effective series - PREFER series_code if subclass defines it
531
- # This allows normalization (e.g., LCIRC → LC in LetterCircular)
532
- effective_series = if series_code
533
- series_code
534
- elsif series
535
- series.to_s
536
- end
540
+ result += ".#{effective_series}" if effective_series
541
+ result += ".#{number}" if number
542
+ result += parts.map { |p| "-#{p}" }.join if parts&.any?
537
543
 
538
- result += ".#{effective_series}" if effective_series
539
- result += ".#{number}" if number
540
- result += parts.map { |p| "-#{p}" }.join if parts&.any?
544
+ result += append_mr_components
541
545
 
542
- result += append_mr_components
546
+ result
547
+ end
543
548
 
544
- result
549
+ # Render the optional component tail for machine-readable (MR) form.
550
+ # Mirrors #append_short_components so the MR output is just as lossless;
551
+ # subclasses that override #to_mr_style reuse this instead of
552
+ # hand-listing components. skip_part: behaves as in the short helper.
553
+ def append_mr_components(skip_part: false)
554
+ result = ""
555
+ effective_part = skip_part ? nil : part
556
+
557
+ # Volume / Part components (pt1, v6n1, etc.)
558
+ if volume.is_a?(Components::Volume) && effective_part.is_a?(Components::Part)
559
+ result += "#{volume}#{effective_part}"
560
+ elsif effective_part.is_a?(Components::Part)
561
+ result += effective_part.to_s
562
+ elsif volume && !issue_number && !effective_part
563
+ result += (volume.is_a?(Components::Volume) ? volume.to_s : "v#{volume}")
564
+ elsif volume && issue_number
565
+ vol_str = volume.is_a?(Components::Volume) ? volume.to_s : "v#{volume}"
566
+ result += "#{vol_str}n#{issue_number.number}"
545
567
  end
546
568
 
547
- # Render the optional component tail for machine-readable (MR) form.
548
- # Mirrors #append_short_components so the MR output is just as lossless;
549
- # subclasses that override #to_mr_style reuse this instead of
550
- # hand-listing components. skip_part: behaves as in the short helper.
551
- def append_mr_components(skip_part: false)
552
- result = ""
553
- effective_part = skip_part ? nil : part
554
-
555
- # Volume / Part components (pt1, v6n1, etc.)
556
- if volume.is_a?(Components::Volume) && effective_part.is_a?(Components::Part)
557
- result += "#{volume}#{effective_part}"
558
- elsif effective_part.is_a?(Components::Part)
559
- result += effective_part.to_s
560
- elsif volume && !issue_number && !effective_part
561
- result += (volume.is_a?(Components::Volume) ? volume.to_s : "v#{volume}")
562
- elsif volume && issue_number
563
- vol_str = volume.is_a?(Components::Volume) ? volume.to_s : "v#{volume}"
564
- result += "#{vol_str}n#{issue_number.number}"
565
- end
569
+ # Use edition component - NO space before edition in MR format (per NIST spec)
570
+ result += edition.to_s if edition
566
571
 
567
- # Use edition component - NO space before edition in MR format (per NIST spec)
568
- result += edition.to_s if edition
572
+ # Use version_component
573
+ result += version_component.to_s(:mr) if version_component
569
574
 
570
- # Use version_component
571
- result += version_component.to_s(:mr) if version_component
575
+ # Supplement (e.g. ".9981sup7") - keep distinct documents distinct
576
+ result += supplement_short
572
577
 
573
- # Supplement (e.g. ".9981sup7") - keep distinct documents distinct
574
- result += supplement_short
578
+ # Use update_component
579
+ result += update_component.to_s(:mr) if update_component
575
580
 
576
- # Use update_component
577
- result += update_component.to_s(:mr) if update_component
581
+ # Use stage
582
+ result += ".#{stage.to_s(:mr)}" if stage
578
583
 
579
- # Use stage
580
- result += ".#{stage.to_s(:mr)}" if stage
584
+ # Add addendum - render as ".Add." suffix in MR format
585
+ if addendum || addendum_number
586
+ result += ".Add."
587
+ end
581
588
 
582
- # Add addendum - render as ".Add." suffix in MR format
583
- if addendum || addendum_number
584
- result += ".Add."
585
- end
589
+ # Use translation_component
590
+ result += translation_component.to_s(:mr) if translation_component
586
591
 
587
- # Use translation_component
588
- result += translation_component.to_s(:mr) if translation_component
592
+ result
593
+ end
589
594
 
590
- result
591
- end
595
+ def series_full_name
596
+ {
597
+ "SP" => "Special Publication",
598
+ "FIPS" => "Federal Information Processing Standards",
599
+ "IR" => "Interagency Report",
600
+ "TN" => "Technical Note",
601
+ }[series] || series
602
+ end
592
603
 
593
- def series_full_name
594
- {
595
- "SP" => "Special Publication",
596
- "FIPS" => "Federal Information Processing Standards",
597
- "IR" => "Interagency Report",
598
- "TN" => "Technical Note",
599
- }[series] || series
600
- end
604
+ def series_abbreviated_name
605
+ {
606
+ "SP" => "Spec. Publ.",
607
+ "FIPS" => "Fed. Inf. Proc. Stand.",
608
+ "IR" => "Interag. Rep.",
609
+ "TN" => "Tech. Note",
610
+ }[series&.to_s || series_code] || (series&.to_s || series_code)
611
+ end
601
612
 
602
- def series_abbreviated_name
603
- {
604
- "SP" => "Spec. Publ.",
605
- "FIPS" => "Fed. Inf. Proc. Stand.",
606
- "IR" => "Interag. Rep.",
607
- "TN" => "Tech. Note",
608
- }[series&.to_s || series_code] || (series&.to_s || series_code)
609
- end
610
-
611
- def publisher_full_name
612
- case publisher.to_s
613
- when "NBS"
614
- "National Bureau of Standards"
615
- when "NIST"
616
- "National Institute of Standards and Technology"
617
- else
618
- publisher.to_s
619
- end
613
+ def publisher_full_name
614
+ case publisher.to_s
615
+ when "NBS"
616
+ "National Bureau of Standards"
617
+ when "NIST"
618
+ "National Institute of Standards and Technology"
619
+ else
620
+ publisher.to_s
620
621
  end
622
+ end
621
623
 
622
- def publisher_abbreviated_name
623
- case publisher.to_s
624
- when "NBS"
625
- "Natl. Bur. Stand."
626
- when "NIST"
627
- "Natl. Inst. Stand. Technol."
628
- else
629
- publisher.to_s
630
- end
624
+ def publisher_abbreviated_name
625
+ case publisher.to_s
626
+ when "NBS"
627
+ "Natl. Bur. Stand."
628
+ when "NIST"
629
+ "Natl. Inst. Stand. Technol."
630
+ else
631
+ publisher.to_s
631
632
  end
633
+ end
632
634
 
633
- # Default publisher for series without explicit publisher
634
- # Subclasses can override
635
- def default_publisher
636
- "NIST"
637
- end
635
+ # Default publisher for series without explicit publisher
636
+ # Subclasses can override
637
+ def default_publisher
638
+ "NIST"
638
639
  end
639
640
  end
641
+
642
+ module Identifiers
643
+ # Backward-compatible alias: NIST's base class used to be
644
+ # Pubid::Nist::Identifiers::Base. It is now Pubid::Nist::Identifier; keep
645
+ # the old constant working for `class Foo < Base` declarations and any
646
+ # external reference.
647
+ Base = Pubid::Nist::Identifier
648
+ end
640
649
  end
641
650
  end