relaton-etsi 2.1.1 → 2.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 83ff53d11a3552ae1912b417996db91727e92e8e3603db51e8cb7e952e1201b2
4
- data.tar.gz: 325c2b01e11be54efaacb079750b09f28dc9111beb496fbacae85516943177b2
3
+ metadata.gz: abf8bf4e0aa5207f1a957ebca5b69ebd3a44857a4f8fbc0f17d9f3d9617bb174
4
+ data.tar.gz: f1ae62b230c9a6a32e4037b26690c2668890c9e8911d9665a7e46176628c3a98
5
5
  SHA512:
6
- metadata.gz: 2cfa40bd4c74fb3f5ed33e89f8b334a02e27ceeddbdc6885f603b59cef183b7dd2d90ae768b11e2340bb4a02ec0ab25728f55df1471df260e7c19ff1c6b7f6dd
7
- data.tar.gz: 3245710e0f9cdb786477c02b164b8e953ef2b9fedda33fa70604d464e26ad91c221c200290cf6ad2b07f504e47d66f3043c25e131b156c874f897c1123749399
6
+ metadata.gz: f7ee42ae9e44aa09dcc8c3e822eed2e1d5e78454a2faecc552060f61471fa3bc927edd92cdd5736403ef28fed9e3701413c902bb6e6a48fda8d2e10fc0a557ca
7
+ data.tar.gz: a0cc870769279fd8a0c0f2448cd107b22245fa43dffa133df7cc7ddd4fe711295fb51b981aa1b3389d0ca75f133f4c6b90f776fa7be51f03102d9a175a785d5d
data/CLAUDE.md CHANGED
@@ -60,7 +60,7 @@ Uses Lutaml for serialization:
60
60
 
61
61
  ### Data Flow
62
62
 
63
- 1. `Bibliography.get(ref)` searches the relaton-data-etsi index
63
+ 1. `Bibliography.get(ref)` searches the relaton-data-etsi index. `Bibliography#best_match` keeps the rows that match the whole document number, then picks the highest version, and among equal versions the latest date. The index matches a String query with `include?`, so this filter is what stops `ETSI TS 103 1` from matching `ETSI TS 103 104`.
64
64
  2. Fetches YAML from GitHub, converts to `Item` using `from_yaml`
65
65
  3. `DataFetcher.fetch` pulls CSV from etsi.org, parses with `DataParser`, saves to output folder
66
66
 
data/README.adoc CHANGED
@@ -37,6 +37,24 @@ item = Relaton::Etsi::Bibliography.get("ETSI GS ZSM 012 V1.1.1")
37
37
  ...
38
38
  ----
39
39
 
40
+ === Reference matching
41
+
42
+ A reference matches a whole ETSI document number. Neither `ETSI TS 103 1` nor
43
+ `ETSI TS 103` matches `ETSI TS 103 104`. A reference without a part number
44
+ matches every part of the document. The `ETSI` prefix is optional.
45
+
46
+ A reference without a version returns the newest edition of the document. A
47
+ reference with a version and a date returns that edition.
48
+
49
+ [source,ruby]
50
+ ----
51
+ Relaton::Etsi::Bibliography.get("ETSI EN 319 401")
52
+ [relaton-etsi] INFO: (ETSI EN 319 401) Found: `ETSI EN 319 401 V3.2.1 (2026-01)`
53
+
54
+ Relaton::Etsi::Bibliography.get("ETSI EN 319 401 V2.3.1 (2021-05)")
55
+ [relaton-etsi] INFO: (ETSI EN 319 401 V2.3.1 (2021-05)) Found: `ETSI EN 319 401 V2.3.1 (2021-05)`
56
+ ----
57
+
40
58
  === XML serialization
41
59
 
42
60
  [source,ruby]
@@ -6,11 +6,16 @@ module Relaton
6
6
  module Bibliography
7
7
  SOURCE = "https://raw.githubusercontent.com/relaton/relaton-data-etsi/refs/heads/v2/"
8
8
 
9
+ # What follows the document number in an index id: the part numbers, then
10
+ # the version. A reference matches a whole number only, so a part number
11
+ # is optional but a group of digits of the number is not.
12
+ SUFFIX = /\A(?:-\d+)*\s(?:V\d|ed\.\d)/
13
+
9
14
  # @param text [String]
10
15
  # @return [Relaton::Etsi::ItemData, nil]
11
16
  def search(text) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
12
17
  index = Relaton::Index.find_or_create :etsi, url: "#{SOURCE}index-v1.zip", file: INDEX_FILE
13
- row = index.search(text).min_by { |r| r[:id] }
18
+ row = best_match(index, text)
14
19
  return unless row
15
20
 
16
21
  url = "#{SOURCE}#{row[:file]}"
@@ -40,6 +45,64 @@ module Relaton
40
45
  result
41
46
  end
42
47
 
48
+ # Match the reference against the index and return the newest edition.
49
+ #
50
+ # An index id renders as `ETSI <type> <number> <V1.2.3|ed.4> (<yyyy-mm>)`.
51
+ # The index matches a String query with `include?`, so a bare reference
52
+ # matches every edition of the document, but also longer numbers. The
53
+ # `matches?` filter drops the longer numbers, then `edition_key` picks the
54
+ # highest version, and among equal versions the latest date.
55
+ #
56
+ # @param index [Relaton::Index::Type]
57
+ # @param text [String] the reference
58
+ # @return [Hash, nil] the winning index row (`{ id:, file: }`)
59
+ def best_match(index, text)
60
+ ref = normalize(text)
61
+ index.search(ref)
62
+ .select { |row| matches? ref, row[:id] }
63
+ .max_by { |row| edition_key row[:id] }
64
+ end
65
+
66
+ private
67
+
68
+ # Every index id has the `ETSI` prefix. A reference can omit it.
69
+ #
70
+ # @param text [String] the reference
71
+ # @return [String]
72
+ def normalize(text)
73
+ ref = text.strip.squeeze(" ").sub(/\AETSI\s+/i, "")
74
+ "ETSI #{ref}"
75
+ end
76
+
77
+ # `ETSI EN 319 142` matches every part of the document. Neither
78
+ # `ETSI EN 319 14` nor `ETSI EN 319` matches anything: the first stops
79
+ # inside a group of digits, the second stops before the last group.
80
+ #
81
+ # @param ref [String] the normalized reference
82
+ # @param id [String] the index id
83
+ # @return [Boolean]
84
+ def matches?(ref, id)
85
+ return true if id == ref
86
+
87
+ rest = id[ref.length..]
88
+ !rest.nil? && rest.match?(SUFFIX)
89
+ end
90
+
91
+ # Sort key of one edition. The numbers keep `V19.0.0` above `V9.0.0` and
92
+ # `ed.11` above `ed.9`, which a comparison of the strings does not. The id
93
+ # is the last element to make the order of equal editions stable.
94
+ #
95
+ # A `V` version and an `ed.` edition give keys of a different length. No
96
+ # ETSI document has the two forms together, and `matches?` keeps the rows
97
+ # of one document number only, so the two forms do not meet here.
98
+ #
99
+ # @param id [String] the index id
100
+ # @return [Array]
101
+ def edition_key(id)
102
+ version = id[/\sV(\d+(?:\.\d+)*)/, 1] || id[/\sed\.(\d+)/, 1] || ""
103
+ [version.split(".").map(&:to_i), id[/\((\d{4}-\d{2})\)/, 1].to_s, id]
104
+ end
105
+
43
106
  extend Bibliography
44
107
  end
45
108
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Relaton
4
4
  module Etsi
5
- VERSION = "2.1.1"
5
+ VERSION = "2.1.2"
6
6
  end
7
7
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/relaton/etsi/version"
4
+
5
+ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
6
+ spec.name = "relaton-etsi"
7
+ spec.version = Relaton::Etsi::VERSION
8
+ spec.authors = ["Ribose Inc."]
9
+ spec.email = ["open.source@ribose.com"]
10
+
11
+ spec.summary = "Relaton::Etsi: retrieve ETSI Standards for bibliographic " \
12
+ "using the BibliographicItem model"
13
+ spec.description = "Relaton::Etsi: retrieve ETSI Standards for bibliographic " \
14
+ "using the BibliographicItem model"
15
+ spec.homepage = "https://github.com/relaton/relaton-etsi"
16
+ spec.license = "BSD-2-Clause"
17
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0")
18
+
19
+ # spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
20
+
21
+ spec.metadata["homepage_uri"] = spec.homepage
22
+ # spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
23
+ # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
24
+
25
+ # Specify which files should be added to the gem when it is released.
26
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
27
+ spec.files = Dir.chdir(__dir__) do
28
+ `git ls-files -z`.split("\x0").reject do |f|
29
+ (File.expand_path(f) == __FILE__) ||
30
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
31
+ end
32
+ end
33
+ spec.bindir = "exe"
34
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
35
+ spec.require_paths = ["lib"]
36
+
37
+ spec.add_dependency "mechanize", "~> 2.8"
38
+ spec.add_dependency "relaton-bib", "~> 2.1.0"
39
+ spec.add_dependency "relaton-core", "~> 0.0.13"
40
+ spec.add_dependency "relaton-index", "~> 0.2.7"
41
+
42
+ # For more information and examples about making a new gem, check out our
43
+ # guide at: https://bundler.io/guides/creating_gem.html
44
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: relaton-etsi
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.1
4
+ version: 2.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-11 00:00:00.000000000 Z
11
+ date: 2026-08-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mechanize
@@ -80,11 +80,6 @@ files:
80
80
  - LICENSE.txt
81
81
  - README.adoc
82
82
  - Rakefile
83
- - grammars/basicdoc.rng
84
- - grammars/biblio-standoc.rng
85
- - grammars/biblio.rng
86
- - grammars/relaton-etsi-compile.rng
87
- - grammars/relaton-etsi.rng
88
83
  - lib/relaton/etsi.rb
89
84
  - lib/relaton/etsi/bibdata.rb
90
85
  - lib/relaton/etsi/bibitem.rb
@@ -100,6 +95,7 @@ files:
100
95
  - lib/relaton/etsi/status.rb
101
96
  - lib/relaton/etsi/util.rb
102
97
  - lib/relaton/etsi/version.rb
98
+ - relaton-etsi.gemspec
103
99
  - sig/relaton_etsi.rbs
104
100
  homepage: https://github.com/relaton/relaton-etsi
105
101
  licenses: