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.
@@ -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 false candidate ever escapes; the {HOMOGLYPHS} table bounds recall.
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(luhn_sum_double_add_double(reversed_digits_single(identifier)))
64
+ mod10(luhn_sum_single(identifier))
65
65
  end
66
66
 
67
67
  # Generates a random CUSIP body: 8 alphanumeric characters.
@@ -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.dup.freeze
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
- validate_and_sort(input, candidates)
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
- matches = candidates.filter_map { |klass| (i = klass.new(input)).valid? ? i : nil }
61
- matches.min_by { |instance| instance.class.detection_priority }
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 and returns sorted symbol keys.
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 validate_and_sort(input, candidates)
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 = Hash.new { |h, k| h[k] = [] }
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(luhn_sum_indexed(reversed_digits_single(identifier)))
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".each_char.map { |char| CHAR_TO_DIGIT.fetch(char) }.join
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(reversed_digits_multi(identifier)))
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.map { |char| CHAR_TO_DIGIT.fetch(char) }.join
93
+ identifier.each_char.with_object(+'') { |char, numeric| numeric << DECIMAL_VALUES.fetch(char) }
94
94
  end
95
95
  end
96
96
  end
@@ -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.new(cleaned))
121
+ Match.new(type: best.class.type_key, raw: raw, range: start_pos...end_pos, identifier: best)
122
122
  end
123
123
 
124
- # @return [Class, nil]
124
+ # @return [Base, nil]
125
125
  def best_match(cleaned, classes)
126
126
  return if classes.empty?
127
127
 
128
- candidates = (@candidates_by_length[cleaned.length] || []) & classes
129
- return if candidates.empty?
128
+ candidates = @candidates_by_length[cleaned.length]
129
+ return unless candidates
130
130
 
131
- validated = candidates.select { |k| cleaned.match?(k::VALID_CHARS_REGEX) && k.valid?(cleaned) }
132
- validated.min_by(&:detection_priority)
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
@@ -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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module SecID
4
4
  # Current gem version.
5
- VERSION = '7.1.0'
5
+ VERSION = '8.0.0'
6
6
  end
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 { |suggestion| [Suggestable::RANK.fetch(suggestion.edit), self[suggestion.type].detection_priority.last] }
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 — validate, parse, normalize, detect, convert, ' \
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.2.0'
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[untyped, untyped]
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
- def luhn_sum_double_add_double: (Array[Integer] digits) -> Integer
37
- def luhn_sum_indexed: (Array[Integer] digits) -> Integer
38
- def luhn_sum_standard: (Array[Integer] digits) -> Integer
39
- def reversed_digits_single: (untyped id) -> Array[Integer]
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?
@@ -11,7 +11,7 @@ module SecID
11
11
  private
12
12
 
13
13
  def filter_candidates: (String upcased) -> Array[singleton(Base)]
14
- def validate_and_sort: (String input, Array[untyped] candidates) -> Array[Symbol]
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
@@ -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) -> singleton(Base)?
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: 7.1.0
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.2.0
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.12
129
+ rubygems_version: 4.0.20
130
130
  specification_version: 4
131
- summary: A Ruby toolkit for securities identifiers — validate, parse, normalize, detect,
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: []