sec_id 6.0.0 → 7.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/MIGRATION.md +147 -0
  4. data/README.md +116 -44
  5. data/lib/sec_id/active_model.rb +1 -1
  6. data/lib/sec_id/base.rb +63 -14
  7. data/lib/sec_id/cei.rb +7 -7
  8. data/lib/sec_id/cik.rb +1 -1
  9. data/lib/sec_id/concerns/checkable.rb +90 -46
  10. data/lib/sec_id/concerns/generatable.rb +5 -5
  11. data/lib/sec_id/concerns/normalizable.rb +4 -4
  12. data/lib/sec_id/concerns/validatable.rb +7 -7
  13. data/lib/sec_id/cusip.rb +8 -8
  14. data/lib/sec_id/deprecation.rb +26 -0
  15. data/lib/sec_id/detector.rb +3 -26
  16. data/lib/sec_id/dti.rb +8 -14
  17. data/lib/sec_id/figi.rb +9 -9
  18. data/lib/sec_id/iban.rb +20 -20
  19. data/lib/sec_id/isin.rb +9 -9
  20. data/lib/sec_id/lei.rb +10 -10
  21. data/lib/sec_id/occ.rb +1 -1
  22. data/lib/sec_id/scanner.rb +17 -22
  23. data/lib/sec_id/sedol.rb +11 -11
  24. data/lib/sec_id/upi.rb +76 -0
  25. data/lib/sec_id/valoren.rb +2 -2
  26. data/lib/sec_id/version.rb +1 -1
  27. data/lib/sec_id/wkn.rb +1 -1
  28. data/lib/sec_id.rb +11 -6
  29. data/sec_id.gemspec +2 -2
  30. data/sig/sec_id/base.rbs +15 -4
  31. data/sig/sec_id/cei.rbs +4 -0
  32. data/sig/sec_id/concerns/checkable.rbs +17 -7
  33. data/sig/sec_id/concerns/generatable.rbs +1 -1
  34. data/sig/sec_id/concerns/validatable.rbs +1 -1
  35. data/sig/sec_id/cusip.rbs +4 -0
  36. data/sig/sec_id/deprecation.rbs +6 -0
  37. data/sig/sec_id/detector.rbs +0 -6
  38. data/sig/sec_id/dti.rbs +6 -1
  39. data/sig/sec_id/figi.rbs +4 -0
  40. data/sig/sec_id/iban.rbs +7 -3
  41. data/sig/sec_id/isin.rbs +4 -0
  42. data/sig/sec_id/lei.rbs +5 -1
  43. data/sig/sec_id/scanner.rbs +2 -4
  44. data/sig/sec_id/sedol.rbs +4 -0
  45. data/sig/sec_id/upi.rbs +29 -0
  46. data/sig/sec_id.rbs +5 -2
  47. metadata +7 -3
@@ -58,7 +58,7 @@ module SecID
58
58
 
59
59
  candidates = filter_candidates(input.upcase)
60
60
  matches = candidates.filter_map { |klass| (i = klass.new(input)).valid? ? i : nil }
61
- matches.min_by { |instance| @priority_for[instance.class] }
61
+ matches.min_by { |instance| instance.class.detection_priority }
62
62
  end
63
63
 
64
64
  private
@@ -81,16 +81,14 @@ module SecID
81
81
  # @return [Array<Symbol>]
82
82
  def validate_and_sort(input, candidates)
83
83
  matches = candidates.select { |klass| klass.valid?(input) }
84
- matches.sort_by! { |klass| @priority_for[klass] }
85
- matches.map! { |klass| @key_for[klass] }
84
+ matches.sort_by!(&:detection_priority)
85
+ matches.map!(&:type_key)
86
86
  end
87
87
 
88
88
  # @return [void]
89
89
  def precompute
90
90
  build_discriminator_sets
91
91
  build_length_table
92
- build_priority_table
93
- build_key_table
94
92
  end
95
93
 
96
94
  # Classifies types by which special characters their VALID_CHARS_REGEX accepts.
@@ -114,27 +112,6 @@ module SecID
114
112
  @candidates_by_length.each_value(&:freeze)
115
113
  end
116
114
 
117
- # Builds composite sort keys: check-digit types first, then smaller range, then load order.
118
- #
119
- # @return [void]
120
- def build_priority_table
121
- @priority_for = {}
122
- @classes.each_with_index do |klass, index|
123
- check_digit_rank = klass.has_check_digit? ? 0 : 1
124
- @priority_for[klass] = [check_digit_rank, klass.length_specificity, index].freeze
125
- end
126
- @priority_for.freeze
127
- end
128
-
129
- # Maps each class to its registry symbol key.
130
- #
131
- # @return [void]
132
- def build_key_table
133
- @key_for = {}
134
- @classes.each { |klass| @key_for[klass] = klass.short_name.downcase.to_sym }
135
- @key_for.freeze
136
- end
137
-
138
115
  # Stage 1: route strings with special characters to the only types that accept them.
139
116
  # Returns nil if no special chars found (fall through to stage 2).
140
117
  #
data/lib/sec_id/dti.rb CHANGED
@@ -13,7 +13,7 @@ module SecID
13
13
  # @example Validate a DTI
14
14
  # SecID::DTI.valid?('X9J9K872S') #=> true
15
15
  #
16
- # @example Restore check digit
16
+ # @example Restore checksum
17
17
  # SecID::DTI.restore('X9J9K872') #=> 'X9J9K872S'
18
18
  class DTI < Base
19
19
  include Checkable
@@ -33,7 +33,7 @@ module SecID
33
33
  (?<identifier>
34
34
  [1-9B-DF-HJ-NP-TV-XZ]
35
35
  [0-9B-DF-HJ-NP-TV-XZ]{7})
36
- (?<check_digit>[0-9B-DF-HJ-NP-TV-XZ])?
36
+ (?<checksum>[0-9B-DF-HJ-NP-TV-XZ])?
37
37
  \z/x
38
38
 
39
39
  # The 30-symbol DTI alphabet, ordered by check-character value (0-29).
@@ -55,14 +55,14 @@ module SecID
55
55
  def initialize(dti)
56
56
  dti_parts = parse dti
57
57
  @identifier = dti_parts[:identifier]
58
- @check_digit = dti_parts[:check_digit]
58
+ @checksum = dti_parts[:checksum]
59
59
  end
60
60
 
61
61
  # @return [String] the calculated or grandfathered check character
62
62
  # @raise [InvalidFormatError] if the DTI format is invalid
63
- def calculate_check_digit
63
+ def calculate_checksum
64
64
  validate_format_for_calculation!
65
- grandfathered_check_digit(identifier) || iso7064_mod31_30_check_char(identifier)
65
+ grandfathered_checksum(identifier) || iso7064_mod31_30_check_char(identifier)
66
66
  end
67
67
 
68
68
  # Generates a random DTI body: first character non-zero, 8 characters total,
@@ -79,11 +79,11 @@ module SecID
79
79
  private
80
80
 
81
81
  # @return [Hash]
82
- def components = { check_digit: }
82
+ def components = { checksum: }
83
83
 
84
84
  # @param base [String] the 8-character DTI base
85
85
  # @return [String, nil] the registered check character, or nil if not grandfathered
86
- def grandfathered_check_digit(base)
86
+ def grandfathered_checksum(base)
87
87
  GRANDFATHERED_CODES[base]&.delete_prefix(base)
88
88
  end
89
89
 
@@ -92,13 +92,7 @@ module SecID
92
92
  # @param base [String] the 8-character DTI base
93
93
  # @return [String] the single computed check character
94
94
  def iso7064_mod31_30_check_char(base)
95
- perm = 30
96
- base.each_char do |c|
97
- s = (perm + ALPHABET_VALUE.fetch(c)) % 30
98
- s = 30 if s.zero?
99
- perm = (s * 2) % 31
100
- end
101
- ALPHABET.fetch((31 - perm) % 30)
95
+ mod31_30_check_char(base, ALPHABET, ALPHABET_VALUE)
102
96
  end
103
97
  end
104
98
  end
data/lib/sec_id/figi.rb CHANGED
@@ -4,7 +4,7 @@ module SecID
4
4
  # Financial Instrument Global Identifier (FIGI) - a 12-character alphanumeric code
5
5
  # that uniquely identifies financial instruments.
6
6
  #
7
- # Format: 2-character prefix + 'G' + 8-character random part + 1-digit check digit
7
+ # Format: 2-character prefix + 'G' + 8-character random part + 1-digit checksum
8
8
  # Note: FIGI excludes vowels (A, E, I, O, U) from valid characters.
9
9
  #
10
10
  # @see https://en.wikipedia.org/wiki/Financial_Instrument_Global_Identifier
@@ -12,7 +12,7 @@ module SecID
12
12
  # @example Validate a FIGI
13
13
  # SecID::FIGI.valid?('BBG000BLNQ16') #=> true
14
14
  #
15
- # @example Restore check digit
15
+ # @example Restore checksum
16
16
  # SecID::FIGI.restore!('BBG000BLNQ1') #=> #<SecID::FIGI>
17
17
  class FIGI < Base
18
18
  include Checkable
@@ -33,7 +33,7 @@ module SecID
33
33
  (?<prefix>[B-DF-HJ-NP-TV-Z0-9]{2})
34
34
  G
35
35
  (?<random_part>[B-DF-HJ-NP-TV-Z0-9]{8}))
36
- (?<check_digit>\d)?
36
+ (?<checksum>\d)?
37
37
  \z/x
38
38
 
39
39
  # Country-code prefixes that are restricted from use in FIGI.
@@ -54,19 +54,19 @@ module SecID
54
54
  @identifier = figi_parts[:identifier]
55
55
  @prefix = figi_parts[:prefix]
56
56
  @random_part = figi_parts[:random_part]
57
- @check_digit = figi_parts[:check_digit]&.to_i
57
+ @checksum = figi_parts[:checksum]&.to_i
58
58
  end
59
59
 
60
60
  # @return [String, nil]
61
61
  def to_pretty_s
62
62
  return nil unless valid?
63
63
 
64
- "#{prefix}G #{random_part} #{check_digit}"
64
+ "#{prefix}G #{random_part} #{checksum}"
65
65
  end
66
66
 
67
- # @return [Integer] the calculated check digit (0-9)
67
+ # @return [Integer] the calculated checksum (0-9)
68
68
  # @raise [InvalidFormatError] if the FIGI format is invalid
69
- def calculate_check_digit
69
+ def calculate_checksum
70
70
  validate_format_for_calculation!
71
71
  mod10(luhn_sum_indexed(reversed_digits_single(identifier)))
72
72
  end
@@ -74,7 +74,7 @@ module SecID
74
74
  # Generates a random FIGI body: non-reserved 2-char prefix + 'G' + 8-char random part.
75
75
  #
76
76
  # @param random [Random] source of randomness
77
- # @return [String] an 11-character FIGI body without check digit
77
+ # @return [String] an 11-character FIGI body without checksum
78
78
  def self.generate_body(random)
79
79
  prefix = random_string(GENERATE_CHARSET, 2, random: random)
80
80
  prefix = random_string(GENERATE_CHARSET, 2, random: random) while RESTRICTED_PREFIXES.include?(prefix)
@@ -85,7 +85,7 @@ module SecID
85
85
  private
86
86
 
87
87
  # @return [Hash]
88
- def components = { prefix:, random_part:, check_digit: }
88
+ def components = { prefix:, random_part:, checksum: }
89
89
 
90
90
  # @return [Boolean]
91
91
  def valid_format?
data/lib/sec_id/iban.rb CHANGED
@@ -6,8 +6,8 @@ module SecID
6
6
  # International Bank Account Number (IBAN) - an international standard for identifying
7
7
  # bank accounts across national borders (ISO 13616).
8
8
  #
9
- # Format: 2-letter country code + 2-digit check digits + BBAN (Basic Bank Account Number, 11-30 chars)
10
- # Note: Unlike other SecID identifiers, the check digits are in positions 3-4, not at the end.
9
+ # Format: 2-letter country code + 2-digit checksum + BBAN (Basic Bank Account Number, 11-30 chars)
10
+ # Note: Unlike other SecID identifiers, the checksum are in positions 3-4, not at the end.
11
11
  #
12
12
  # @see https://en.wikipedia.org/wiki/International_Bank_Account_Number
13
13
  # @see https://www.iban.com/structure
@@ -15,7 +15,7 @@ module SecID
15
15
  # @example Validate an IBAN
16
16
  # SecID::IBAN.valid?('DE89370400440532013000') #=> true
17
17
  #
18
- # @example Restore check digits
18
+ # @example Restore checksum
19
19
  # SecID::IBAN.restore!('DE00370400440532013000') #=> #<SecID::IBAN>
20
20
  class IBAN < Base
21
21
  include Checkable
@@ -30,7 +30,7 @@ module SecID
30
30
  VALID_CHARS_REGEX = /\A[A-Z0-9]+\z/
31
31
 
32
32
  # Regular expression for parsing IBAN components.
33
- # Note: Check digit positioning is handled in initialize, not in the regex.
33
+ # Note: Checksum positioning is handled in initialize, not in the regex.
34
34
  ID_REGEX = /\A
35
35
  (?<country_code>[A-Z]{2})
36
36
  (?<rest>[A-Z0-9]{13,32})
@@ -62,7 +62,7 @@ module SecID
62
62
  # @return [String, nil] the account number (extracted from BBAN if country rules define it)
63
63
  attr_reader :account_number
64
64
 
65
- # @return [String, nil] the national check digit (extracted from BBAN if country rules define it)
65
+ # @return [String, nil] the national checksum (extracted from BBAN if country rules define it)
66
66
  attr_reader :national_check
67
67
 
68
68
  # @param iban [String] the IBAN string to parse
@@ -72,7 +72,7 @@ module SecID
72
72
  rest = iban_parts[:rest]
73
73
 
74
74
  if @country_code && rest
75
- extract_check_digit_and_bban(rest)
75
+ extract_checksum_and_bban(rest)
76
76
  @identifier = "#{@country_code}#{@bban}" if @bban
77
77
  end
78
78
 
@@ -82,19 +82,19 @@ module SecID
82
82
  # @return [String]
83
83
  # @raise [InvalidFormatError] if the IBAN format is invalid
84
84
  def restore
85
- cd = calculate_check_digit
85
+ cd = calculate_checksum
86
86
  "#{country_code}#{cd.to_s.rjust(2, '0')}#{bban}"
87
87
  end
88
88
 
89
89
  # @return [Integer] the calculated 2-digit check value (1-98)
90
90
  # @raise [InvalidFormatError] if the IBAN format is invalid
91
- def calculate_check_digit
91
+ def calculate_checksum
92
92
  validate_format_for_calculation!
93
93
  mod97(numeric_string_for_check)
94
94
  end
95
95
 
96
- # Generates a random IBAN body for a numeric-BBAN country with placeholder check digits.
97
- # The default {Generatable::ClassMethods#generate} restores the real check digits via `restore!`.
96
+ # Generates a random IBAN body for a numeric-BBAN country with placeholder checksum.
97
+ # The default {Generatable::ClassMethods#generate} restores the real checksum via `restore!`.
98
98
  #
99
99
  # @param random [Random] source of randomness
100
100
  # @return [String] a country code + "00" + numeric BBAN
@@ -126,9 +126,9 @@ module SecID
126
126
 
127
127
  # @return [String]
128
128
  def to_s
129
- return full_id unless check_digit
129
+ return full_id unless checksum
130
130
 
131
- "#{country_code}#{check_digit.to_s.rjust(2, '0')}#{bban}"
131
+ "#{country_code}#{checksum.to_s.rjust(2, '0')}#{bban}"
132
132
  end
133
133
 
134
134
  # @return [String, nil]
@@ -140,7 +140,7 @@ module SecID
140
140
 
141
141
  # @return [Hash]
142
142
  def components
143
- hash = { country_code:, bban:, check_digit: }
143
+ hash = { country_code:, bban:, checksum: }
144
144
  hash[:bank_code] = bank_code if bank_code
145
145
  hash[:branch_code] = branch_code if branch_code
146
146
  hash[:account_number] = account_number if account_number
@@ -149,7 +149,7 @@ module SecID
149
149
  end
150
150
 
151
151
  # @return [Integer]
152
- def check_digit_width
152
+ def checksum_width
153
153
  2
154
154
  end
155
155
 
@@ -177,14 +177,14 @@ module SecID
177
177
 
178
178
  # @param rest [String] the IBAN string after country code
179
179
  # @return [void]
180
- def extract_check_digit_and_bban(rest)
180
+ def extract_checksum_and_bban(rest)
181
181
  expected = expected_bban_length_for_country
182
182
 
183
- if check_digits?(rest, expected)
184
- @check_digit = rest[0, 2].to_i
183
+ if checksum?(rest, expected)
184
+ @checksum = rest[0, 2].to_i
185
185
  @bban = rest[2..]
186
186
  else
187
- @check_digit = nil
187
+ @checksum = nil
188
188
  @bban = rest
189
189
  end
190
190
  end
@@ -197,11 +197,11 @@ module SecID
197
197
  # @param rest [String] the IBAN string after country code
198
198
  # @param expected_bban_length [Integer, nil] the expected BBAN length for the country
199
199
  # @return [Boolean]
200
- def check_digits?(rest, expected_bban_length)
200
+ def checksum?(rest, expected_bban_length)
201
201
  return false unless rest[0, 2].match?(/\A\d{2}\z/)
202
202
  return true unless expected_bban_length
203
203
 
204
- # If we know expected BBAN length, check if rest matches with or without check digits
204
+ # If we know expected BBAN length, check if rest matches with or without checksum
205
205
  rest.length == expected_bban_length + 2 || rest.length != expected_bban_length
206
206
  end
207
207
 
data/lib/sec_id/isin.rb CHANGED
@@ -4,14 +4,14 @@ module SecID
4
4
  # International Securities Identification Number (ISIN) - a 12-character alphanumeric code
5
5
  # that uniquely identifies a security globally.
6
6
  #
7
- # Format: 2-letter country code + 9-character NSIN + 1-digit check digit
7
+ # Format: 2-letter country code + 9-character NSIN + 1-digit checksum
8
8
  #
9
9
  # @see https://en.wikipedia.org/wiki/International_Securities_Identification_Number
10
10
  #
11
11
  # @example Validate an ISIN
12
12
  # SecID::ISIN.valid?('US5949181045') #=> true
13
13
  #
14
- # @example Restore check digit
14
+ # @example Restore checksum
15
15
  # SecID::ISIN.restore!('US594918104') #=> #<SecID::ISIN>
16
16
  class ISIN < Base
17
17
  include Checkable
@@ -30,7 +30,7 @@ module SecID
30
30
  (?<identifier>
31
31
  (?<country_code>[A-Z]{2})
32
32
  (?<nsin>[A-Z0-9]{9}))
33
- (?<check_digit>\d)?
33
+ (?<checksum>\d)?
34
34
  \z/x
35
35
 
36
36
  # Country codes that use CUSIP Global Services (CGS) for NSIN assignment.
@@ -71,19 +71,19 @@ module SecID
71
71
  @identifier = isin_parts[:identifier]
72
72
  @country_code = isin_parts[:country_code]
73
73
  @nsin = isin_parts[:nsin]
74
- @check_digit = isin_parts[:check_digit]&.to_i
74
+ @checksum = isin_parts[:checksum]&.to_i
75
75
  end
76
76
 
77
77
  # @return [String, nil]
78
78
  def to_pretty_s
79
79
  return nil unless valid?
80
80
 
81
- "#{country_code} #{nsin} #{check_digit}"
81
+ "#{country_code} #{nsin} #{checksum}"
82
82
  end
83
83
 
84
- # @return [Integer] the calculated check digit (0-9)
84
+ # @return [Integer] the calculated checksum (0-9)
85
85
  # @raise [InvalidFormatError] if the ISIN format is invalid
86
- def calculate_check_digit
86
+ def calculate_checksum
87
87
  validate_format_for_calculation!
88
88
  mod10(luhn_sum_standard(reversed_digits_multi(identifier)))
89
89
  end
@@ -91,7 +91,7 @@ module SecID
91
91
  # Generates a random ISIN body: 2-letter country code + 9-character NSIN.
92
92
  #
93
93
  # @param random [Random] source of randomness
94
- # @return [String] an 11-character ISIN body without check digit
94
+ # @return [String] an 11-character ISIN body without checksum
95
95
  def self.generate_body(random)
96
96
  "#{random_string(ALPHA, 2, random: random)}#{random_string(ALPHANUMERIC, 9, random: random)}"
97
97
  end
@@ -177,6 +177,6 @@ module SecID
177
177
  private
178
178
 
179
179
  # @return [Hash]
180
- def components = { country_code:, nsin:, check_digit: }
180
+ def components = { country_code:, nsin:, checksum: }
181
181
  end
182
182
  end
data/lib/sec_id/lei.rb CHANGED
@@ -4,7 +4,7 @@ module SecID
4
4
  # Legal Entity Identifier (LEI) - a 20-character alphanumeric code that
5
5
  # uniquely identifies legal entities participating in financial transactions.
6
6
  #
7
- # Format: 4-character LOU ID + 2-character reserved + 12-character entity ID + 2-digit check digit
7
+ # Format: 4-character LOU ID + 2-character reserved + 12-character entity ID + 2-digit checksum
8
8
  #
9
9
  # @see https://en.wikipedia.org/wiki/Legal_Entity_Identifier
10
10
  # @see https://www.gleif.org/en/about-lei/iso-17442-the-lei-code-structure
@@ -12,8 +12,8 @@ module SecID
12
12
  # @example Validate a LEI
13
13
  # SecID::LEI.valid?('529900T8BM49AURSDO55') #=> true
14
14
  #
15
- # @example Calculate check digit
16
- # SecID::LEI.check_digit('529900T8BM49AURSDO') #=> 55
15
+ # @example Calculate checksum
16
+ # SecID::LEI.checksum('529900T8BM49AURSDO') #=> 55
17
17
  class LEI < Base
18
18
  include Checkable
19
19
 
@@ -32,7 +32,7 @@ module SecID
32
32
  (?<lou_id>[0-9A-Z]{4})
33
33
  (?<reserved>[0-9A-Z]{2})
34
34
  (?<entity_id>[0-9A-Z]{12}))
35
- (?<check_digit>\d{2})?
35
+ (?<checksum>\d{2})?
36
36
  \z/x
37
37
 
38
38
  # @return [String, nil] the 4-character Local Operating Unit (LOU) identifier
@@ -51,7 +51,7 @@ module SecID
51
51
  @lou_id = lei_parts[:lou_id]
52
52
  @reserved = lei_parts[:reserved]
53
53
  @entity_id = lei_parts[:entity_id]
54
- @check_digit = lei_parts[:check_digit]&.to_i
54
+ @checksum = lei_parts[:checksum]&.to_i
55
55
  end
56
56
 
57
57
  # @return [String, nil]
@@ -61,9 +61,9 @@ module SecID
61
61
  to_s.scan(/.{1,4}/).join(' ')
62
62
  end
63
63
 
64
- # @return [Integer] the calculated 2-digit check digit (1-98)
64
+ # @return [Integer] the calculated 2-digit checksum (1-98)
65
65
  # @raise [InvalidFormatError] if the LEI format is invalid
66
- def calculate_check_digit
66
+ def calculate_checksum
67
67
  validate_format_for_calculation!
68
68
  mod97("#{numeric_identifier}00")
69
69
  end
@@ -71,7 +71,7 @@ module SecID
71
71
  # Generates a random LEI body: 18 alphanumeric characters.
72
72
  #
73
73
  # @param random [Random] source of randomness
74
- # @return [String] an 18-character LEI body without check digits
74
+ # @return [String] an 18-character LEI body without checksum
75
75
  def self.generate_body(random)
76
76
  random_string(ALPHANUMERIC, 18, random: random)
77
77
  end
@@ -80,10 +80,10 @@ module SecID
80
80
  private
81
81
 
82
82
  # @return [Hash]
83
- def components = { lou_id:, reserved:, entity_id:, check_digit: }
83
+ def components = { lou_id:, reserved:, entity_id:, checksum: }
84
84
 
85
85
  # @return [Integer]
86
- def check_digit_width
86
+ def checksum_width
87
87
  2
88
88
  end
89
89
 
data/lib/sec_id/occ.rb CHANGED
@@ -6,7 +6,7 @@ module SecID
6
6
  # OCC Option Symbol - standardized option symbol format used by Option Clearing Corporation.
7
7
  # Format: 6-char underlying (padded) + 6-char date (YYMMDD) + type (C/P) + 8-digit strike (in mills).
8
8
  #
9
- # @note OCC identifiers have no check digit and validation includes both format
9
+ # @note OCC identifiers have no checksum and validation includes both format
10
10
  # and date parseability checks.
11
11
  #
12
12
  # @see https://en.wikipedia.org/wiki/Option_symbol#The_OCC_Option_Symbol
@@ -51,12 +51,24 @@ module SecID
51
51
  private
52
52
 
53
53
  # @return [void]
54
- def precompute # rubocop:disable Metrics/AbcSize
55
- build_key_table
56
- build_priority_table
54
+ def precompute
55
+ build_candidate_partitions
56
+ build_length_table
57
+ end
58
+
59
+ # Splits the registered classes by which CANDIDATE_RE group routes to them.
60
+ #
61
+ # @return [void]
62
+ def build_candidate_partitions
57
63
  @fisn_classes = @classes.select { |k| k.short_name == 'FISN' }
58
64
  @occ_classes = @classes.select { |k| k.short_name == 'OCC' }
59
65
  @simple_classes = @classes - @fisn_classes - @occ_classes
66
+ end
67
+
68
+ # Builds a Hash mapping each possible length to the classes that accept it.
69
+ #
70
+ # @return [void]
71
+ def build_length_table
60
72
  @candidates_by_length = Hash.new { |h, k| h[k] = [] }
61
73
  @classes.each do |klass|
62
74
  klass.length_values.each { |len| @candidates_by_length[len] << klass }
@@ -64,23 +76,6 @@ module SecID
64
76
  @candidates_by_length.each_value(&:freeze)
65
77
  end
66
78
 
67
- # @return [void]
68
- def build_key_table
69
- @key_for = {}
70
- @classes.each { |klass| @key_for[klass] = klass.short_name.downcase.to_sym }
71
- @key_for.freeze
72
- end
73
-
74
- # @return [void]
75
- def build_priority_table
76
- @priority_for = {}
77
- @classes.each_with_index do |klass, index|
78
- check_digit_rank = klass.has_check_digit? ? 0 : 1
79
- @priority_for[klass] = [check_digit_rank, klass.length_specificity, index].freeze
80
- end
81
- @priority_for.freeze
82
- end
83
-
84
79
  # @param input [String]
85
80
  # @param target_classes [Array<Class>]
86
81
  # @return [void]
@@ -123,7 +118,7 @@ module SecID
123
118
  return unless best
124
119
 
125
120
  end_pos = start_pos + raw.length
126
- Match.new(type: @key_for[best], raw: raw, range: start_pos...end_pos, identifier: best.new(cleaned))
121
+ Match.new(type: best.type_key, raw: raw, range: start_pos...end_pos, identifier: best.new(cleaned))
127
122
  end
128
123
 
129
124
  # @return [Class, nil]
@@ -134,7 +129,7 @@ module SecID
134
129
  return if candidates.empty?
135
130
 
136
131
  validated = candidates.select { |k| cleaned.match?(k::VALID_CHARS_REGEX) && k.valid?(cleaned) }
137
- validated.min_by { |k| @priority_for[k] }
132
+ validated.min_by(&:detection_priority)
138
133
  end
139
134
  end
140
135
  end
data/lib/sec_id/sedol.rb CHANGED
@@ -4,7 +4,7 @@ module SecID
4
4
  # Stock Exchange Daily Official List (SEDOL) - a 7-character alphanumeric code
5
5
  # that identifies securities traded on the London Stock Exchange and other UK exchanges.
6
6
  #
7
- # Format: 6-character identifier + 1-digit check digit
7
+ # Format: 6-character identifier + 1-digit checksum
8
8
  # Note: SEDOL excludes vowels (A, E, I, O, U) to avoid forming words.
9
9
  #
10
10
  # @see https://en.wikipedia.org/wiki/SEDOL
@@ -12,8 +12,8 @@ module SecID
12
12
  # @example Validate a SEDOL
13
13
  # SecID::SEDOL.valid?('B19GKT4') #=> true
14
14
  #
15
- # @example Calculate check digit
16
- # SecID::SEDOL.check_digit('B19GKT') #=> 4
15
+ # @example Calculate checksum
16
+ # SecID::SEDOL.checksum('B19GKT') #=> 4
17
17
  class SEDOL < Base
18
18
  include Checkable
19
19
 
@@ -30,10 +30,10 @@ module SecID
30
30
  # Excludes vowels (A, E, I, O, U) from valid characters.
31
31
  ID_REGEX = /\A
32
32
  (?<identifier>[0-9BCDFGHJKLMNPQRSTVWXYZ]{6})
33
- (?<check_digit>\d)?
33
+ (?<checksum>\d)?
34
34
  \z/x
35
35
 
36
- # Weights applied to each character position in the check digit calculation.
36
+ # Weights applied to each character position in the checksum calculation.
37
37
  CHARACTER_WEIGHTS = [1, 3, 1, 7, 3, 9].freeze
38
38
 
39
39
  # Characters valid in a SEDOL body (alphanumeric excluding vowels).
@@ -43,14 +43,14 @@ module SecID
43
43
  def initialize(sedol)
44
44
  sedol_parts = parse sedol
45
45
  @identifier = sedol_parts[:identifier]
46
- @check_digit = sedol_parts[:check_digit]&.to_i
46
+ @checksum = sedol_parts[:checksum]&.to_i
47
47
  end
48
48
 
49
49
  # Valid country codes for SEDOL to ISIN conversion.
50
50
  ISIN_COUNTRY_CODES = Set.new(%w[GB IE GG IM JE FK]).freeze
51
51
 
52
52
  # @param country_code [String] the ISO 3166-1 alpha-2 country code (default: 'GB')
53
- # @return [ISIN] a new ISIN instance with calculated check digit
53
+ # @return [ISIN] a new ISIN instance with calculated checksum
54
54
  # @raise [InvalidFormatError] if the country code is not valid for SEDOL
55
55
  def to_isin(country_code = 'GB')
56
56
  unless ISIN_COUNTRY_CODES.include?(country_code)
@@ -60,9 +60,9 @@ module SecID
60
60
  ISIN.new("#{country_code}00#{restore}").restore!
61
61
  end
62
62
 
63
- # @return [Integer] the calculated check digit (0-9)
63
+ # @return [Integer] the calculated checksum (0-9)
64
64
  # @raise [InvalidFormatError] if the SEDOL format is invalid
65
- def calculate_check_digit
65
+ def calculate_checksum
66
66
  validate_format_for_calculation!
67
67
  mod10(weighted_sum)
68
68
  end
@@ -70,7 +70,7 @@ module SecID
70
70
  # Generates a random SEDOL body: 6 characters excluding vowels.
71
71
  #
72
72
  # @param random [Random] source of randomness
73
- # @return [String] a 6-character SEDOL body without check digit
73
+ # @return [String] a 6-character SEDOL body without checksum
74
74
  def self.generate_body(random)
75
75
  random_string(GENERATE_CHARSET, 6, random: random)
76
76
  end
@@ -79,7 +79,7 @@ module SecID
79
79
  private
80
80
 
81
81
  # @return [Hash]
82
- def components = { check_digit: }
82
+ def components = { checksum: }
83
83
 
84
84
  # NOTE: Not idiomatic Ruby, but optimized for performance.
85
85
  #