sec_id 7.1.0 → 8.0.0
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/CHANGELOG.md +86 -30
- data/README.md +118 -63
- data/lib/sec_id/active_model.rb +5 -4
- data/lib/sec_id/bic/country_codes.rb +1 -2
- data/lib/sec_id/cei.rb +1 -1
- data/lib/sec_id/cfi/attribute_set.rb +4 -1
- data/lib/sec_id/cfi/tables.rb +1 -2
- data/lib/sec_id/concerns/checkable.rb +46 -62
- data/lib/sec_id/concerns/generatable.rb +4 -0
- data/lib/sec_id/concerns/normalizable.rb +4 -0
- data/lib/sec_id/concerns/suggestable.rb +6 -1
- data/lib/sec_id/concerns/validatable.rb +4 -0
- data/lib/sec_id/cusip.rb +1 -1
- data/lib/sec_id/detector.rb +13 -10
- data/lib/sec_id/figi.rb +1 -1
- data/lib/sec_id/iban/country_rules.rb +1 -2
- data/lib/sec_id/iban.rb +1 -1
- data/lib/sec_id/isin.rb +1 -1
- data/lib/sec_id/lei.rb +1 -1
- data/lib/sec_id/scanner.rb +12 -7
- data/lib/sec_id/suggestion.rb +4 -0
- data/lib/sec_id/version.rb +1 -1
- data/lib/sec_id.rb +3 -1
- data/sec_id.gemspec +2 -2
- data/sig/sec_id/cfi/attribute_set.rbs +2 -2
- data/sig/sec_id/concerns/checkable.rbs +7 -6
- data/sig/sec_id/detector.rbs +1 -1
- data/sig/sec_id/scanner.rbs +1 -1
- metadata +5 -5
|
@@ -8,6 +8,10 @@ module SecID
|
|
|
8
8
|
# Characters stripped during normalization (whitespace and hyphens); classes may override.
|
|
9
9
|
SEPARATORS = /[\s-]/
|
|
10
10
|
|
|
11
|
+
# Extends the including identifier class with the concern's class methods.
|
|
12
|
+
#
|
|
13
|
+
# @param base [Class] the identifier class including this concern
|
|
14
|
+
# @return [void]
|
|
11
15
|
# @api private
|
|
12
16
|
def self.included(base)
|
|
13
17
|
base.extend(ClassMethods)
|
|
@@ -7,7 +7,8 @@ module SecID
|
|
|
7
7
|
#
|
|
8
8
|
# Included by the 9 checksum types. The candidate space is the human-error net —
|
|
9
9
|
# {HOMOGLYPHS} plus adjacent transpositions — never a full coincidental net. `valid?`
|
|
10
|
-
# is the oracle, so no
|
|
10
|
+
# is the oracle, so no checksum-invalid candidate ever escapes (a valid candidate is not
|
|
11
|
+
# necessarily the intended correction); the {HOMOGLYPHS} table bounds recall.
|
|
11
12
|
#
|
|
12
13
|
# @see SecID::Suggestion
|
|
13
14
|
#
|
|
@@ -30,6 +31,10 @@ module SecID
|
|
|
30
31
|
# then the checksum-recompute fallback last (R6/R7).
|
|
31
32
|
RANK = { substitution: 0, transposition: 1, checksum: 2 }.freeze
|
|
32
33
|
|
|
34
|
+
# Extends the including identifier class with the concern's class methods.
|
|
35
|
+
#
|
|
36
|
+
# @param base [Class] the identifier class including this concern
|
|
37
|
+
# @return [void]
|
|
33
38
|
# @api private
|
|
34
39
|
def self.included(base)
|
|
35
40
|
base.extend(ClassMethods)
|
|
@@ -18,6 +18,10 @@ module SecID
|
|
|
18
18
|
invalid_country: InvalidStructureError
|
|
19
19
|
}.freeze
|
|
20
20
|
|
|
21
|
+
# Extends the including identifier class with the concern's class methods.
|
|
22
|
+
#
|
|
23
|
+
# @param base [Class] the identifier class including this concern
|
|
24
|
+
# @return [void]
|
|
21
25
|
# @api private
|
|
22
26
|
def self.included(base)
|
|
23
27
|
base.extend(ClassMethods)
|
data/lib/sec_id/cusip.rb
CHANGED
|
@@ -61,7 +61,7 @@ module SecID
|
|
|
61
61
|
# @raise [InvalidFormatError] if the CUSIP format is invalid
|
|
62
62
|
def calculate_checksum
|
|
63
63
|
validate_format_for_calculation!
|
|
64
|
-
mod10(
|
|
64
|
+
mod10(luhn_sum_single(identifier))
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
# Generates a random CUSIP body: 8 alphanumeric characters.
|
data/lib/sec_id/detector.rb
CHANGED
|
@@ -19,7 +19,7 @@ module SecID
|
|
|
19
19
|
class Detector
|
|
20
20
|
# @param identifier_list [Array<Class>] registered identifier classes
|
|
21
21
|
def initialize(identifier_list)
|
|
22
|
-
@classes = identifier_list.
|
|
22
|
+
@classes = identifier_list.sort_by(&:detection_priority).freeze
|
|
23
23
|
precompute
|
|
24
24
|
end
|
|
25
25
|
|
|
@@ -33,7 +33,7 @@ module SecID
|
|
|
33
33
|
|
|
34
34
|
upcased = input.upcase
|
|
35
35
|
candidates = filter_candidates(upcased)
|
|
36
|
-
|
|
36
|
+
valid_types(input, candidates)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
# Returns whether any registered type matches, short-circuiting on the first
|
|
@@ -57,8 +57,11 @@ module SecID
|
|
|
57
57
|
return if input.empty?
|
|
58
58
|
|
|
59
59
|
candidates = filter_candidates(input.upcase)
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
candidates.each do |klass|
|
|
61
|
+
instance = klass.new(input)
|
|
62
|
+
return instance if instance.valid?
|
|
63
|
+
end
|
|
64
|
+
nil
|
|
62
65
|
end
|
|
63
66
|
|
|
64
67
|
private
|
|
@@ -74,14 +77,13 @@ module SecID
|
|
|
74
77
|
stage3_charset(upcased, candidates)
|
|
75
78
|
end
|
|
76
79
|
|
|
77
|
-
# Validates candidates
|
|
80
|
+
# Validates candidates in precomputed specificity order.
|
|
78
81
|
#
|
|
79
82
|
# @param input [String]
|
|
80
83
|
# @param candidates [Array<Class>]
|
|
81
84
|
# @return [Array<Symbol>]
|
|
82
|
-
def
|
|
85
|
+
def valid_types(input, candidates)
|
|
83
86
|
matches = candidates.select { |klass| klass.valid?(input) }
|
|
84
|
-
matches.sort_by!(&:detection_priority)
|
|
85
87
|
matches.map!(&:type_key)
|
|
86
88
|
end
|
|
87
89
|
|
|
@@ -105,11 +107,12 @@ module SecID
|
|
|
105
107
|
#
|
|
106
108
|
# @return [void]
|
|
107
109
|
def build_length_table
|
|
108
|
-
@candidates_by_length =
|
|
110
|
+
@candidates_by_length = {}
|
|
109
111
|
@classes.each do |klass|
|
|
110
|
-
klass.length_values.each { |len| @candidates_by_length[len] << klass }
|
|
112
|
+
klass.length_values.each { |len| (@candidates_by_length[len] ||= []) << klass }
|
|
111
113
|
end
|
|
112
114
|
@candidates_by_length.each_value(&:freeze)
|
|
115
|
+
@candidates_by_length.default = [].freeze
|
|
113
116
|
end
|
|
114
117
|
|
|
115
118
|
# Stage 1: route strings with special characters to the only types that accept them.
|
|
@@ -130,7 +133,7 @@ module SecID
|
|
|
130
133
|
# @param length [Integer]
|
|
131
134
|
# @return [Array<Class>]
|
|
132
135
|
def stage2_length(length)
|
|
133
|
-
@candidates_by_length[length]
|
|
136
|
+
@candidates_by_length[length]
|
|
134
137
|
end
|
|
135
138
|
|
|
136
139
|
# Stage 3: filter candidates by character set.
|
data/lib/sec_id/figi.rb
CHANGED
|
@@ -69,7 +69,7 @@ module SecID
|
|
|
69
69
|
# @raise [InvalidFormatError] if the FIGI format is invalid
|
|
70
70
|
def calculate_checksum
|
|
71
71
|
validate_format_for_calculation!
|
|
72
|
-
mod10(
|
|
72
|
+
mod10(luhn_sum_single(identifier, double_first: false))
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
# Generates a random FIGI body: non-reserved 2-char prefix + 'G' + 8-char random part.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SecID
|
|
4
|
-
# rubocop:disable Metrics/ClassLength
|
|
4
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
5
5
|
class IBAN < Base
|
|
6
6
|
# Country-specific BBAN rules for EU/EEA countries
|
|
7
7
|
# Each entry defines:
|
|
@@ -265,5 +265,4 @@ module SecID
|
|
|
265
265
|
'XK' => 16 # Kosovo
|
|
266
266
|
}.freeze
|
|
267
267
|
end
|
|
268
|
-
# rubocop:enable Metrics/ClassLength
|
|
269
268
|
end
|
data/lib/sec_id/iban.rb
CHANGED
|
@@ -226,7 +226,7 @@ module SecID
|
|
|
226
226
|
|
|
227
227
|
# @return [String] the numeric string representation
|
|
228
228
|
def numeric_string_for_check
|
|
229
|
-
"#{bban}#{country_code}00".
|
|
229
|
+
"#{bban}#{country_code}00".gsub(/[^0-9]/) { |char| DECIMAL_VALUES.fetch(char) }
|
|
230
230
|
end
|
|
231
231
|
end
|
|
232
232
|
end
|
data/lib/sec_id/isin.rb
CHANGED
|
@@ -86,7 +86,7 @@ module SecID
|
|
|
86
86
|
# @raise [InvalidFormatError] if the ISIN format is invalid
|
|
87
87
|
def calculate_checksum
|
|
88
88
|
validate_format_for_calculation!
|
|
89
|
-
mod10(luhn_sum_standard(
|
|
89
|
+
mod10(luhn_sum_standard(identifier))
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
# Generates a random ISIN body: 2-letter country code + 9-character NSIN.
|
data/lib/sec_id/lei.rb
CHANGED
|
@@ -90,7 +90,7 @@ module SecID
|
|
|
90
90
|
|
|
91
91
|
# @return [String] the numeric string representation
|
|
92
92
|
def numeric_identifier
|
|
93
|
-
identifier.each_char.
|
|
93
|
+
identifier.each_char.with_object(+'') { |char, numeric| numeric << DECIMAL_VALUES.fetch(char) }
|
|
94
94
|
end
|
|
95
95
|
end
|
|
96
96
|
end
|
data/lib/sec_id/scanner.rb
CHANGED
|
@@ -73,7 +73,7 @@ module SecID
|
|
|
73
73
|
@classes.each do |klass|
|
|
74
74
|
klass.length_values.each { |len| @candidates_by_length[len] << klass }
|
|
75
75
|
end
|
|
76
|
-
@candidates_by_length.each_value(&:freeze
|
|
76
|
+
@candidates_by_length.each_value { |classes| classes.sort_by!(&:detection_priority).freeze }
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
# @param input [String]
|
|
@@ -118,18 +118,23 @@ module SecID
|
|
|
118
118
|
return unless best
|
|
119
119
|
|
|
120
120
|
end_pos = start_pos + raw.length
|
|
121
|
-
Match.new(type: best.type_key, raw: raw, range: start_pos...end_pos, identifier: best
|
|
121
|
+
Match.new(type: best.class.type_key, raw: raw, range: start_pos...end_pos, identifier: best)
|
|
122
122
|
end
|
|
123
123
|
|
|
124
|
-
# @return [
|
|
124
|
+
# @return [Base, nil]
|
|
125
125
|
def best_match(cleaned, classes)
|
|
126
126
|
return if classes.empty?
|
|
127
127
|
|
|
128
|
-
candidates =
|
|
129
|
-
return
|
|
128
|
+
candidates = @candidates_by_length[cleaned.length]
|
|
129
|
+
return unless candidates
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
candidates.each do |klass|
|
|
132
|
+
next unless classes.include?(klass) && cleaned.match?(klass::VALID_CHARS_REGEX)
|
|
133
|
+
|
|
134
|
+
instance = klass.new(cleaned)
|
|
135
|
+
return instance if instance.valid?
|
|
136
|
+
end
|
|
137
|
+
nil
|
|
133
138
|
end
|
|
134
139
|
end
|
|
135
140
|
end
|
data/lib/sec_id/suggestion.rb
CHANGED
|
@@ -46,11 +46,15 @@ module SecID
|
|
|
46
46
|
# The non-nil confidence tiers `confidence` can take; `:checksum` candidates carry `nil`.
|
|
47
47
|
CONFIDENCE_LEVELS = %i[high medium].freeze
|
|
48
48
|
|
|
49
|
+
# Returns a JSON-compatible hash representation of the suggestion.
|
|
50
|
+
#
|
|
49
51
|
# @return [Hash] the symbol-keyed field hash (JSON-compatible)
|
|
50
52
|
def as_json(*)
|
|
51
53
|
to_h
|
|
52
54
|
end
|
|
53
55
|
|
|
56
|
+
# Returns the corrected identifier as a string.
|
|
57
|
+
#
|
|
54
58
|
# @return [String] the corrected identifier string
|
|
55
59
|
def to_s
|
|
56
60
|
identifier.to_s
|
data/lib/sec_id/version.rb
CHANGED
data/lib/sec_id.rb
CHANGED
|
@@ -158,7 +158,9 @@ module SecID
|
|
|
158
158
|
input = str.to_s.strip.upcase
|
|
159
159
|
suggestable_types(input, types)
|
|
160
160
|
.flat_map { |klass| klass.suggest(input) }
|
|
161
|
-
.sort_by
|
|
161
|
+
.sort_by do |suggestion|
|
|
162
|
+
[Suggestable::RANK.fetch(suggestion.edit), self[suggestion.type].detection_priority.last]
|
|
163
|
+
end
|
|
162
164
|
end
|
|
163
165
|
|
|
164
166
|
private
|
data/sec_id.gemspec
CHANGED
|
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
|
|
|
10
10
|
spec.authors = ['Leonid Svyatov']
|
|
11
11
|
spec.email = ['leonid@svyatov.ru']
|
|
12
12
|
|
|
13
|
-
spec.summary = 'A Ruby toolkit for securities identifiers
|
|
13
|
+
spec.summary = 'A Ruby toolkit for securities identifiers: validate, parse, normalize, detect, convert, ' \
|
|
14
14
|
'generate, classify, and repair.'
|
|
15
15
|
spec.description = 'Validate, normalize, parse, convert, generate, classify, and repair securities identifiers. ' \
|
|
16
16
|
'Auto-detect identifier type from any string. Calculate and restore checksums. Suggest ' \
|
|
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
|
|
|
22
22
|
spec.homepage = 'https://github.com/svyatov/sec_id'
|
|
23
23
|
spec.license = 'MIT'
|
|
24
24
|
|
|
25
|
-
spec.required_ruby_version = '>= 3.
|
|
25
|
+
spec.required_ruby_version = '>= 3.3.0'
|
|
26
26
|
|
|
27
27
|
spec.require_paths = ['lib']
|
|
28
28
|
spec.files = Dir['lib/**/*.rb'] + Dir['sig/**/*'] +
|
|
@@ -12,8 +12,8 @@ module SecID
|
|
|
12
12
|
| () -> Enumerator[Field, self]
|
|
13
13
|
def []: (Symbol meaning) -> Field?
|
|
14
14
|
def empty?: () -> bool
|
|
15
|
-
def to_h: () -> Hash[
|
|
16
|
-
def as_json: (*untyped) -> Hash[Symbol, untyped]
|
|
15
|
+
def to_h: () -> Hash[Symbol, Hash[Symbol, untyped]]
|
|
16
|
+
def as_json: (*untyped) -> Hash[Symbol, Hash[Symbol, untyped]]
|
|
17
17
|
|
|
18
18
|
# Per-meaning readers — the union of every attribute meaning. Each instance
|
|
19
19
|
# only answers the meanings actually present; an absent one raises
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
module SecID
|
|
2
2
|
# Provides checksum validation and calculation for identifiers with a checksum.
|
|
3
3
|
module Checkable : SecID::Base
|
|
4
|
-
CHAR_TO_DIGITS: Hash[String, Integer | Array[Integer]]
|
|
5
4
|
CHAR_TO_DIGIT: Hash[String, Integer]
|
|
5
|
+
BYTE_TO_DIGIT: Hash[Integer, Integer]
|
|
6
|
+
DECIMAL_VALUES: Hash[String, String]
|
|
7
|
+
LUHN_DOUBLE: Array[Integer]
|
|
6
8
|
|
|
7
9
|
def self.included: (Module base) -> void
|
|
8
10
|
|
|
@@ -33,11 +35,10 @@ module SecID
|
|
|
33
35
|
|
|
34
36
|
private
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def
|
|
39
|
-
def
|
|
40
|
-
def reversed_digits_multi: (untyped id) -> Array[Integer]
|
|
38
|
+
# Readers are nilable before validation. The callers validate first; local
|
|
39
|
+
# annotations keep the byte traversal typed without adding runtime guards.
|
|
40
|
+
def luhn_sum_single: (untyped id, ?double_first: bool) -> Integer
|
|
41
|
+
def luhn_sum_standard: (untyped id) -> Integer
|
|
41
42
|
def error_codes: () -> Array[Symbol]
|
|
42
43
|
def checksum_width: () -> Integer
|
|
43
44
|
def validation_message: (Symbol code) -> String?
|
data/sig/sec_id/detector.rbs
CHANGED
|
@@ -11,7 +11,7 @@ module SecID
|
|
|
11
11
|
private
|
|
12
12
|
|
|
13
13
|
def filter_candidates: (String upcased) -> Array[singleton(Base)]
|
|
14
|
-
def
|
|
14
|
+
def valid_types: (String input, Array[untyped] candidates) -> Array[Symbol]
|
|
15
15
|
def precompute: () -> void
|
|
16
16
|
def build_discriminator_sets: () -> void
|
|
17
17
|
def build_length_table: () -> void
|
data/sig/sec_id/scanner.rbs
CHANGED
|
@@ -33,7 +33,7 @@ module SecID
|
|
|
33
33
|
def scan_text: (String input, Array[singleton(Base)] target_classes) { (Match) -> void } -> void
|
|
34
34
|
def identify_candidate: (untyped match_data, Array[singleton(Base)] target_classes) -> Match?
|
|
35
35
|
def try_classes: (String raw, String cleaned, Integer start_pos, Array[singleton(Base)] classes) -> Match?
|
|
36
|
-
def best_match: (String cleaned, Array[singleton(Base)] classes) ->
|
|
36
|
+
def best_match: (String cleaned, Array[singleton(Base)] classes) -> Base?
|
|
37
37
|
|
|
38
38
|
@classes: Array[singleton(Base)]
|
|
39
39
|
@fisn_classes: Array[singleton(Base)]
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sec_id
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 8.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Leonid Svyatov
|
|
@@ -119,15 +119,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
119
119
|
requirements:
|
|
120
120
|
- - ">="
|
|
121
121
|
- !ruby/object:Gem::Version
|
|
122
|
-
version: 3.
|
|
122
|
+
version: 3.3.0
|
|
123
123
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
124
124
|
requirements:
|
|
125
125
|
- - ">="
|
|
126
126
|
- !ruby/object:Gem::Version
|
|
127
127
|
version: '0'
|
|
128
128
|
requirements: []
|
|
129
|
-
rubygems_version: 4.0.
|
|
129
|
+
rubygems_version: 4.0.20
|
|
130
130
|
specification_version: 4
|
|
131
|
-
summary: A Ruby toolkit for securities identifiers
|
|
132
|
-
convert, generate, classify, and repair.
|
|
131
|
+
summary: 'A Ruby toolkit for securities identifiers: validate, parse, normalize, detect,
|
|
132
|
+
convert, generate, classify, and repair.'
|
|
133
133
|
test_files: []
|