pubid 2.0.0.pre.alpha.1 → 2.0.0.pre.alpha.2
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.
- checksums.yaml +4 -4
- data/data/nist/update_codes.yaml +2 -0
- data/lib/pubid/amca/identifier.rb +39 -0
- data/lib/pubid/ansi/identifier.rb +42 -0
- data/lib/pubid/api/identifier.rb +47 -0
- data/lib/pubid/ashrae/identifier.rb +39 -0
- data/lib/pubid/asme/identifier.rb +46 -0
- data/lib/pubid/astm/identifier.rb +77 -0
- data/lib/pubid/bsi/identifier.rb +60 -0
- data/lib/pubid/ccsds/identifier.rb +68 -0
- data/lib/pubid/ccsds/identifiers/base.rb +11 -0
- data/lib/pubid/ccsds/single_identifier.rb +4 -1
- data/lib/pubid/cen_cenelec/identifier.rb +37 -0
- data/lib/pubid/cie/identifier.rb +53 -0
- data/lib/pubid/components/factory.rb +50 -0
- data/lib/pubid/components/typed_stage.rb +4 -0
- data/lib/pubid/components.rb +1 -0
- data/lib/pubid/csa/identifier.rb +56 -0
- data/lib/pubid/etsi/identifier.rb +43 -0
- data/lib/pubid/identifier.rb +8 -1
- data/lib/pubid/idf/identifier.rb +60 -0
- data/lib/pubid/iec/builder.rb +2 -1
- data/lib/pubid/iec/components/code.rb +2 -1
- data/lib/pubid/iec/components/publisher.rb +2 -1
- data/lib/pubid/iec/identifier.rb +235 -0
- data/lib/pubid/iec/identifiers/base.rb +4 -0
- data/lib/pubid/iec/identifiers/consolidated_identifier.rb +0 -4
- data/lib/pubid/iec/identifiers/fragment_identifier.rb +0 -4
- data/lib/pubid/iec/identifiers/sheet_identifier.rb +0 -4
- data/lib/pubid/iec/identifiers/vap_identifier.rb +0 -4
- data/lib/pubid/iec/parser.rb +7 -2
- data/lib/pubid/iec/urn_generator.rb +57 -171
- data/lib/pubid/iec/urn_parser.rb +53 -252
- data/lib/pubid/ieee/identifier.rb +41 -0
- data/lib/pubid/iho/identifier.rb +42 -0
- data/lib/pubid/iho/identifiers/base.rb +1 -1
- data/lib/pubid/iho/identifiers/bibliographic.rb +0 -4
- data/lib/pubid/iho/identifiers/circular_letter.rb +0 -4
- data/lib/pubid/iho/identifiers/miscellaneous.rb +0 -4
- data/lib/pubid/iho/identifiers/publication.rb +0 -4
- data/lib/pubid/iho/identifiers/standard.rb +0 -4
- data/lib/pubid/iho/urn_generator.rb +1 -1
- data/lib/pubid/iso/builder.rb +5 -1
- data/lib/pubid/iso/identifier.rb +261 -0
- data/lib/pubid/iso/parser.rb +4 -2
- data/lib/pubid/iso/scheme.rb +6 -0
- data/lib/pubid/iso/single_identifier.rb +6 -3
- data/lib/pubid/iso/urn_generator.rb +17 -3
- data/lib/pubid/iso/urn_parser.rb +16 -2
- data/lib/pubid/itu/identifier.rb +87 -22
- data/lib/pubid/jcgm/identifier.rb +43 -0
- data/lib/pubid/jis/identifier.rb +43 -0
- data/lib/pubid/nist/builder.rb +174 -5
- data/lib/pubid/nist/components/edition.rb +16 -0
- data/lib/pubid/nist/components/supplement.rb +88 -21
- data/lib/pubid/nist/identifier.rb +62 -0
- data/lib/pubid/nist/identifiers/base.rb +103 -24
- data/lib/pubid/nist/identifiers/circular_supplement.rb +1 -1
- data/lib/pubid/nist/identifiers/crpl_report.rb +1 -4
- data/lib/pubid/nist/identifiers/federal_information_processing_standards.rb +10 -0
- data/lib/pubid/nist/identifiers/report.rb +1 -2
- data/lib/pubid/nist/parser.rb +36 -3
- data/lib/pubid/nist/supplement_identifier.rb +8 -24
- data/lib/pubid/nist/urn_generator.rb +14 -8
- data/lib/pubid/nist.rb +1 -0
- data/lib/pubid/oiml/identifier.rb +50 -0
- data/lib/pubid/plateau/identifier.rb +57 -0
- data/lib/pubid/plateau.rb +1 -0
- data/lib/pubid/renderers/base.rb +34 -0
- data/lib/pubid/renderers/directives_renderer.rb +13 -14
- data/lib/pubid/renderers/guide_renderer.rb +5 -1
- data/lib/pubid/renderers/human_readable.rb +20 -8
- data/lib/pubid/renderers/iwa_renderer.rb +5 -1
- data/lib/pubid/renderers/supplement_renderer.rb +4 -1
- data/lib/pubid/rendering/context.rb +5 -2
- data/lib/pubid/sae/identifier.rb +23 -0
- data/lib/pubid/scheme.rb +12 -0
- data/lib/pubid/version.rb +1 -1
- metadata +5 -2
|
@@ -6,6 +6,49 @@ module Pubid
|
|
|
6
6
|
def self.parse(string)
|
|
7
7
|
Pubid::Jcgm.parse(string)
|
|
8
8
|
end
|
|
9
|
+
|
|
10
|
+
# Factory mirroring pubid 1.x's `Pubid::Jcgm::Identifier.create` API.
|
|
11
|
+
# Dispatches `:guide` (default) → Guide, `:gum_guide` → GumGuide.
|
|
12
|
+
TYPE_KEY_TO_KLASS = {
|
|
13
|
+
guide: "Guide",
|
|
14
|
+
gum_guide: "GumGuide",
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def self.create(type: nil, **opts)
|
|
18
|
+
klass = resolve_create_class(type)
|
|
19
|
+
attrs = coerce_create_attrs(opts)
|
|
20
|
+
ts = klass.const_defined?(:TYPED_STAGES) &&
|
|
21
|
+
klass.const_get(:TYPED_STAGES).find do |t|
|
|
22
|
+
t.stage_code&.to_sym == :published
|
|
23
|
+
end
|
|
24
|
+
attrs[:typed_stage] = ts if ts
|
|
25
|
+
klass.new(**attrs)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.resolve_create_class(type)
|
|
29
|
+
return Identifiers::Guide if type.nil?
|
|
30
|
+
|
|
31
|
+
klass_name = TYPE_KEY_TO_KLASS[type.to_sym]
|
|
32
|
+
raise ArgumentError, "Unknown JCGM type: #{type.inspect}" unless klass_name
|
|
33
|
+
|
|
34
|
+
Identifiers.const_get(klass_name)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.coerce_create_attrs(opts)
|
|
38
|
+
attrs = {
|
|
39
|
+
publisher: Pubid::Jcgm::Components::Publisher.new(
|
|
40
|
+
publisher: (opts[:publisher] || "JCGM").to_s,
|
|
41
|
+
),
|
|
42
|
+
}
|
|
43
|
+
if (v = opts[:number])
|
|
44
|
+
attrs[:number] = Pubid::Components::Code.new(value: v.to_s)
|
|
45
|
+
end
|
|
46
|
+
if (v = opts[:year])
|
|
47
|
+
attrs[:date] = Pubid::Components::Date.new(year: v.to_s)
|
|
48
|
+
end
|
|
49
|
+
attrs
|
|
50
|
+
end
|
|
51
|
+
private_class_method :resolve_create_class, :coerce_create_attrs
|
|
9
52
|
end
|
|
10
53
|
end
|
|
11
54
|
end
|
data/lib/pubid/jis/identifier.rb
CHANGED
|
@@ -13,6 +13,49 @@ module Pubid
|
|
|
13
13
|
rescue Parslet::ParseFailed => e
|
|
14
14
|
raise "Failed to parse JIS identifier '#{identifier}': #{e.message}"
|
|
15
15
|
end
|
|
16
|
+
|
|
17
|
+
# Factory mirroring pubid 1.x's `Pubid::Jis::Identifier.create` API.
|
|
18
|
+
#
|
|
19
|
+
# JIS uses a different attribute schema from most flavors — `series`
|
|
20
|
+
# (single letter A–Z), `number` (integer), `parts` (integer collection),
|
|
21
|
+
# `year` (integer), `language`. Publisher is hardcoded "JIS" on the
|
|
22
|
+
# instance and not a constructor kwarg; supplying `:publisher` is
|
|
23
|
+
# silently ignored.
|
|
24
|
+
#
|
|
25
|
+
# Type dispatch: :jis → Standard, :tr → TechnicalReport,
|
|
26
|
+
# :ts → TechnicalSpecification. Default → Standard.
|
|
27
|
+
def self.create(type: nil, **opts)
|
|
28
|
+
klass = if type
|
|
29
|
+
safe_locate_klass(type) || Identifiers::Standard
|
|
30
|
+
else
|
|
31
|
+
Identifiers::Standard
|
|
32
|
+
end
|
|
33
|
+
klass.new(**coerce_create_attrs(opts))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# JIS Scheme raises ArgumentError on miss; wrap.
|
|
37
|
+
def self.safe_locate_klass(type)
|
|
38
|
+
Scheme.locate_identifier_klass_by_type_code(type)
|
|
39
|
+
rescue ArgumentError
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.coerce_create_attrs(opts)
|
|
44
|
+
attrs = {}
|
|
45
|
+
attrs[:series] = opts[:series].to_s if opts[:series]
|
|
46
|
+
attrs[:number] = opts[:number].to_i if opts[:number]
|
|
47
|
+
if opts[:parts]
|
|
48
|
+
attrs[:parts] = Array(opts[:parts]).map(&:to_i)
|
|
49
|
+
end
|
|
50
|
+
attrs[:code] = opts[:code] if opts[:code]
|
|
51
|
+
attrs[:year] = opts[:year].to_i if opts[:year]
|
|
52
|
+
attrs[:language] = opts[:language].to_s if opts[:language]
|
|
53
|
+
attrs[:all_parts] = opts[:all_parts] if opts.key?(:all_parts)
|
|
54
|
+
# TODO(create-shim): :publisher silently ignored (JIS hardcodes
|
|
55
|
+
# "JIS"); supplement subclasses not yet wired through.
|
|
56
|
+
attrs
|
|
57
|
+
end
|
|
58
|
+
private_class_method :safe_locate_klass, :coerce_create_attrs
|
|
16
59
|
end
|
|
17
60
|
end
|
|
18
61
|
end
|
data/lib/pubid/nist/builder.rb
CHANGED
|
@@ -369,10 +369,30 @@ module Pubid
|
|
|
369
369
|
part_num = nil
|
|
370
370
|
extracted_revision = nil
|
|
371
371
|
|
|
372
|
+
# Accumulate supplement signals from the casts (a flat value string,
|
|
373
|
+
# the has_revision flag, and date-range start/end) and fold them into a
|
|
374
|
+
# single Components::Supplement at the end. They are intercepted (not
|
|
375
|
+
# assigned) because :supplement is now a component attribute, so a raw
|
|
376
|
+
# string must never be written to it directly.
|
|
377
|
+
supp = { value: nil, has_revision: false, range_start: nil,
|
|
378
|
+
range_end: nil, present: false }
|
|
379
|
+
capture_supplement = lambda do |k, v|
|
|
380
|
+
case k
|
|
381
|
+
when :supplement then supp[:value] = v
|
|
382
|
+
when :supplement_has_revision then supp[:has_revision] = !!v
|
|
383
|
+
when :supplement_date_range_start then supp[:range_start] = v
|
|
384
|
+
when :supplement_date_range_end then supp[:range_end] = v
|
|
385
|
+
else return false
|
|
386
|
+
end
|
|
387
|
+
supp[:present] = true
|
|
388
|
+
true
|
|
389
|
+
end
|
|
390
|
+
|
|
372
391
|
# Cast and assign all attributes
|
|
373
392
|
parsed_hash.each_pair do |key, value|
|
|
374
393
|
realized_components = cast(key.to_sym, value, parsed_hash) # Pass parsed_hash for context
|
|
375
394
|
next if realized_components.nil?
|
|
395
|
+
next if !realized_components.is_a?(Hash) && capture_supplement.call(key.to_sym, realized_components)
|
|
376
396
|
|
|
377
397
|
# Track number components
|
|
378
398
|
if key == :first_number && realized_components.is_a?(Components::Code)
|
|
@@ -427,6 +447,10 @@ module Pubid
|
|
|
427
447
|
# Skip assignment for second_number hashes - they'll be processed during compound number construction
|
|
428
448
|
next if sub_key == :second_number && sub_value.is_a?(Hash) && sub_value[:number_only]
|
|
429
449
|
|
|
450
|
+
# Intercept supplement signals into the accumulator instead of
|
|
451
|
+
# assigning them (supplement is now a component built at the end).
|
|
452
|
+
next if capture_supplement.call(sub_key, sub_value)
|
|
453
|
+
|
|
430
454
|
attrs = identifier.class.attributes
|
|
431
455
|
setter = "#{sub_key}="
|
|
432
456
|
if attrs.key?(sub_key.to_sym)
|
|
@@ -543,7 +567,16 @@ module Pubid
|
|
|
543
567
|
year_part = second_num.value.to_s
|
|
544
568
|
|
|
545
569
|
identifier.number = Components::Code.new(number: number_part)
|
|
546
|
-
|
|
570
|
+
supp[:value] = year_part
|
|
571
|
+
supp[:present] = true
|
|
572
|
+
elsif second_num.value.to_s.match?(/^(\d+)supp?$/)
|
|
573
|
+
# Pattern: "800-53sup"/"800-53supp" - bare marker on the compound
|
|
574
|
+
# second number. Strip it and isolate as supplement="" (single-p).
|
|
575
|
+
second_part = second_num.value.to_s.match(/^(\d+)supp?$/)[1]
|
|
576
|
+
compound = "#{first_num.value}-#{second_part}"
|
|
577
|
+
identifier.number = Components::Code.new(number: compound)
|
|
578
|
+
supp[:value] = ""
|
|
579
|
+
supp[:present] = true
|
|
547
580
|
elsif identifier.is_a?(Identifiers::TechnicalNote) &&
|
|
548
581
|
second_num.value.to_s.match?(/^(19|20)\d{2}$/)
|
|
549
582
|
# SPECIAL CASE FOR TN: second_num is edition year
|
|
@@ -633,9 +666,41 @@ module Pubid
|
|
|
633
666
|
identifier.revision_month = nil
|
|
634
667
|
end
|
|
635
668
|
|
|
669
|
+
# Fold the accumulated supplement signals into the single structured
|
|
670
|
+
# supplement component (the source of truth).
|
|
671
|
+
if (supp[:present] || supp[:has_revision]) &&
|
|
672
|
+
identifier.respond_to?(:supplement=)
|
|
673
|
+
identifier.supplement = supplement_from(
|
|
674
|
+
value: supp[:value], has_revision: supp[:has_revision],
|
|
675
|
+
range_start: supp[:range_start], range_end: supp[:range_end]
|
|
676
|
+
)
|
|
677
|
+
end
|
|
678
|
+
|
|
636
679
|
identifier
|
|
637
680
|
end
|
|
638
681
|
|
|
682
|
+
# Build a Components::Supplement from the builder's accumulated raw signals
|
|
683
|
+
# (flat value string, has_revision flag, fused date-range start/end). This
|
|
684
|
+
# is the one place raw supplement text becomes the structured component.
|
|
685
|
+
def supplement_from(value:, has_revision:, range_start:, range_end:)
|
|
686
|
+
if range_start || range_end
|
|
687
|
+
component = Components::Supplement.new
|
|
688
|
+
# Split the fused "Jun1925"/"Jun1926" strings into isolated start/end
|
|
689
|
+
# month+year nodes (start reuses :month/:year, end uses *_end).
|
|
690
|
+
if range_start && (m = range_start.match(/\A([A-Za-z]{3,9})(\d{4})\z/))
|
|
691
|
+
component.month = m[1]
|
|
692
|
+
component.year = m[2]
|
|
693
|
+
end
|
|
694
|
+
if range_end && (m = range_end.match(/\A([A-Za-z]{3,9})(\d{4})\z/))
|
|
695
|
+
component.month_end = m[1]
|
|
696
|
+
component.year_end = m[2]
|
|
697
|
+
end
|
|
698
|
+
component
|
|
699
|
+
else
|
|
700
|
+
Components::Supplement.from_raw(value, has_revision: has_revision)
|
|
701
|
+
end
|
|
702
|
+
end
|
|
703
|
+
|
|
639
704
|
# Convert month name to month number
|
|
640
705
|
# @param month_name [String] month abbreviation (Jan, Feb, Mar, etc.)
|
|
641
706
|
# @return [Integer] month number (1-12)
|
|
@@ -659,8 +724,109 @@ module Pubid
|
|
|
659
724
|
|
|
660
725
|
# Build CircularSupplement with base_identifier wrapping
|
|
661
726
|
# @param parsed_hash [Hash] the parsed supplement data
|
|
662
|
-
#
|
|
727
|
+
# Build a CIRC/LCIRC supplement. Most forms collapse onto the base
|
|
728
|
+
# identifier's normal class (Circular / LetterCircular) with isolated
|
|
729
|
+
# supplement attributes, so they share one model with every other series
|
|
730
|
+
# and are queryable by part. The V1-compat update forms (slash-year
|
|
731
|
+
# "118supp3/1926" → ".../Upd1-192603"; implicit revision "145r11/1925")
|
|
732
|
+
# render through an Update component the flat supplement model can't
|
|
733
|
+
# express, so they stay on the CircularSupplement wrapper for now.
|
|
663
734
|
def build_circular_supplement(parsed_hash)
|
|
735
|
+
if parsed_hash[:supplement_slash_year].is_a?(Hash) ||
|
|
736
|
+
parsed_hash[:implicit_supplement].is_a?(Hash)
|
|
737
|
+
return build_circular_supplement_wrapper(parsed_hash)
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
series_value = if parsed_hash[:circ_series].is_a?(Hash)
|
|
741
|
+
parsed_hash[:circ_series][:series]
|
|
742
|
+
else
|
|
743
|
+
parsed_hash[:series]
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
# Date-range supplement (no base document): a plain base identifier with
|
|
747
|
+
# no number, carrying the range. parsed_format is left at the default so
|
|
748
|
+
# a dotted MR input still normalizes to the spaced short form.
|
|
749
|
+
if parsed_hash[:supplement_date_range].is_a?(Hash)
|
|
750
|
+
range = parsed_hash[:supplement_date_range]
|
|
751
|
+
identifier = build({ series: series_value })
|
|
752
|
+
ms = range[:supp_month_start]&.to_s
|
|
753
|
+
ys = range[:supp_year_start]&.to_s
|
|
754
|
+
me = range[:supp_month_end]&.to_s
|
|
755
|
+
ye = range[:supp_year_end]&.to_s
|
|
756
|
+
identifier.supplement = supplement_from(
|
|
757
|
+
value: nil, has_revision: false,
|
|
758
|
+
range_start: (ms && ys ? "#{ms}#{ys}" : nil),
|
|
759
|
+
range_end: (me && ye ? "#{me}#{ye}" : nil)
|
|
760
|
+
)
|
|
761
|
+
return identifier
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
# Based supplement: build the base, then attach the supplement as an
|
|
765
|
+
# isolated component on that normal class.
|
|
766
|
+
identifier = build_circular_supplement_base(parsed_hash, series_value)
|
|
767
|
+
raw = if parsed_hash[:supplement_month_year]
|
|
768
|
+
parsed_hash[:supplement_month_year].to_s
|
|
769
|
+
elsif parsed_hash[:supplement_year]
|
|
770
|
+
parsed_hash[:supplement_year].to_s
|
|
771
|
+
else
|
|
772
|
+
"" # supplement_empty or bare marker
|
|
773
|
+
end
|
|
774
|
+
identifier.supplement = Components::Supplement.from_raw(raw)
|
|
775
|
+
identifier
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
# Build just the base identifier for a based CIRC/LCIRC supplement, from
|
|
779
|
+
# base_portion (number, optional edition "101e2" or letter suffix "378G")
|
|
780
|
+
# or the merged first_number fallback. Returns the normal class.
|
|
781
|
+
def build_circular_supplement_base(parsed_hash, series_value)
|
|
782
|
+
base_portion = parsed_hash[:base_portion]
|
|
783
|
+
unless base_portion
|
|
784
|
+
return build({
|
|
785
|
+
publisher: parsed_hash[:publisher],
|
|
786
|
+
series: series_value || parsed_hash[:series],
|
|
787
|
+
first_number: parsed_hash[:first_number],
|
|
788
|
+
parsed_format: parsed_hash[:parsed_format],
|
|
789
|
+
})
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
base_number = if base_portion.is_a?(Hash)
|
|
793
|
+
base_portion[:simple_number] || base_portion[:base_number]
|
|
794
|
+
else
|
|
795
|
+
base_portion
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
letter_suffix = nil
|
|
799
|
+
if base_portion.is_a?(Hash) && base_portion[:letter_suffix]
|
|
800
|
+
letter_suffix = base_portion[:letter_suffix].to_s.upcase
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
publisher_value = nil
|
|
804
|
+
if parsed_hash[:circ_series].is_a?(Hash) && parsed_hash[:circ_series][:series]
|
|
805
|
+
series_str = parsed_hash[:circ_series][:series].to_s
|
|
806
|
+
publisher_value = series_str.split.first if series_str.include?(" ")
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
has_edition = base_portion.is_a?(Hash) && base_portion[:edition_number]
|
|
810
|
+
|
|
811
|
+
base_number_with_suffix = base_number.to_s
|
|
812
|
+
base_number_with_suffix += letter_suffix if letter_suffix
|
|
813
|
+
if has_edition
|
|
814
|
+
base_number_with_suffix += "e#{base_portion[:edition_number]}"
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
base_hash = {
|
|
818
|
+
series: series_value,
|
|
819
|
+
first_number: base_number_with_suffix,
|
|
820
|
+
parsed_format: parsed_hash[:parsed_format],
|
|
821
|
+
}
|
|
822
|
+
base_hash[:publisher] = publisher_value if publisher_value
|
|
823
|
+
base_hash[:edition_e] = { edition_id: base_portion[:edition_number] } if has_edition
|
|
824
|
+
|
|
825
|
+
build(base_hash)
|
|
826
|
+
end
|
|
827
|
+
|
|
828
|
+
# @return [Identifiers::CircularSupplement] the supplement identifier
|
|
829
|
+
def build_circular_supplement_wrapper(parsed_hash)
|
|
664
830
|
supplement = Identifiers::CircularSupplement.new
|
|
665
831
|
|
|
666
832
|
# Extract series from circ_series if present (nested structure from parser)
|
|
@@ -1066,9 +1232,12 @@ module Pubid
|
|
|
1066
1232
|
end
|
|
1067
1233
|
end
|
|
1068
1234
|
|
|
1069
|
-
#
|
|
1070
|
-
#
|
|
1071
|
-
|
|
1235
|
+
# Pattern "9350sup"/"5893supp" - number with bare supplement marker
|
|
1236
|
+
# (no trailing payload). Accept both single-p "sup" and double-p
|
|
1237
|
+
# "supp" so the marker is isolated as supplement="" and rendered as
|
|
1238
|
+
# canonical single-p "sup", instead of staying baked into the number
|
|
1239
|
+
# as an opaque suffix. E.g. "NBS RPT 5893supp", "NBS MONO 32supp".
|
|
1240
|
+
if str_value =~ /^(\d+)supp?$/
|
|
1072
1241
|
return {
|
|
1073
1242
|
first_number: Components::Code.new(number: $1),
|
|
1074
1243
|
supplement: "",
|
|
@@ -49,6 +49,22 @@ module Pubid
|
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
# Identity ignores original_prefix: it records how the edition was
|
|
53
|
+
# spelled in the source ("Rev. " vs "r") for round-trip rendering
|
|
54
|
+
# only. "r1" and "Rev. 1" are the same edition.
|
|
55
|
+
def ==(other)
|
|
56
|
+
return false unless other.is_a?(self.class)
|
|
57
|
+
|
|
58
|
+
type == other.type && id == other.id &&
|
|
59
|
+
additional_text == other.additional_text
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
alias eql? ==
|
|
63
|
+
|
|
64
|
+
def hash
|
|
65
|
+
[self.class, type, id, additional_text].hash
|
|
66
|
+
end
|
|
67
|
+
|
|
52
68
|
private
|
|
53
69
|
|
|
54
70
|
# Build short format: "e2", "e2.June1908", "e2.1908", "e2.50", "r1963", "-April1909", "r1a"
|
|
@@ -9,17 +9,19 @@ module Pubid
|
|
|
9
9
|
# Represents supplement notation with number, year, month, or revision
|
|
10
10
|
#
|
|
11
11
|
# Examples:
|
|
12
|
-
# Supplement.new(number: "2").to_s(:short) # => "
|
|
13
|
-
# Supplement.new(year: "1925").to_s(:short) # => "
|
|
14
|
-
# Supplement.new(number: "3", year: "1926").to_s(:short) # => "
|
|
15
|
-
# Supplement.new(month: "Jan", year: "1924").to_s(:short) # => "
|
|
16
|
-
# Supplement.new(has_revision: true).to_s(:short) # => "
|
|
12
|
+
# Supplement.new(number: "2").to_s(:short) # => "sup2"
|
|
13
|
+
# Supplement.new(year: "1925").to_s(:short) # => "sup1925"
|
|
14
|
+
# Supplement.new(number: "3", year: "1926").to_s(:short) # => "sup3/1926"
|
|
15
|
+
# Supplement.new(month: "Jan", year: "1924").to_s(:short) # => "supJan1924"
|
|
16
|
+
# Supplement.new(has_revision: true).to_s(:short) # => "suprev"
|
|
17
17
|
class Supplement < Lutaml::Model::Serializable
|
|
18
18
|
attribute :number, :string # Supplement number (e.g., "2" in "supp2")
|
|
19
|
-
attribute :year, :string # Year (4 digits)
|
|
20
|
-
attribute :month, :string # Month abbreviation
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
attribute :year, :string # Year (4 digits); range START year
|
|
20
|
+
attribute :month, :string # Month abbreviation; range START month
|
|
21
|
+
# Date ranges reuse :month/:year as the start and add the end below. A
|
|
22
|
+
# present :month_end/:year_end is what marks a value as a range, so a
|
|
23
|
+
# lone month-year supplement is just a range with no end — and :year is
|
|
24
|
+
# the single field to filter on for both.
|
|
23
25
|
attribute :month_end, :string # Date range end month
|
|
24
26
|
attribute :year_end, :string # Date range end year
|
|
25
27
|
attribute :has_revision, :boolean, default: -> {
|
|
@@ -27,12 +29,36 @@ module Pubid
|
|
|
27
29
|
} # "supprev" pattern
|
|
28
30
|
attribute :suffix, :string # General suffix for other patterns
|
|
29
31
|
|
|
32
|
+
# Build a Supplement from the flat string the parser/builder produces
|
|
33
|
+
# ("1924" year, "Jan1924" month+year, "1" number, "A" suffix, "" bare).
|
|
34
|
+
# has_revision is supplied separately. Returns a present-but-empty
|
|
35
|
+
# Supplement for an empty/nil value so callers can distinguish a bare
|
|
36
|
+
# supplement ("sup") from no supplement (nil).
|
|
37
|
+
def self.from_raw(value, has_revision: false)
|
|
38
|
+
supp = new
|
|
39
|
+
if has_revision
|
|
40
|
+
supp.has_revision = true
|
|
41
|
+
elsif value.nil? || value.to_s.empty?
|
|
42
|
+
# bare marker: present but no isolated parts
|
|
43
|
+
elsif (m = value.to_s.match(/\A([A-Za-z]{3,9})(\d{4})\z/))
|
|
44
|
+
supp.month = m[1]
|
|
45
|
+
supp.year = m[2]
|
|
46
|
+
elsif value.to_s.match?(/\A(?:18|19|20)\d{2}\z/)
|
|
47
|
+
supp.year = value.to_s
|
|
48
|
+
elsif value.to_s.match?(/\A\d+\z/)
|
|
49
|
+
supp.number = value.to_s
|
|
50
|
+
else
|
|
51
|
+
supp.suffix = value.to_s
|
|
52
|
+
end
|
|
53
|
+
supp
|
|
54
|
+
end
|
|
55
|
+
|
|
30
56
|
# Render supplement in specified format
|
|
31
57
|
# @param format [:short, :mr, :long] The output format
|
|
32
58
|
# @return [String] The formatted supplement representation
|
|
33
59
|
def to_s(format = :short)
|
|
34
|
-
return "" if number.nil? && year.nil? &&
|
|
35
|
-
|
|
60
|
+
return "" if number.nil? && year.nil? && month.nil? && !has_revision &&
|
|
61
|
+
suffix.nil? && month_end.nil? && year_end.nil?
|
|
36
62
|
|
|
37
63
|
case format
|
|
38
64
|
when :short, :mr
|
|
@@ -44,12 +70,52 @@ module Pubid
|
|
|
44
70
|
end
|
|
45
71
|
end
|
|
46
72
|
|
|
73
|
+
# True when this supplement is a date range (start reuses month/year).
|
|
74
|
+
def range?
|
|
75
|
+
date_range?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The text after the "sup" marker for the simple (non-range, non-rev)
|
|
79
|
+
# forms: "1924" / "Jan1924" / "1" / "A"; "" for a bare supplement. Used
|
|
80
|
+
# by URN rendering, which handles range/revision separately.
|
|
81
|
+
def value_string
|
|
82
|
+
return "" if has_revision || date_range?
|
|
83
|
+
return suffix.to_s if suffix
|
|
84
|
+
return "#{month}#{year}" if month && year
|
|
85
|
+
return "#{number}/#{year}" if number && year
|
|
86
|
+
return year.to_s if year
|
|
87
|
+
return number.to_s if number
|
|
88
|
+
|
|
89
|
+
""
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Value equality over the isolated parts, so a structured supplement
|
|
93
|
+
# participates correctly in Identifier#== and #matches? (two supplements
|
|
94
|
+
# with the same number/year/month/range/revision are the same).
|
|
95
|
+
IDENTITY_FIELDS = %i[
|
|
96
|
+
number year month month_end year_end has_revision suffix
|
|
97
|
+
].freeze
|
|
98
|
+
|
|
99
|
+
def ==(other)
|
|
100
|
+
return false unless other.is_a?(self.class)
|
|
101
|
+
|
|
102
|
+
IDENTITY_FIELDS.all? { |f| send(f) == other.send(f) }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
alias eql? ==
|
|
106
|
+
|
|
107
|
+
def hash
|
|
108
|
+
[self.class, *IDENTITY_FIELDS.map { |f| send(f) }].hash
|
|
109
|
+
end
|
|
110
|
+
|
|
47
111
|
private
|
|
48
112
|
|
|
49
|
-
# Build short format: "
|
|
113
|
+
# Build short format: "sup2", "sup1925", "sup3/1926", "supJan1924", "suprev"
|
|
114
|
+
# NIST/NBS canonical short form is single-p "sup" with the suffix
|
|
115
|
+
# attached directly (relaton-data-nist uses "sup2", "sup1940", "supA").
|
|
50
116
|
def build_short_format
|
|
51
|
-
return "
|
|
52
|
-
return "
|
|
117
|
+
return "suprev" if has_revision
|
|
118
|
+
return "sup#{suffix}" if suffix
|
|
53
119
|
return build_date_range_format if date_range?
|
|
54
120
|
return build_month_year_format if month && year
|
|
55
121
|
return build_number_year_format if number && year
|
|
@@ -72,32 +138,33 @@ module Pubid
|
|
|
72
138
|
""
|
|
73
139
|
end
|
|
74
140
|
|
|
141
|
+
# A range is marked by the presence of an END; START reuses month/year.
|
|
75
142
|
def date_range?
|
|
76
|
-
|
|
143
|
+
month && year && month_end && year_end
|
|
77
144
|
end
|
|
78
145
|
|
|
79
146
|
def build_date_range_format
|
|
80
|
-
"
|
|
147
|
+
"sup#{month}#{year}-#{month_end}#{year_end}"
|
|
81
148
|
end
|
|
82
149
|
|
|
83
150
|
def build_month_year_format
|
|
84
|
-
"
|
|
151
|
+
"sup#{month}#{year}"
|
|
85
152
|
end
|
|
86
153
|
|
|
87
154
|
def build_number_year_format
|
|
88
|
-
"
|
|
155
|
+
"sup#{number}/#{year}"
|
|
89
156
|
end
|
|
90
157
|
|
|
91
158
|
def build_year_format
|
|
92
|
-
"
|
|
159
|
+
"sup#{year}"
|
|
93
160
|
end
|
|
94
161
|
|
|
95
162
|
def build_number_format
|
|
96
|
-
"
|
|
163
|
+
"sup#{number}"
|
|
97
164
|
end
|
|
98
165
|
|
|
99
166
|
def build_long_date_range_format
|
|
100
|
-
"Supplement #{
|
|
167
|
+
"Supplement #{month} #{year}-#{month_end} #{year_end}"
|
|
101
168
|
end
|
|
102
169
|
|
|
103
170
|
def build_long_month_year_format
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Nist
|
|
5
|
+
# NIST factory entry point. `.parse` lives on `Pubid::Nist` itself for
|
|
6
|
+
# historical reasons; this module hosts `.create` for API consistency
|
|
7
|
+
# with the other pubid flavors.
|
|
8
|
+
module Identifier
|
|
9
|
+
# Delegate to the flavor module so callers can use
|
|
10
|
+
# `Pubid::Nist::Identifier.parse` consistently with other flavors.
|
|
11
|
+
def self.parse(identifier)
|
|
12
|
+
Pubid::Nist.parse(identifier)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Factory mirroring pubid 1.x's `Pubid::Nist::Identifier.create` API.
|
|
16
|
+
#
|
|
17
|
+
# Dispatch is by `:series` (e.g. "SP", "FIPS", "IR", "HB", "TN")
|
|
18
|
+
# rather than a type code — NIST organises identifiers by document
|
|
19
|
+
# series. {Scheme.locate_identifier_klass} handles the mapping,
|
|
20
|
+
# including compound prefixes like "NBS CIRC" and special cases
|
|
21
|
+
# (CSM, CS-E, etc.).
|
|
22
|
+
def self.create(**opts)
|
|
23
|
+
klass = Scheme.locate_identifier_klass(
|
|
24
|
+
series: opts[:series]&.to_s,
|
|
25
|
+
) || Identifiers::Base
|
|
26
|
+
klass.new(**coerce_create_attrs(opts))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.coerce_create_attrs(opts)
|
|
30
|
+
attrs = {}
|
|
31
|
+
if (v = opts[:publisher])
|
|
32
|
+
attrs[:publisher] = Pubid::Nist::Components::Publisher.new(
|
|
33
|
+
publisher: v.to_s,
|
|
34
|
+
)
|
|
35
|
+
# NIST renderer prints the publisher only when it was parsed
|
|
36
|
+
# (or explicitly flagged as such).
|
|
37
|
+
attrs[:publisher_was_parsed] = true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
attrs[:series] = wrap_code(opts[:series]) if opts[:series]
|
|
41
|
+
attrs[:number] = wrap_code(opts[:number]) if opts[:number]
|
|
42
|
+
|
|
43
|
+
%i[edition volume part stage version_component update_component
|
|
44
|
+
translation_component issue_number].each do |k|
|
|
45
|
+
attrs[k] = opts[k] unless opts[k].nil?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# TODO(create-shim): expose primitive coercion for edition/volume/
|
|
49
|
+
# part etc. once a caller needs it; for now pass-through accepts
|
|
50
|
+
# already-built NIST Components.
|
|
51
|
+
attrs
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.wrap_code(v)
|
|
55
|
+
return v if v.is_a?(Pubid::Nist::Components::Code)
|
|
56
|
+
|
|
57
|
+
Pubid::Nist::Components::Code.new(number: v.to_s)
|
|
58
|
+
end
|
|
59
|
+
private_class_method :coerce_create_attrs, :wrap_code
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|