relaton 3.0.0.pre.alpha.1 → 3.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/README.adoc +57 -1
- data/bin/console +0 -1
- data/lib/relaton/3gpp/bibliography.rb +82 -7
- data/lib/relaton/3gpp/data_fetcher.rb +51 -3
- data/lib/relaton/3gpp/docidentifier.rb +114 -0
- data/lib/relaton/3gpp/item.rb +6 -0
- data/lib/relaton/3gpp/parser.rb +1 -1
- data/lib/relaton/3gpp/processor.rb +4 -1
- data/lib/relaton/3gpp.rb +5 -1
- data/lib/relaton/adobe/bibdata.rb +8 -0
- data/lib/relaton/adobe/bibitem.rb +8 -0
- data/lib/relaton/adobe/bibliography.rb +92 -0
- data/lib/relaton/adobe/docidentifier.rb +49 -0
- data/lib/relaton/adobe/doctype.rb +14 -0
- data/lib/relaton/adobe/ext.rb +32 -0
- data/lib/relaton/adobe/item.rb +15 -0
- data/lib/relaton/adobe/item_base.rb +18 -0
- data/lib/relaton/adobe/item_data.rb +6 -0
- data/lib/relaton/adobe/processor.rb +45 -0
- data/lib/relaton/adobe/util.rb +8 -0
- data/lib/relaton/adobe.rb +37 -0
- data/lib/relaton/bib/model/address.rb +2 -2
- data/lib/relaton/bib/model/docidentifier.rb +24 -9
- data/lib/relaton/bib/model/localized_string.rb +1 -1
- data/lib/relaton/bib/model/structured_identifier.rb +10 -9
- data/lib/relaton/bib/sanitizer.rb +202 -6
- data/lib/relaton/bib.rb +0 -2
- data/lib/relaton/bipm/bibliography.rb +159 -10
- data/lib/relaton/bipm/data_fetcher.rb +26 -2
- data/lib/relaton/bipm/data_outcomes_parser.rb +3 -4
- data/lib/relaton/bipm/id_parser.rb +5 -4
- data/lib/relaton/bipm/model/structured_identifier.rb +21 -0
- data/lib/relaton/bipm/processor.rb +2 -2
- data/lib/relaton/bipm/rawdata_bipm_metrologia/fetcher.rb +2 -4
- data/lib/relaton/bipm/si_brochure_parser.rb +5 -3
- data/lib/relaton/bipm.rb +7 -1
- data/lib/relaton/bsi/bibliography.rb +115 -43
- data/lib/relaton/bsi/hit.rb +14 -0
- data/lib/relaton/bsi/hit_collection.rb +15 -16
- data/lib/relaton/bsi/model/docidentifier.rb +99 -1
- data/lib/relaton/bsi/processor.rb +1 -0
- data/lib/relaton/calconnect/bibliography.rb +12 -14
- data/lib/relaton/calconnect/data_fetcher.rb +77 -9
- data/lib/relaton/calconnect/docidentifier.rb +80 -0
- data/lib/relaton/calconnect/hit_collection.rb +65 -57
- data/lib/relaton/calconnect/model/item.rb +7 -0
- data/lib/relaton/calconnect/processor.rb +7 -1
- data/lib/relaton/calconnect.rb +11 -1
- data/lib/relaton/ccsds/data/fetcher.rb +17 -12
- data/lib/relaton/ccsds/data/parser.rb +1 -1
- data/lib/relaton/ccsds/hit_collection.rb +6 -1
- data/lib/relaton/ccsds/model/docidentifier.rb +121 -0
- data/lib/relaton/ccsds/model/item.rb +2 -0
- data/lib/relaton/cen/bibliography.rb +75 -47
- data/lib/relaton/cen/hit.rb +16 -1
- data/lib/relaton/cen/hit_collection.rb +59 -11
- data/lib/relaton/cen/model/docidentifier.rb +92 -1
- data/lib/relaton/cen/processor.rb +13 -8
- data/lib/relaton/cen/scraper.rb +13 -5
- data/lib/relaton/cen.rb +1 -0
- data/lib/relaton/cie/data_fetcher.rb +215 -30
- data/lib/relaton/cie/processor.rb +3 -1
- data/lib/relaton/cie/scrapper.rb +15 -2
- data/lib/relaton/cie.rb +2 -1
- data/lib/relaton/core/data_fetcher.rb +150 -3
- data/lib/relaton/core/governor.rb +320 -0
- data/lib/relaton/core/pacer.rb +134 -0
- data/lib/relaton/core/processor.rb +19 -0
- data/lib/relaton/core/request_error.rb +14 -0
- data/lib/relaton/core.rb +3 -0
- data/lib/relaton/db/registry.rb +41 -1
- data/lib/relaton/doi/crossref.rb +19 -2
- data/lib/relaton/doi/parser.rb +109 -15
- data/lib/relaton/easc/bibdata.rb +8 -0
- data/lib/relaton/easc/bibitem.rb +8 -0
- data/lib/relaton/easc/bibliography.rb +95 -0
- data/lib/relaton/easc/docidentifier.rb +100 -0
- data/lib/relaton/easc/doctype.rb +14 -0
- data/lib/relaton/easc/ext.rb +44 -0
- data/lib/relaton/easc/item.rb +13 -0
- data/lib/relaton/easc/item_base.rb +18 -0
- data/lib/relaton/easc/item_data.rb +6 -0
- data/lib/relaton/easc/processor.rb +46 -0
- data/lib/relaton/easc/util.rb +8 -0
- data/lib/relaton/easc.rb +35 -0
- data/lib/relaton/ecma/bibliography.rb +93 -25
- data/lib/relaton/ecma/data_fetcher.rb +71 -12
- data/lib/relaton/ecma/docidentifier.rb +124 -0
- data/lib/relaton/ecma/item.rb +2 -0
- data/lib/relaton/ecma/memento_parser.rb +1 -1
- data/lib/relaton/ecma/page_fetcher.rb +15 -3
- data/lib/relaton/ecma/parser_common.rb +2 -2
- data/lib/relaton/ecma/processor.rb +4 -1
- data/lib/relaton/ecma/standard_parser.rb +2 -2
- data/lib/relaton/ecma.rb +10 -1
- data/lib/relaton/etsi/bibliography.rb +67 -2
- data/lib/relaton/etsi/data_fetcher.rb +43 -4
- data/lib/relaton/etsi/processor.rb +3 -1
- data/lib/relaton/etsi.rb +2 -1
- data/lib/relaton/gb/bibliography.rb +55 -29
- data/lib/relaton/gb/docidentifier.rb +58 -9
- data/lib/relaton/gb/processor.rb +3 -0
- data/lib/relaton/gb/scraper.rb +27 -10
- data/lib/relaton/gost/bibdata.rb +8 -0
- data/lib/relaton/gost/bibitem.rb +8 -0
- data/lib/relaton/gost/bibliography.rb +107 -0
- data/lib/relaton/gost/docidentifier.rb +80 -0
- data/lib/relaton/gost/doctype.rb +16 -0
- data/lib/relaton/gost/ext.rb +46 -0
- data/lib/relaton/gost/item.rb +15 -0
- data/lib/relaton/gost/item_base.rb +18 -0
- data/lib/relaton/gost/item_data.rb +6 -0
- data/lib/relaton/gost/processor.rb +49 -0
- data/lib/relaton/gost/util.rb +8 -0
- data/lib/relaton/gost.rb +36 -0
- data/lib/relaton/iala/bibdata.rb +8 -0
- data/lib/relaton/iala/bibitem.rb +8 -0
- data/lib/relaton/iala/bibliography.rb +146 -0
- data/lib/relaton/iala/docidentifier.rb +89 -0
- data/lib/relaton/iala/doctype.rb +18 -0
- data/lib/relaton/iala/ext.rb +32 -0
- data/lib/relaton/iala/item.rb +21 -0
- data/lib/relaton/iala/item_base.rb +18 -0
- data/lib/relaton/iala/item_data.rb +6 -0
- data/lib/relaton/iala/processor.rb +43 -0
- data/lib/relaton/iala/relation.rb +7 -0
- data/lib/relaton/iala/util.rb +8 -0
- data/lib/relaton/iala.rb +35 -0
- data/lib/relaton/iana/bibliography.rb +67 -14
- data/lib/relaton/iana/data_fetcher.rb +35 -5
- data/lib/relaton/iana/processor.rb +3 -1
- data/lib/relaton/iana.rb +12 -1
- data/lib/relaton/iec/data_fetcher.rb +7 -1
- data/lib/relaton/iec/hit_collection.rb +1 -1
- data/lib/relaton/iec/model/docidentifier.rb +9 -5
- data/lib/relaton/iec/model/ext.rb +2 -2
- data/lib/relaton/iec/processor.rb +1 -0
- data/lib/relaton/ieee/bibliography.rb +25 -3
- data/lib/relaton/ieee/data_fetcher.rb +158 -17
- data/lib/relaton/ieee/idams_parser.rb +18 -11
- data/lib/relaton/ieee/processor.rb +4 -1
- data/lib/relaton/ieee/rawbib_id_parser.rb +291 -86
- data/lib/relaton/ieee.rb +2 -1
- data/lib/relaton/ietf/data_fetcher.rb +295 -12
- data/lib/relaton/ietf/processor.rb +7 -3
- data/lib/relaton/ietf/rfc/entry.rb +39 -3
- data/lib/relaton/ietf/scraper.rb +69 -36
- data/lib/relaton/ietf.rb +4 -1
- data/lib/relaton/iho/bibliography.rb +1 -1
- data/lib/relaton/iho/docidentifier.rb +1 -1
- data/lib/relaton/index/file_io.rb +11 -11
- data/lib/relaton/index/file_storage.rb +6 -1
- data/lib/relaton/index/pool.rb +6 -1
- data/lib/relaton/index/shard_source.rb +201 -0
- data/lib/relaton/index/type.rb +63 -12
- data/lib/relaton/index.rb +2 -1
- data/lib/relaton/iso/bibliography.rb +20 -15
- data/lib/relaton/iso/data_fetcher.rb +3 -3
- data/lib/relaton/iso/data_parser.rb +17 -3
- data/lib/relaton/iso/hit_collection.rb +27 -15
- data/lib/relaton/iso/item_data.rb +22 -0
- data/lib/relaton/iso/model/docidentifier.rb +24 -12
- data/lib/relaton/iso/processor.rb +1 -0
- data/lib/relaton/iso/scraper.rb +19 -3
- data/lib/relaton/itu/bibliography.rb +9 -4
- data/lib/relaton/itu/data_crawler_r.rb +664 -0
- data/lib/relaton/itu/data_fetcher.rb +496 -50
- data/lib/relaton/itu/data_merge_r.rb +149 -0
- data/lib/relaton/itu/data_parser_r.rb +163 -89
- data/lib/relaton/itu/data_parser_t.rb +228 -0
- data/lib/relaton/itu/family_cache.rb +177 -0
- data/lib/relaton/itu/governor.rb +56 -0
- data/lib/relaton/itu/hit.rb +9 -3
- data/lib/relaton/itu/hit_collection.rb +258 -86
- data/lib/relaton/itu/model/docidentifier.rb +67 -1
- data/lib/relaton/itu/model/structured_identifier.rb +19 -0
- data/lib/relaton/itu/processor.rb +10 -4
- data/lib/relaton/itu/pubid.rb +27 -5
- data/lib/relaton/itu/recommendation_fields.rb +334 -0
- data/lib/relaton/itu/recommendation_parser.rb +18 -149
- data/lib/relaton/itu/scraper.rb +13 -3
- data/lib/relaton/itu.rb +2 -1
- data/lib/relaton/jcgm/bibdata.rb +8 -0
- data/lib/relaton/jcgm/bibitem.rb +8 -0
- data/lib/relaton/jcgm/bibliography.rb +97 -0
- data/lib/relaton/jcgm/data_fetcher.rb +81 -0
- data/lib/relaton/jcgm/docidentifier.rb +102 -0
- data/lib/relaton/jcgm/doctype.rb +12 -0
- data/lib/relaton/jcgm/ext.rb +23 -0
- data/lib/relaton/jcgm/item.rb +20 -0
- data/lib/relaton/jcgm/item_base.rb +18 -0
- data/lib/relaton/jcgm/item_data.rb +6 -0
- data/lib/relaton/jcgm/meetings_parser.rb +175 -0
- data/lib/relaton/jcgm/processor.rb +71 -0
- data/lib/relaton/jcgm/relation.rb +9 -0
- data/lib/relaton/jcgm/structured_identifier.rb +40 -0
- data/lib/relaton/jcgm/util.rb +8 -0
- data/lib/relaton/jcgm.rb +24 -0
- data/lib/relaton/jis/bibliography.rb +8 -10
- data/lib/relaton/jis/data_fetcher.rb +21 -19
- data/lib/relaton/jis/docidentifier.rb +104 -5
- data/lib/relaton/jis/hit.rb +18 -23
- data/lib/relaton/jis/hit_collection.rb +19 -18
- data/lib/relaton/jis/processor.rb +1 -1
- data/lib/relaton/jis.rb +2 -3
- data/lib/relaton/logger/channels/gh_issue.rb +78 -13
- data/lib/relaton/nist/data_fetcher.rb +63 -13
- data/lib/relaton/nist/docidentifier.rb +165 -0
- data/lib/relaton/nist/item.rb +2 -0
- data/lib/relaton/nist/item_base.rb +16 -0
- data/lib/relaton/nist/mods_parser.rb +38 -12
- data/lib/relaton/nist/processor.rb +2 -1
- data/lib/relaton/nist/relation.rb +3 -0
- data/lib/relaton/nist/scraper.rb +6 -3
- data/lib/relaton/oasis/bibliography.rb +147 -6
- data/lib/relaton/oasis/data_fetcher.rb +41 -5
- data/lib/relaton/oasis/data_parser_utils.rb +37 -3
- data/lib/relaton/oasis/docidentifier.rb +54 -0
- data/lib/relaton/oasis/item.rb +3 -0
- data/lib/relaton/oasis/processor.rb +7 -1
- data/lib/relaton/oasis.rb +14 -1
- data/lib/relaton/ogc/data_fetcher.rb +23 -2
- data/lib/relaton/ogc/docidentifier.rb +105 -0
- data/lib/relaton/ogc/hit_collection.rb +78 -3
- data/lib/relaton/ogc/processor.rb +2 -1
- data/lib/relaton/ogc.rb +5 -1
- data/lib/relaton/oiml/bibliography.rb +90 -15
- data/lib/relaton/oiml/docidentifier.rb +18 -3
- data/lib/relaton/omg/docidentifier.rb +67 -0
- data/lib/relaton/omg/item.rb +1 -0
- data/lib/relaton/omg/processor.rb +1 -0
- data/lib/relaton/omg/scraper.rb +61 -16
- data/lib/relaton/omg.rb +1 -0
- data/lib/relaton/plateau/bibliography.rb +10 -3
- data/lib/relaton/plateau/data_fetcher.rb +25 -2
- data/lib/relaton/plateau/handbook_parser.rb +8 -1
- data/lib/relaton/plateau/hit.rb +10 -2
- data/lib/relaton/plateau/hit_collection.rb +31 -11
- data/lib/relaton/plateau/processor.rb +3 -1
- data/lib/relaton/plateau/technical_report_parser.rb +8 -1
- data/lib/relaton/plateau.rb +2 -1
- data/lib/relaton/sdo/config.rb +34 -0
- data/lib/relaton/sdo/fetcher.rb +52 -0
- data/lib/relaton/sdo/logo.rb +95 -0
- data/lib/relaton/sdo/name.rb +26 -0
- data/lib/relaton/sdo/organization.rb +71 -0
- data/lib/relaton/sdo/store.rb +49 -0
- data/lib/relaton/sdo.rb +29 -0
- data/lib/relaton/version.rb +1 -1
- data/lib/relaton/w3c/bibliography.rb +132 -12
- data/lib/relaton/w3c/data_fetcher.rb +194 -16
- data/lib/relaton/w3c/data_parser.rb +3 -3
- data/lib/relaton/w3c/docidentifier.rb +48 -0
- data/lib/relaton/w3c/governor.rb +32 -0
- data/lib/relaton/w3c/item.rb +3 -0
- data/lib/relaton/w3c/pubid.rb +12 -0
- data/lib/relaton/w3c/safe_realize.rb +110 -21
- data/lib/relaton/w3c.rb +12 -1
- data/lib/relaton/xsf/bibliography.rb +61 -1
- data/lib/relaton/xsf/data_fetcher.rb +55 -5
- data/lib/relaton/xsf/docidentifier.rb +46 -0
- data/lib/relaton/xsf/hit_collection.rb +31 -3
- data/lib/relaton/xsf/item.rb +6 -0
- data/lib/relaton/xsf/processor.rb +1 -0
- data/lib/relaton/xsf.rb +5 -1
- data/lib/relaton.rb +42 -0
- metadata +135 -24
- data/lib/relaton/ieee/pub_id.rb +0 -161
- data/lib/relaton/index/id_number.rb +0 -30
|
@@ -4,38 +4,76 @@ require "mechanize"
|
|
|
4
4
|
|
|
5
5
|
module Relaton
|
|
6
6
|
module Ecma
|
|
7
|
-
#
|
|
7
|
+
# ECMA bibliography module
|
|
8
8
|
module Bibliography
|
|
9
9
|
ENDPOINT = "https://raw.githubusercontent.com/relaton/relaton-data-ecma/refs/heads/v2/"
|
|
10
10
|
|
|
11
11
|
class << self
|
|
12
|
+
# @return [Relaton::Index::Type]
|
|
13
|
+
def index
|
|
14
|
+
Relaton::Index.find_or_create(
|
|
15
|
+
:ECMA, url: "#{ENDPOINT}#{INDEXFILE}.zip", file: "#{INDEXFILE}.yaml",
|
|
16
|
+
pubid_class: ::Pubid::Ecma::Identifier
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
#
|
|
21
|
+
# Search the index for a reference.
|
|
12
22
|
#
|
|
13
|
-
#
|
|
23
|
+
# **The pubid is passed to `Index::Type#search`, not the string.**
|
|
24
|
+
# `search_candidates` narrows only when the argument is not a `String`,
|
|
25
|
+
# and a block alone never narrows — so the plain string this used to
|
|
26
|
+
# pass disabled the binary search however the index was built.
|
|
27
|
+
# `pubid_class:` on the index alone fixes nothing; both had to change
|
|
28
|
+
# together.
|
|
14
29
|
#
|
|
15
|
-
#
|
|
30
|
+
# A reference pubid cannot parse finds nothing. There is deliberately no
|
|
31
|
+
# substring-scan fallback here (the OGC idiom): an ECMA id renders as
|
|
32
|
+
# `ECMA-262 ed17`, so a substring scan would answer a truncated
|
|
33
|
+
# reference like `ECMA-26` with every ECMA-26x document — an ambiguous
|
|
34
|
+
# answer is worse than none. By the same trade-off, a reference must now
|
|
35
|
+
# parse WHOLE: the old regex was unanchored at the end, so
|
|
36
|
+
# `ECMA-6 (draft)` silently resolved to ECMA-6 and now does not. See
|
|
37
|
+
# lib/relaton/ecma/CLAUDE.md for the measured table.
|
|
16
38
|
#
|
|
17
|
-
#
|
|
39
|
+
# The rows are selected with pubid's subset match `pubid === row`, the
|
|
40
|
+
# default of `Index::Type#search`. An `edition` or a `volume` that the
|
|
41
|
+
# reference omits matches any value, so a bare `ECMA-269` reaches the
|
|
42
|
+
# edition rows. pubid declares `part` strict for ECMA, so a bare
|
|
43
|
+
# `ECMA-418` does not match `ECMA-418-1`. The class must be identical,
|
|
44
|
+
# which keeps `ECMA-100` and `ECMA TR/100` apart.
|
|
45
|
+
#
|
|
46
|
+
# @param ref [String] the ECMA reference (e.g. "ECMA-6", "ECMA-269 ed3 vol2")
|
|
47
|
+
#
|
|
48
|
+
# @return [Array<Hash>] matching index rows
|
|
18
49
|
#
|
|
19
50
|
def search(ref)
|
|
20
|
-
|
|
21
|
-
return [] unless
|
|
51
|
+
pubid = parse_ref ref
|
|
52
|
+
return [] unless pubid
|
|
22
53
|
|
|
23
|
-
index
|
|
24
|
-
index.search { |row| match_ref refparts, row }
|
|
54
|
+
index.search(pubid)
|
|
25
55
|
end
|
|
26
56
|
|
|
57
|
+
#
|
|
58
|
+
# Parse a user reference into a `Pubid::Ecma::Identifier`, or nil.
|
|
59
|
+
#
|
|
60
|
+
# `Pubid::Ecma` takes the space form (`ECMA 6`) as well as the hyphen
|
|
61
|
+
# one, and parses the ` ed<N>` / ` vol<N>` suffixes the old bespoke
|
|
62
|
+
# regex accepted, so every reference shape the flavor has to handle
|
|
63
|
+
# parses without normalization here.
|
|
64
|
+
#
|
|
65
|
+
# @param ref [String]
|
|
66
|
+
# @return [Pubid::Ecma::Identifier, nil]
|
|
67
|
+
#
|
|
68
|
+
# An unrecognized reference **raises**; like ISO, ETSI and 3GPP we let it
|
|
69
|
+
# propagate. relaton-cli rescues `Pubid::Errors::Error` and renders
|
|
70
|
+
# `"..." is not a recognized standards identifier`
|
|
71
|
+
# (`gems/relaton-cli/lib/relaton/cli/command.rb:324`), and `Db#fetch`
|
|
72
|
+
# logs it through the `StandardError` arm at `lib/relaton/db.rb:122`.
|
|
73
|
+
# Rescuing here would collapse "this identifier is malformed" into "no
|
|
74
|
+
# such document", leaving a caller unable to tell them apart.
|
|
27
75
|
def parse_ref(ref)
|
|
28
|
-
|
|
29
|
-
(?<id>(?:\d[\d-]*|\w+/\d+))
|
|
30
|
-
(?:\sed(?<ed>[\d.]+))?
|
|
31
|
-
(?:\svol(?<vol>\d+))?
|
|
32
|
-
}x.match ref
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def match_ref(refparts, row) # rubocop:disable Metrics/AbcSize
|
|
36
|
-
row[:id][:id].match?(/^ECMA[-\s]#{refparts[:id]}/) &&
|
|
37
|
-
(refparts[:ed].nil? || row[:id][:ed] == refparts[:ed]) &&
|
|
38
|
-
(refparts[:vol].nil? || row[:id][:vol] == refparts[:vol])
|
|
76
|
+
::Pubid::Ecma::Identifier.parse ref.to_s.strip
|
|
39
77
|
end
|
|
40
78
|
|
|
41
79
|
# @param code [String] the ECMA standard Code to look up (e..g "ECMA-6")
|
|
@@ -47,20 +85,50 @@ module Relaton
|
|
|
47
85
|
result = fetch_doc(code)
|
|
48
86
|
if result
|
|
49
87
|
Util.info "Found: `#{result.docidentifier.first.content}`", key: code
|
|
50
|
-
# item
|
|
51
88
|
else
|
|
52
89
|
Util.info "Not found.", key: code
|
|
53
90
|
end
|
|
54
91
|
result
|
|
55
92
|
end
|
|
56
93
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
#
|
|
97
|
+
# The index row for a reference: latest edition, then lowest volume.
|
|
98
|
+
#
|
|
99
|
+
# That is the selection order the bespoke `compare_edition_volume` +
|
|
100
|
+
# `min` implemented, kept deliberately — but the edition is now compared
|
|
101
|
+
# segment-wise as INTEGERS. Comparing the rendered strings made "9" beat
|
|
102
|
+
# "17" and "5" beat "5.1", so 5 of the 421 document families returned an
|
|
103
|
+
# older document than the reference asked for: ECMA-262 answered ed9
|
|
104
|
+
# (2018-06) where ed17 (2026-06) exists, ECMA-74 ed9 (2005-12) where
|
|
105
|
+
# ed22 (2025-12) exists, and likewise 402, 328 and 109.
|
|
106
|
+
#
|
|
107
|
+
# `r[:file]` breaks the tie, because the index sort is not stable.
|
|
108
|
+
#
|
|
109
|
+
# @param ref [String]
|
|
110
|
+
# @return [Hash, nil]
|
|
111
|
+
#
|
|
112
|
+
def best_match(ref)
|
|
113
|
+
search(ref).max_by { |r| [edition_key(r[:id].edition), -r[:id].volume.to_i, r[:file]] }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
#
|
|
117
|
+
# Order key for an ECMA edition: its dot-separated segments as integers.
|
|
118
|
+
#
|
|
119
|
+
# `[]` for an absent edition, which therefore sorts below every present
|
|
120
|
+
# one — right for the 64 edition-less rows (mementos, a few reports),
|
|
121
|
+
# none of which shares a document with an edition-bearing row.
|
|
122
|
+
#
|
|
123
|
+
# @param edition [String, nil]
|
|
124
|
+
# @return [Array<Integer>]
|
|
125
|
+
#
|
|
126
|
+
def edition_key(edition)
|
|
127
|
+
edition.to_s.split(".").map(&:to_i)
|
|
60
128
|
end
|
|
61
129
|
|
|
62
|
-
def fetch_doc(code)
|
|
63
|
-
row =
|
|
130
|
+
def fetch_doc(code)
|
|
131
|
+
row = best_match code
|
|
64
132
|
return unless row
|
|
65
133
|
|
|
66
134
|
url = "#{ENDPOINT}#{row[:file]}"
|
|
@@ -18,7 +18,12 @@ module Relaton
|
|
|
18
18
|
SOURCES = %w[standards technical-reports mementos].freeze
|
|
19
19
|
|
|
20
20
|
def index
|
|
21
|
-
|
|
21
|
+
# `pubid_class:` on the producer too: FileIO#save only calls `to_hash`
|
|
22
|
+
# for instances of it, so without it the crawl writes v1-shaped rows
|
|
23
|
+
# under a v2 name, silently.
|
|
24
|
+
@index ||= Relaton::Index.find_or_create(
|
|
25
|
+
:ecma, file: "#{INDEXFILE}.yaml", pubid_class: ::Pubid::Ecma::Identifier
|
|
26
|
+
)
|
|
22
27
|
end
|
|
23
28
|
|
|
24
29
|
def log_error(msg)
|
|
@@ -31,30 +36,84 @@ module Relaton
|
|
|
31
36
|
|
|
32
37
|
# @param bib [Relaton::Ecma::ItemData]
|
|
33
38
|
def write_file(bib)
|
|
34
|
-
|
|
39
|
+
# Two distinct ids can sanitize to one filename, in which case the
|
|
40
|
+
# second document used to be dropped outright. Give it a path of its
|
|
41
|
+
# own; a genuine duplicate (same id) still resolves to one path and is
|
|
42
|
+
# still skipped.
|
|
43
|
+
file = unique_output_file filename_id(bib)
|
|
44
|
+
# A reserved path only ever belongs to one id, so a hit here is the same
|
|
45
|
+
# document again. Checked FIRST: a disambiguated path stays != filename
|
|
46
|
+
# forever, so gating this on that comparison would make a repeat of a
|
|
47
|
+
# disambiguated id skip the skip and overwrite its own file.
|
|
35
48
|
if @files.include? file
|
|
36
49
|
Util.warn "Duplicate file #{file}"
|
|
37
|
-
|
|
38
|
-
@files << file
|
|
39
|
-
File.write file, serialize(bib), encoding: "UTF-8"
|
|
40
|
-
index.add_or_update index_id(bib), file
|
|
50
|
+
return
|
|
41
51
|
end
|
|
52
|
+
|
|
53
|
+
Util.warn "Duplicate file #{filename bib}; writing #{file} instead" if file != filename(bib)
|
|
54
|
+
@files << file
|
|
55
|
+
File.write file, serialize(bib), encoding: "UTF-8"
|
|
56
|
+
add_to_index bib, file
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
#
|
|
60
|
+
# Index the document, or record why it could not be indexed.
|
|
61
|
+
#
|
|
62
|
+
# An id pubid cannot rebuild is recorded in `@errors` — the inherited
|
|
63
|
+
# `report_errors` logs a String value as the message, and its GhIssue
|
|
64
|
+
# channel opens a GitHub issue at the end of the crawl — and the row is
|
|
65
|
+
# skipped rather than indexed unparsed: `Relaton::Index` rejects the WHOLE
|
|
66
|
+
# index if a single row fails to deserialize, and its sort calls
|
|
67
|
+
# `.root.number` on every id. The data file is already written by the
|
|
68
|
+
# caller, so the document is unindexed, never lost. (The 3GPP/W3C shape.)
|
|
69
|
+
#
|
|
70
|
+
# @param bib [Relaton::Ecma::ItemData]
|
|
71
|
+
# @param file [String] path the document was written to
|
|
72
|
+
#
|
|
73
|
+
def add_to_index(bib, file)
|
|
74
|
+
id = index_id bib
|
|
75
|
+
return index.add_or_update(id, file) if id
|
|
76
|
+
|
|
77
|
+
docid = bib.docidentifier[0]&.content || file
|
|
78
|
+
@errors[docid.to_s] = "Unparseable primary id `#{docid}` was not indexed (#{file})"
|
|
42
79
|
end
|
|
43
80
|
|
|
44
81
|
def filename(bib)
|
|
82
|
+
output_file filename_id(bib)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The docid the filename is derived from.
|
|
86
|
+
def filename_id(bib)
|
|
45
87
|
id = bib.docidentifier[0].content
|
|
46
88
|
id += " #{bib.edition.content}" if bib.edition
|
|
47
89
|
locality = locality_with_volume bib
|
|
48
90
|
id += " #{locality.reference_from}" if locality
|
|
49
|
-
|
|
91
|
+
id
|
|
50
92
|
end
|
|
51
93
|
|
|
94
|
+
#
|
|
95
|
+
# The index key: a `Pubid::Ecma::Identifier` carrying the number, the
|
|
96
|
+
# edition and the volume.
|
|
97
|
+
#
|
|
98
|
+
# Built from the MODEL, never from a rendered string — the same three
|
|
99
|
+
# fields `#filename_id` reads — so the crawl cannot lose a component to a
|
|
100
|
+
# parse or to a render default. The base identifier is the docidentifier's
|
|
101
|
+
# own pubid, **duplicated** first: `edition` and `volume` are index
|
|
102
|
+
# metadata, and every `ECMA-269` volume file carries the bare
|
|
103
|
+
# `docidentifier: ECMA-269`, so setting them on the shared object would
|
|
104
|
+
# promote the document's own printed id to the index form.
|
|
105
|
+
#
|
|
106
|
+
# @param bib [Relaton::Ecma::ItemData]
|
|
107
|
+
# @return [Pubid::Ecma::Identifier, nil] nil if pubid rejects the docid
|
|
108
|
+
#
|
|
52
109
|
def index_id(bib)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
110
|
+
pubid = bib.docidentifier[0]&.pubid&.dup
|
|
111
|
+
return unless pubid
|
|
112
|
+
|
|
113
|
+
pubid.edition = bib.edition.content if bib.edition
|
|
114
|
+
locality = locality_with_volume bib
|
|
115
|
+
pubid.volume = locality.reference_from if locality
|
|
116
|
+
pubid
|
|
58
117
|
end
|
|
59
118
|
|
|
60
119
|
def locality_with_volume(bib)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
module Relaton
|
|
2
|
+
module Ecma
|
|
3
|
+
# Structured ECMA document identifier. The docid string is the BARE printed
|
|
4
|
+
# form ("ECMA-269", "ECMA TR/101", "ECMA MEM/2021"), and it is parsed into a
|
|
5
|
+
# `Pubid::Ecma::Identifier` kept in `@pubid` while the lutaml `content`
|
|
6
|
+
# attribute stays a plain string for serialization.
|
|
7
|
+
#
|
|
8
|
+
# Parsing is **soft**: `content=` lazily requires pubid and rescues
|
|
9
|
+
# `LoadError`/`StandardError`, so a missing pubid gem or non-ECMA content
|
|
10
|
+
# leaves `@pubid` nil rather than raising.
|
|
11
|
+
#
|
|
12
|
+
# ## Why this class exists, and the one rule that is ECMA-specific
|
|
13
|
+
#
|
|
14
|
+
# `Pubid::Ecma::Identifier#to_s` renders the edition and the volume **by
|
|
15
|
+
# default** — `"ECMA-269 ed3 vol2"`. That default is deliberate on the pubid
|
|
16
|
+
# side: `Relaton::Index::Type#add_or_update` keys on a bare `id.to_s`, and
|
|
17
|
+
# 740 of the 804 published rows carry an edition, so without it 383 rows
|
|
18
|
+
# collapse onto another row's key and vanish from the index.
|
|
19
|
+
#
|
|
20
|
+
# A **document's own** docidentifier is the opposite. Every `ECMA-269`
|
|
21
|
+
# volume file in `relaton-data-ecma` carries `docidentifier: ECMA-269` and
|
|
22
|
+
# the same title — the edition and the volume are index metadata, not part
|
|
23
|
+
# of the printed id. So `refresh_content!` must opt OUT of both, or a
|
|
24
|
+
# mutation would silently promote the stored content to the index form.
|
|
25
|
+
#
|
|
26
|
+
# This is the mirror image of `Relaton::ThreeGpp::Docidentifier`, whose
|
|
27
|
+
# `refresh_content!` must pass `with_publisher: true` because ITS pubid
|
|
28
|
+
# defaults to the index rendering and its stored content is the fuller one.
|
|
29
|
+
# The two look contradictory only until you notice each one re-renders what
|
|
30
|
+
# its own stored `content` already holds.
|
|
31
|
+
class Docidentifier < Bib::Docidentifier
|
|
32
|
+
attr_reader :pubid
|
|
33
|
+
|
|
34
|
+
# Capture the inherited (LocalizedMarkedUpString) content setter before
|
|
35
|
+
# overriding #content=, so #refresh_content! can write the re-rendered
|
|
36
|
+
# string back WITHOUT re-parsing (a re-parse would rebuild @pubid from the
|
|
37
|
+
# string and discard in-place mutations).
|
|
38
|
+
alias_method :store_content, :content=
|
|
39
|
+
|
|
40
|
+
def initialize(attrs = {}, options = {})
|
|
41
|
+
pubid = attrs.is_a?(Hash) ? attrs.delete(:pubid) : nil
|
|
42
|
+
attrs[:content] ||= render(pubid) if pubid
|
|
43
|
+
super
|
|
44
|
+
@pubid = pubid if pubid
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def content=(value)
|
|
48
|
+
super
|
|
49
|
+
return unless value
|
|
50
|
+
|
|
51
|
+
@pubid = begin
|
|
52
|
+
require "pubid"
|
|
53
|
+
::Pubid::Ecma::Identifier.parse(value.to_s)
|
|
54
|
+
rescue LoadError, StandardError
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# `Pubid::Ecma::Identifier` models number/part/subpart/edition/volume, so
|
|
60
|
+
# the mapping is:
|
|
61
|
+
#
|
|
62
|
+
# - **`remove_date!` → clears `edition`.** ECMA has no date component;
|
|
63
|
+
# `edition` is its version discriminator, so clearing it yields the
|
|
64
|
+
# version-agnostic ("most recent") reference. It is invisible in the
|
|
65
|
+
# rendered bare form, which never carried the edition to begin with —
|
|
66
|
+
# but the identifier really does change, and a consumer reading
|
|
67
|
+
# `#pubid` sees it.
|
|
68
|
+
# - **`remove_part!` → clears `part`/`subpart`.** Real here:
|
|
69
|
+
# "ECMA-418-1" -> "ECMA-418".
|
|
70
|
+
# - **`to_all_parts!` → both, then wraps `@pubid` in pubid's `AllParts`.**
|
|
71
|
+
# `content` stays the plain stripped id — ECMA's own renderer has no
|
|
72
|
+
# "(all parts)" marker, and `content` is cached from the pre-wrap
|
|
73
|
+
# pubid on purpose. `#pubid` itself, read directly, now answers
|
|
74
|
+
# `all_parts? == true` and renders WITH pubid's generic marker
|
|
75
|
+
# (`#to_s`), since it's the wrapper.
|
|
76
|
+
#
|
|
77
|
+
# All three no-op safely when `@pubid` is nil, so `Bib::ItemData`'s
|
|
78
|
+
# `#to_all_parts` / `#to_most_recent_reference` never raise on ECMA items.
|
|
79
|
+
|
|
80
|
+
def remove_part!
|
|
81
|
+
clear_attrs! :part
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def remove_date!
|
|
85
|
+
clear_attrs! :edition
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def to_all_parts!
|
|
89
|
+
return if !@pubid || @pubid.all_parts?
|
|
90
|
+
|
|
91
|
+
# `#exclude` (no args) rebuilds a full independent copy — `remove_part!`
|
|
92
|
+
# / `remove_date!` mutate `@pubid` in place, so a bare reference here
|
|
93
|
+
# would lose the original part/edition to that mutation too.
|
|
94
|
+
original = @pubid.exclude
|
|
95
|
+
remove_part!
|
|
96
|
+
remove_date!
|
|
97
|
+
@pubid = original.to_all_parts
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def clear_attrs!(*attrs)
|
|
103
|
+
return unless @pubid
|
|
104
|
+
|
|
105
|
+
cleared = attrs.select { |attr| @pubid.respond_to?("#{attr}=") }
|
|
106
|
+
return if cleared.empty?
|
|
107
|
+
|
|
108
|
+
cleared.each { |attr| @pubid.public_send("#{attr}=", nil) }
|
|
109
|
+
@pubid.subpart = nil if attrs.include?(:part) && @pubid.respond_to?(:subpart=)
|
|
110
|
+
refresh_content!
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def refresh_content!
|
|
114
|
+
store_content(render(@pubid)) if @pubid
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# The bare document form. See the class comment for why both flags are
|
|
118
|
+
# opted out of, and why 3GPP's twin does the opposite.
|
|
119
|
+
def render(pubid)
|
|
120
|
+
pubid.to_s(with_edition: false, with_volume: false)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
data/lib/relaton/ecma/item.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require_relative "docidentifier"
|
|
1
2
|
require_relative "ext"
|
|
2
3
|
|
|
3
4
|
module Relaton
|
|
@@ -5,6 +6,7 @@ module Relaton
|
|
|
5
6
|
class Item < Bib::Item
|
|
6
7
|
model ItemData
|
|
7
8
|
|
|
9
|
+
attribute :docidentifier, Docidentifier, collection: true, initialize_empty: true
|
|
8
10
|
attribute :ext, Ext
|
|
9
11
|
end
|
|
10
12
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module Relaton
|
|
2
2
|
module Ecma
|
|
3
3
|
class PageFetcher
|
|
4
|
+
RETRIES = 3
|
|
5
|
+
|
|
4
6
|
def initialize
|
|
5
7
|
@agent = Mechanize.new
|
|
6
8
|
@agent.user_agent_alias = Mechanize::AGENT_ALIASES.keys[rand(21)]
|
|
@@ -9,18 +11,28 @@ module Relaton
|
|
|
9
11
|
#
|
|
10
12
|
# Get page with retries
|
|
11
13
|
#
|
|
14
|
+
# A failed fetch must raise, not fall out of the loop. `RETRIES.times`
|
|
15
|
+
# returns its receiver, so the exhausted loop used to hand the caller the
|
|
16
|
+
# Integer 3 as if it were a page, and `StandardParser#fetch_title`
|
|
17
|
+
# reported the transport error as `undefined method 'xpath' for an
|
|
18
|
+
# instance of Integer`. `DataFetcher#html_index` already rescues per hit,
|
|
19
|
+
# so one unreachable page is skipped with a readable message.
|
|
20
|
+
#
|
|
12
21
|
# @param [String] url url to fetch
|
|
13
22
|
#
|
|
14
23
|
# @return [Mechanize::Page] document
|
|
24
|
+
# @raise [Relaton::RequestError] after RETRIES attempts
|
|
15
25
|
#
|
|
16
26
|
def get(url)
|
|
17
|
-
|
|
27
|
+
error = nil
|
|
28
|
+
RETRIES.times do |n|
|
|
18
29
|
sleep n
|
|
19
|
-
|
|
20
|
-
return doc
|
|
30
|
+
return @agent.get url
|
|
21
31
|
rescue StandardError => e
|
|
32
|
+
error = e
|
|
22
33
|
Util.error e.message
|
|
23
34
|
end
|
|
35
|
+
raise Relaton::RequestError, "Could not access #{url}: #{error.message}"
|
|
24
36
|
end
|
|
25
37
|
end
|
|
26
38
|
end
|
|
@@ -14,11 +14,11 @@ module Relaton
|
|
|
14
14
|
[Bib::Contributor.new(organization: org, role: [role])]
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
# @return [Array<Relaton::
|
|
17
|
+
# @return [Array<Relaton::Ecma::Docidentifier>]
|
|
18
18
|
def fetch_docidentifier(id = nil)
|
|
19
19
|
return [] if id.nil? || id.empty?
|
|
20
20
|
|
|
21
|
-
[
|
|
21
|
+
[Docidentifier.new(type: "ECMA", content: id, primary: true)]
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def fetch_ext
|
|
@@ -9,6 +9,7 @@ module Relaton
|
|
|
9
9
|
@defaultprefix = /^ECMA(-|\s)/
|
|
10
10
|
@idtype = "ECMA"
|
|
11
11
|
@datasets = %w[ecma-standards]
|
|
12
|
+
@pubid_flavor = :Ecma
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
# @param code [String]
|
|
@@ -59,7 +60,9 @@ module Relaton
|
|
|
59
60
|
#
|
|
60
61
|
def remove_index_file
|
|
61
62
|
require_relative "../ecma"
|
|
62
|
-
Relaton::Index.find_or_create(
|
|
63
|
+
Relaton::Index.find_or_create(
|
|
64
|
+
:ECMA, url: true, file: "#{INDEXFILE}.yaml"
|
|
65
|
+
).remove_file
|
|
63
66
|
end
|
|
64
67
|
end
|
|
65
68
|
end
|
|
@@ -26,7 +26,7 @@ module Relaton
|
|
|
26
26
|
@translation_source ||= parse_translation_source
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
# @return [Array<Relaton::
|
|
29
|
+
# @return [Array<Relaton::Ecma::Docidentifier>]
|
|
30
30
|
def fetch_docidentifier
|
|
31
31
|
result = super(@hit.text)
|
|
32
32
|
@errors[:standard_docidentifier] &&= result.empty?
|
|
@@ -83,7 +83,7 @@ module Relaton
|
|
|
83
83
|
ref, ed, date, vol = edition_parser.edition_id_parts rel.at("span").text
|
|
84
84
|
next if ed.nil? || ed.empty?
|
|
85
85
|
|
|
86
|
-
docid =
|
|
86
|
+
docid = Docidentifier.new(type: "ECMA", content: ref, primary: true)
|
|
87
87
|
source = rel.xpath("span/a").map { |l| Bib::Uri.new type: "pdf", content: l[:href] }
|
|
88
88
|
edition = Bib::Edition.new content: ed
|
|
89
89
|
extent = edition_parser.create_extent(vol)
|
data/lib/relaton/ecma.rb
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
# Not lazy: DataFetcher names ::Pubid::Ecma::Identifier as the index
|
|
2
|
+
# `pubid_class:`, and Docidentifier parses every docid through it.
|
|
3
|
+
# (The IANA/IHO/IALA/OGC form; spec/relaton/lazy_loading_spec.rb guards that
|
|
4
|
+
# this file is not itself loaded when a Db is built.)
|
|
5
|
+
require "pubid"
|
|
1
6
|
require "relaton/index"
|
|
2
7
|
require "relaton/bib"
|
|
3
8
|
require_relative "version"
|
|
@@ -10,7 +15,11 @@ require_relative "ecma/bibliography"
|
|
|
10
15
|
|
|
11
16
|
module Relaton
|
|
12
17
|
module Ecma
|
|
13
|
-
|
|
18
|
+
# The one index this flavor builds and reads: pubid-keyed rows
|
|
19
|
+
# (`_type: pubid:ecma:*`), via `pubid_class: ::Pubid::Ecma::Identifier`.
|
|
20
|
+
# `relaton-data-ecma`'s crawler derives the legacy `index-v1` from these
|
|
21
|
+
# rows for released consumers, so it is not produced or read here.
|
|
22
|
+
INDEXFILE = "index-v2".freeze
|
|
14
23
|
|
|
15
24
|
# Returns hash of XML reammar
|
|
16
25
|
# @return [String]
|
|
@@ -9,8 +9,15 @@ module Relaton
|
|
|
9
9
|
# @param text [String]
|
|
10
10
|
# @return [Relaton::Etsi::ItemData, nil]
|
|
11
11
|
def search(text) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
# An unrecognized reference raises Pubid::Errors::ParseError; like
|
|
13
|
+
# ISO we let it propagate — the CLI turns it into a friendly message
|
|
14
|
+
# and API callers rescue it themselves. Valid partial refs parse with
|
|
15
|
+
# the omitted refinements (version/date/part) left blank.
|
|
16
|
+
pubid = ::Pubid::Etsi.parse text
|
|
17
|
+
|
|
18
|
+
index = Relaton::Index.find_or_create :etsi, url: "#{SOURCE}#{INDEXFILE}.zip", file: "#{INDEXFILE}.yaml",
|
|
19
|
+
pubid_class: ::Pubid::Etsi::Identifier
|
|
20
|
+
row = best_match(index, pubid)
|
|
14
21
|
return unless row
|
|
15
22
|
|
|
16
23
|
url = "#{SOURCE}#{row[:file]}"
|
|
@@ -24,6 +31,64 @@ module Relaton
|
|
|
24
31
|
raise Relaton::RequestError, e.message
|
|
25
32
|
end
|
|
26
33
|
|
|
34
|
+
# Match the reference against the index and return the most recent edition.
|
|
35
|
+
#
|
|
36
|
+
# The rows are selected with pubid's subset match `pubid === row`. A
|
|
37
|
+
# version or a date that the reference omits matches any value, so a
|
|
38
|
+
# bare `ETSI GS ZSM 012` matches every edition, while a fully-qualified
|
|
39
|
+
# ref matches only that edition. The class must be identical, so a base
|
|
40
|
+
# reference such as `ETSI ETR 310` does not reach its amendment or
|
|
41
|
+
# corrigendum (`ETR 310/C1`).
|
|
42
|
+
# The pubid — not a String — is passed to `index.search` so the index
|
|
43
|
+
# narrows candidates by number via binary search; each row's `:id` is
|
|
44
|
+
# already a Pubid::Etsi identifier (deserialized via `pubid_class`).
|
|
45
|
+
# `max_by` on `edition_key` picks the latest edition among the matches.
|
|
46
|
+
#
|
|
47
|
+
# pubid declares `parts` strict for ETSI, so a part-less reference asks
|
|
48
|
+
# for every part by matching each row **without its parts** (see
|
|
49
|
+
# #comparable). `#to_all_parts` cannot serve here: a
|
|
50
|
+
# `Pubid::AllPartsIdentifier` compares the document alone, so it also
|
|
51
|
+
# drops the version and the date, and
|
|
52
|
+
# `ETSI GR ZSM 011 V1.1.1 (2023-02)` would answer with V2.1.1.
|
|
53
|
+
#
|
|
54
|
+
# @param index [Relaton::Index::Type]
|
|
55
|
+
# @param pubid [::Pubid::Etsi::Identifier]
|
|
56
|
+
# @return [Hash, nil] the winning index row (`{ id:, file: }`)
|
|
57
|
+
def best_match(index, pubid)
|
|
58
|
+
all_parts = pubid.code&.parts.to_a.empty?
|
|
59
|
+
index.search(pubid) { |row| pubid === comparable(row[:id], all_parts) }
|
|
60
|
+
.max_by { |row| edition_key(row[:id]) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The row as the reference sees it: without its parts when the reference
|
|
64
|
+
# names none, unchanged otherwise. `#exclude` returns a copy, so the
|
|
65
|
+
# cached index id is untouched.
|
|
66
|
+
#
|
|
67
|
+
# @param id [::Pubid::Etsi::Identifier]
|
|
68
|
+
# @param all_parts [Boolean] the reference names no part
|
|
69
|
+
# @return [::Pubid::Etsi::Identifier]
|
|
70
|
+
def comparable(id, all_parts)
|
|
71
|
+
all_parts ? id.exclude(:part, :subpart, :parts) : id
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Sort key for one edition: the version numbers, then the publication date.
|
|
75
|
+
#
|
|
76
|
+
# ETSI versions are not zero-padded, so a comparison of the rendered id
|
|
77
|
+
# orders `V9.0.0` above `V19.0.0` and `ed.9` above `ed.11`, and a bare
|
|
78
|
+
# reference then resolves to an old edition. `Pubid::Etsi::Identifier` is
|
|
79
|
+
# not `Comparable` and its `<=>` returns nil, so the key comes from the
|
|
80
|
+
# parsed components: `version.version` holds the bare numbers (`"19.0.0"`,
|
|
81
|
+
# or `"9"` for the `ed.9` form) and `date` renders as `yyyy-mm`. Both
|
|
82
|
+
# delegate to `base` on a corrigendum/amendment id, so every row shape
|
|
83
|
+
# keys the same way. A missing version or date gives `[]` / `""`, which
|
|
84
|
+
# sort below any real value — hence the `.to_s` outside each `&.` chain.
|
|
85
|
+
#
|
|
86
|
+
# @param id [::Pubid::Etsi::Identifier]
|
|
87
|
+
# @return [Array(Array<Integer>, String)]
|
|
88
|
+
def edition_key(id)
|
|
89
|
+
[(id.version&.version).to_s.split(".").map(&:to_i), id.date.to_s]
|
|
90
|
+
end
|
|
91
|
+
|
|
27
92
|
# @param ref [String] the ETSI standard Code to look up
|
|
28
93
|
# @param year [String, nil] year
|
|
29
94
|
# @param opts [Hash] options
|