pubid 2.0.0.pre.alpha.5 → 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.
- checksums.yaml +4 -4
- data/README.adoc +40 -0
- data/lib/pubid/amca.rb +6 -0
- data/lib/pubid/ansi.rb +7 -0
- data/lib/pubid/api.rb +5 -0
- data/lib/pubid/ashrae.rb +6 -0
- data/lib/pubid/asme.rb +5 -0
- data/lib/pubid/astm.rb +5 -0
- data/lib/pubid/bsi/builder.rb +41 -48
- data/lib/pubid/bsi/identifiers/amendment.rb +28 -0
- data/lib/pubid/bsi/identifiers/consolidated_identifier.rb +11 -1
- data/lib/pubid/bsi/identifiers/corrigendum.rb +24 -0
- data/lib/pubid/bsi/identifiers/expert_commentary.rb +5 -0
- data/lib/pubid/bsi/identifiers/flex.rb +4 -0
- data/lib/pubid/bsi/parser.rb +19 -5
- data/lib/pubid/bsi/renderer.rb +19 -17
- data/lib/pubid/bsi.rb +15 -0
- data/lib/pubid/ccsds.rb +5 -0
- data/lib/pubid/cen_cenelec.rb +7 -0
- data/lib/pubid/cie.rb +5 -0
- data/lib/pubid/csa.rb +5 -0
- data/lib/pubid/etsi.rb +7 -0
- data/lib/pubid/iala/builder.rb +107 -0
- data/lib/pubid/iala/identifier.rb +67 -0
- data/lib/pubid/iala/identifiers/advice.rb +15 -0
- data/lib/pubid/iala/identifiers/annex.rb +55 -0
- data/lib/pubid/iala/identifiers/base.rb +14 -0
- data/lib/pubid/iala/identifiers/general_assembly.rb +16 -0
- data/lib/pubid/iala/identifiers/guideline.rb +15 -0
- data/lib/pubid/iala/identifiers/letter.rb +16 -0
- data/lib/pubid/iala/identifiers/manual.rb +16 -0
- data/lib/pubid/iala/identifiers/model_course.rb +15 -0
- data/lib/pubid/iala/identifiers/recommendation.rb +15 -0
- data/lib/pubid/iala/identifiers/report.rb +16 -0
- data/lib/pubid/iala/identifiers/resolution.rb +16 -0
- data/lib/pubid/iala/identifiers/standard.rb +15 -0
- data/lib/pubid/iala/identifiers.rb +20 -0
- data/lib/pubid/iala/parser.rb +130 -0
- data/lib/pubid/iala/renderer.rb +36 -0
- data/lib/pubid/iala/urn_generator.rb +49 -0
- data/lib/pubid/iala/urn_parser.rb +50 -0
- data/lib/pubid/iala.rb +71 -0
- data/lib/pubid/identifier.rb +54 -1
- data/lib/pubid/idf.rb +5 -0
- data/lib/pubid/iec.rb +8 -0
- data/lib/pubid/ieee.rb +8 -0
- data/lib/pubid/iho.rb +5 -0
- data/lib/pubid/iso.rb +6 -0
- data/lib/pubid/itu.rb +5 -0
- data/lib/pubid/jcgm.rb +5 -0
- data/lib/pubid/jis.rb +6 -0
- data/lib/pubid/nist.rb +13 -0
- data/lib/pubid/oiml/builder.rb +33 -0
- data/lib/pubid/oiml/identifier.rb +1 -0
- data/lib/pubid/oiml/identifiers/bulletin.rb +101 -0
- data/lib/pubid/oiml/identifiers.rb +1 -0
- data/lib/pubid/oiml/parser.rb +43 -1
- data/lib/pubid/oiml/renderer.rb +51 -0
- data/lib/pubid/oiml/urn_generator.rb +22 -0
- data/lib/pubid/oiml.rb +5 -0
- data/lib/pubid/plateau.rb +5 -0
- data/lib/pubid/prefixes_support.rb +51 -0
- data/lib/pubid/sae.rb +6 -0
- data/lib/pubid/version.rb +1 -1
- data/lib/pubid.rb +69 -0
- metadata +24 -2
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Iala
|
|
5
|
+
# Parses IALA MRN URNs back into identifiers.
|
|
6
|
+
#
|
|
7
|
+
# UrnGenerator emits:
|
|
8
|
+
# urn:mrn:iala:pub:<type-lower><number>[:ed<edition>][:<lang-lower>]
|
|
9
|
+
#
|
|
10
|
+
# Examples:
|
|
11
|
+
# urn:mrn:iala:pub:s1070:ed2.0 → IALA S1070 Ed 2.0
|
|
12
|
+
# urn:mrn:iala:pub:r1016:ed2.0:f → IALA R1016 Ed 2.0 (F)
|
|
13
|
+
# urn:mrn:iala:pub:c0103-1 → IALA C0103-1
|
|
14
|
+
class UrnParser < Pubid::UrnParser::Base
|
|
15
|
+
PREFIX = "urn:mrn:iala:pub:".freeze
|
|
16
|
+
|
|
17
|
+
def parse_urn(urn)
|
|
18
|
+
body = strip_namespace(urn)
|
|
19
|
+
parts = split_parts(body)
|
|
20
|
+
code = parts.fetch(0)
|
|
21
|
+
|
|
22
|
+
edition = nil
|
|
23
|
+
language = nil
|
|
24
|
+
parts.drop(1).each do |seg|
|
|
25
|
+
if seg.start_with?("ed")
|
|
26
|
+
edition = seg.sub(/\Aed/, "")
|
|
27
|
+
elsif seg.length == 1 && seg.match?(/\A[a-z]\z/i)
|
|
28
|
+
language = seg.upcase
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
text = "IALA #{code.upcase}"
|
|
33
|
+
text += " Ed #{edition}" if edition
|
|
34
|
+
text += " (#{language})" if language
|
|
35
|
+
flavor_parse(text)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def strip_namespace(urn)
|
|
41
|
+
unless urn.downcase.start_with?(PREFIX)
|
|
42
|
+
raise Pubid::UrnParser::Errors::ParseError,
|
|
43
|
+
"Invalid IALA URN: #{urn.inspect}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
urn[PREFIX.length..]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/pubid/iala.rb
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Iala
|
|
5
|
+
extend Pubid::PrefixesSupport
|
|
6
|
+
|
|
7
|
+
# Sole IALA publisher token (see the parser's `publisher` rule).
|
|
8
|
+
PREFIXES = ["IALA"].freeze
|
|
9
|
+
|
|
10
|
+
autoload :Builder, "#{__dir__}/iala/builder"
|
|
11
|
+
autoload :Identifier, "#{__dir__}/iala/identifier"
|
|
12
|
+
autoload :Identifiers, "#{__dir__}/iala/identifiers"
|
|
13
|
+
autoload :Parser, "#{__dir__}/iala/parser"
|
|
14
|
+
autoload :Renderer, "#{__dir__}/iala/renderer"
|
|
15
|
+
autoload :UrnGenerator, "#{__dir__}/iala/urn_generator"
|
|
16
|
+
autoload :UrnParser, "#{__dir__}/iala/urn_parser"
|
|
17
|
+
|
|
18
|
+
# Parse an IALA identifier string into an identifier object.
|
|
19
|
+
# @param identifier [String] The IALA identifier string to parse
|
|
20
|
+
# @return [Pubid::Iala::Identifier]
|
|
21
|
+
def self.parse(identifier)
|
|
22
|
+
Identifier.parse(identifier)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Per-flavor format registry: inherits global formats, overrides :human
|
|
26
|
+
Identifiers::Base.format_registry = FormatRegistry.new(parent: ::Pubid::Identifier.format_registry)
|
|
27
|
+
Identifiers::Base.format_registry.register(:human, renderer: Iala::Renderer)
|
|
28
|
+
|
|
29
|
+
# Auto-discover all identifier types from the Identifiers namespace.
|
|
30
|
+
# @return [Array<Class>] identifier classes (Pubid::Identifier subclasses)
|
|
31
|
+
def self.identifier_types
|
|
32
|
+
@identifier_types ||= Identifiers.constants
|
|
33
|
+
.filter_map { |c| begin; Identifiers.const_get(c); rescue NameError; nil; end }
|
|
34
|
+
.select { |c| c.is_a?(Class) && c < Pubid::Identifier }
|
|
35
|
+
.reject { |c| c == Identifiers::Base }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Build typed stage index from identifier types
|
|
39
|
+
# @return [Array<Pubid::Components::TypedStage>]
|
|
40
|
+
def self.all_typed_stages
|
|
41
|
+
@all_typed_stages ||= identifier_types.flat_map do |klass|
|
|
42
|
+
klass.const_defined?(:TYPED_STAGES) ? klass.const_get(:TYPED_STAGES) : []
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Lookup: type code -> identifier class
|
|
47
|
+
# @param code [String, Symbol]
|
|
48
|
+
# @return [Class, nil]
|
|
49
|
+
def self.locate_type(code)
|
|
50
|
+
identifier_types.find { |t| t.type[:key].to_s == code.to_s }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Lookup: abbreviation -> typed stage
|
|
54
|
+
# @param abbr [String, Symbol]
|
|
55
|
+
# @return [Pubid::Components::TypedStage, nil]
|
|
56
|
+
def self.locate_stage(abbr)
|
|
57
|
+
abbr_str = abbr.to_s.upcase
|
|
58
|
+
all_typed_stages.find { |s| s.abbr.any? { |a| a.to_s.upcase == abbr_str } }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Look up an identifier class by its IALA type letter (S, R, G, M, C, X, P).
|
|
62
|
+
# @param letter [String]
|
|
63
|
+
# @return [Class<Identifiers::Base>]
|
|
64
|
+
def self.identifier_klass_for_type_letter(letter)
|
|
65
|
+
@by_letter ||= identifier_types.to_h { |klass| [klass.type[:short], klass] }
|
|
66
|
+
@by_letter.fetch(letter.to_s.upcase)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
Pubid::Registry.register(:iala, Pubid::Iala)
|
data/lib/pubid/identifier.rb
CHANGED
|
@@ -120,6 +120,13 @@ module Pubid
|
|
|
120
120
|
nil
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
+
# The underlying standard, with amendment / corrigendum / Expert-commentary
|
|
124
|
+
# / Flex-version wrappers peeled recursively. Wrapper subclasses override
|
|
125
|
+
# this; a plain identifier is its own base document.
|
|
126
|
+
def base_document
|
|
127
|
+
self
|
|
128
|
+
end
|
|
129
|
+
|
|
123
130
|
# @return [String, nil] publication year from the date component
|
|
124
131
|
def year
|
|
125
132
|
date&.year&.to_s
|
|
@@ -311,17 +318,48 @@ module Pubid
|
|
|
311
318
|
Pubid::UrnGenerator::Base
|
|
312
319
|
end
|
|
313
320
|
|
|
321
|
+
# Excluded attributes are nilled; every other value is passed through
|
|
322
|
+
# #exclude_from_nested so the exclusion also propagates into nested
|
|
323
|
+
# identifiers — wrapper types (adopted standards, consolidated amendments,
|
|
324
|
+
# expert-commentary wrappers) delegate their date to an inner identifier
|
|
325
|
+
# rather than storing it in their own attribute.
|
|
314
326
|
def exclude(*args)
|
|
327
|
+
# :amendment / :supplement are structural, not attributes — they reduce
|
|
328
|
+
# a supplemented identifier to the standard it wraps (#drop_supplements).
|
|
329
|
+
supplement_keys = args & %i[amendment supplement]
|
|
330
|
+
unless supplement_keys.empty?
|
|
331
|
+
return drop_supplements.exclude(*(args - supplement_keys))
|
|
332
|
+
end
|
|
333
|
+
|
|
315
334
|
excluded_args = args.dup
|
|
316
335
|
# Map :year to :date since identifiers store years inside date
|
|
317
336
|
excluded_args << :date if excluded_args.delete(:year)
|
|
318
337
|
|
|
319
338
|
attrs = self.class.attributes.each_with_object({}) do |(name, _), h|
|
|
320
|
-
|
|
339
|
+
value = excluded_args.include?(name) ? nil : public_send(name)
|
|
340
|
+
h[name] = exclude_from_nested(value, args)
|
|
321
341
|
end
|
|
322
342
|
self.class.new(attrs)
|
|
323
343
|
end
|
|
324
344
|
|
|
345
|
+
# The standard this identifier supplements, dropping its own supplement
|
|
346
|
+
# layer (one level). Overridden by ConsolidatedIdentifier / Amendment /
|
|
347
|
+
# Corrigendum; a non-supplement identifier supplements nothing, so it is
|
|
348
|
+
# returned unchanged.
|
|
349
|
+
def drop_supplements
|
|
350
|
+
self
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Fuzz-level equality: two identifiers match when they are equal after
|
|
354
|
+
# excluding the given aspects. `ignore` accepts the same symbols as #exclude
|
|
355
|
+
# (e.g. :date, :edition, :amendment). This is the primitive relaton uses to
|
|
356
|
+
# match a reference against catalogue hits at varying strictness.
|
|
357
|
+
def matches?(other, ignore: [])
|
|
358
|
+
return false unless other.is_a?(::Pubid::Identifier)
|
|
359
|
+
|
|
360
|
+
exclude(*ignore) == other.exclude(*ignore)
|
|
361
|
+
end
|
|
362
|
+
|
|
325
363
|
def new_edition_of?(other)
|
|
326
364
|
unless publisher == other.publisher
|
|
327
365
|
raise ArgumentError,
|
|
@@ -362,6 +400,21 @@ module Pubid
|
|
|
362
400
|
|
|
363
401
|
private
|
|
364
402
|
|
|
403
|
+
# Propagate an #exclude into a nested attribute value: recurse when it is
|
|
404
|
+
# (or contains) another identifier, otherwise return it unchanged.
|
|
405
|
+
# Components and scalars are copied as-is. Passes the original args so
|
|
406
|
+
# nested identifiers re-apply the same :year->:date mapping themselves.
|
|
407
|
+
def exclude_from_nested(value, args)
|
|
408
|
+
case value
|
|
409
|
+
when ::Pubid::Identifier
|
|
410
|
+
value.exclude(*args)
|
|
411
|
+
when Array
|
|
412
|
+
value.map { |item| exclude_from_nested(item, args) }
|
|
413
|
+
else
|
|
414
|
+
value
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
365
418
|
def build_rendering_context(_renderer, format:, with_edition: false,
|
|
366
419
|
lang: :en, lang_single: false,
|
|
367
420
|
stage_format_long: nil, with_date: nil,
|
data/lib/pubid/idf.rb
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module Pubid
|
|
4
4
|
module Idf
|
|
5
|
+
extend Pubid::PrefixesSupport
|
|
6
|
+
|
|
7
|
+
# Sole IDF publisher token (see the parser's `prefix_sole_publisher` rule).
|
|
8
|
+
PREFIXES = ["IDF"].freeze
|
|
9
|
+
|
|
5
10
|
autoload :Builder, "#{__dir__}/idf/builder"
|
|
6
11
|
autoload :Identifier, "#{__dir__}/idf/identifier"
|
|
7
12
|
autoload :Identifiers, "#{__dir__}/idf/identifiers"
|
data/lib/pubid/iec.rb
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
module Pubid
|
|
4
4
|
module Iec
|
|
5
5
|
autoload :Components, "#{__dir__}/iec/components"
|
|
6
|
+
|
|
7
|
+
extend Pubid::PrefixesSupport
|
|
8
|
+
|
|
9
|
+
# Sourced from the parser's publisher list (Components::Publisher::PUBLISHERS)
|
|
10
|
+
# minus the joint "ISO/IEC", which is contributed symmetrically via
|
|
11
|
+
# Pubid::JOINT_PREFIXES (along with IEC/ISO and ISO/IEC/IEEE).
|
|
12
|
+
PREFIXES = (Components::Publisher::PUBLISHERS.keys - ["ISO/IEC"]).freeze
|
|
13
|
+
|
|
6
14
|
autoload :Identifier, "#{__dir__}/iec/identifier"
|
|
7
15
|
autoload :Identifiers, "#{__dir__}/iec/identifiers"
|
|
8
16
|
autoload :SingleIdentifier, "#{__dir__}/iec/single_identifier"
|
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"
|
data/lib/pubid/oiml/builder.rb
CHANGED
|
@@ -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"
|
data/lib/pubid/oiml/parser.rb
CHANGED
|
@@ -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
|
|
data/lib/pubid/oiml/renderer.rb
CHANGED
|
@@ -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)
|