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
data/lib/sec_id/base.rb CHANGED
@@ -8,28 +8,28 @@ module SecID
8
8
  # - ID_REGEX constant with named capture groups for parsing
9
9
  # - initialize method that calls parse and extracts components
10
10
  #
11
- # Subclasses with check digits should also include the Checkable concern,
12
- # which provides check-digit validation, calculation, and restoration.
11
+ # Subclasses with checksum should also include the Checkable concern,
12
+ # which provides checksum validation, calculation, and restoration.
13
13
  #
14
- # @example Implementing a check-digit identifier
14
+ # @example Implementing a checksum identifier
15
15
  # class MyIdentifier < Base
16
16
  # include Checkable
17
17
  #
18
- # ID_REGEX = /\A(?<identifier>[A-Z]{6})(?<check_digit>\d)?\z/x
18
+ # ID_REGEX = /\A(?<identifier>[A-Z]{6})(?<checksum>\d)?\z/x
19
19
  #
20
20
  # def initialize(id)
21
21
  # parts = parse(id)
22
22
  # @identifier = parts[:identifier]
23
- # @check_digit = parts[:check_digit]&.to_i
23
+ # @checksum = parts[:checksum]&.to_i
24
24
  # end
25
25
  #
26
- # def calculate_check_digit
26
+ # def calculate_checksum
27
27
  # validate_format_for_calculation!
28
28
  # mod10(some_algorithm)
29
29
  # end
30
30
  # end
31
31
  #
32
- # @example Implementing a non-check-digit identifier
32
+ # @example Implementing a non-checksum identifier
33
33
  # class SimpleId < Base
34
34
  # ID_REGEX = /\A(?<identifier>[A-Z]{6})\z/x
35
35
  #
@@ -46,10 +46,26 @@ module SecID
46
46
  # @return [String] the original input after normalization (stripped and uppercased)
47
47
  attr_reader :full_id
48
48
 
49
- # @return [String, nil] the main identifier portion (without check digit)
49
+ # @return [String, nil] the main identifier portion (without checksum)
50
50
  attr_reader :identifier
51
51
 
52
52
  class << self
53
+ # The type's registry symbol: `SecID[SecID::ISIN.type_key] == SecID::ISIN`.
54
+ #
55
+ # @return [Symbol] the registry key (e.g. :isin, :cusip)
56
+ def type_key = @type_key ||= short_name.downcase.to_sym
57
+
58
+ # Composite sort key ranking detection specificity: checksum types first,
59
+ # then narrower length range, then registration order. A class the registry
60
+ # never saw sorts last.
61
+ #
62
+ # @api private
63
+ # @return [Array] frozen [checksum rank, length specificity, registration order]
64
+ def detection_priority
65
+ @detection_priority ||=
66
+ [has_checksum? ? 0 : 1, length_specificity, SecID.identifiers.index(self) || Float::INFINITY].freeze
67
+ end
68
+
53
69
  # @return [String] the unqualified class name (e.g. "ISIN", "CUSIP")
54
70
  def short_name = @short_name ||= name.split('::').last
55
71
 
@@ -73,11 +89,19 @@ module SecID
73
89
  # @return [String] a representative valid identifier string
74
90
  def example = self::EXAMPLE
75
91
 
76
- # @return [Boolean] true if this identifier type uses a check digit
77
- def has_check_digit?
78
- return @has_check_digit if defined?(@has_check_digit)
92
+ # @return [Boolean] true if this identifier type uses a checksum
93
+ def has_checksum?
94
+ return @has_checksum if defined?(@has_checksum)
95
+
96
+ @has_checksum = ancestors.include?(SecID::Checkable)
97
+ end
79
98
 
80
- @has_check_digit = ancestors.include?(SecID::Checkable)
99
+ # @deprecated Use {.has_checksum?}. Kept as a v7 bridge; removed in v8.
100
+ #
101
+ # @return [Boolean]
102
+ def has_check_digit?
103
+ SecID::Deprecation.warn(old: 'has_check_digit?', new: 'has_checksum?')
104
+ has_checksum?
81
105
  end
82
106
  end
83
107
 
@@ -114,11 +138,11 @@ module SecID
114
138
  # @return [Hash] hash with :type, :full_id, :normalized, :valid, and :components keys
115
139
  def to_h
116
140
  {
117
- type: self.class.short_name.downcase.to_sym,
141
+ type: self.class.type_key,
118
142
  full_id: full_id,
119
143
  normalized: valid? ? normalized : nil,
120
144
  valid: valid?,
121
- components: components
145
+ components: components_with_deprecation_bridge
122
146
  }
123
147
  end
124
148
 
@@ -129,6 +153,20 @@ module SecID
129
153
  to_h
130
154
  end
131
155
 
156
+ # Exposes the parsed components for `case/in` destructuring. Validity is not part of
157
+ # the protocol: components of unparseable input are `nil`, and `SecID.parse` returning
158
+ # `nil` is the validity guard.
159
+ #
160
+ # @param _keys [Array<Symbol>, nil] the keys the pattern requests; ignored
161
+ # @return [Hash] the parsed components
162
+ #
163
+ # @example
164
+ # case SecID.parse('US5949181045')
165
+ # in SecID::ISIN[country_code:, nsin:] then [country_code, nsin]
166
+ # in nil then :invalid
167
+ # end #=> ['US', '594918104']
168
+ def deconstruct_keys(_keys) = components_with_deprecation_bridge
169
+
132
170
  protected
133
171
 
134
172
  # @return [String]
@@ -138,6 +176,17 @@ module SecID
138
176
 
139
177
  private
140
178
 
179
+ # Mirrors the canonical `:checksum` components key onto the deprecated `:check_digit`
180
+ # key for the v7 bridge, when a checksum is present. The mirror reads `:checksum`, so the
181
+ # deprecated `check_digit` reader never fires internally. Removed in v8 (revert `to_h` and
182
+ # `deconstruct_keys` to call `components` directly).
183
+ #
184
+ # @return [Hash] the parsed components, with `:check_digit` added when `:checksum` is present
185
+ def components_with_deprecation_bridge
186
+ parsed = components
187
+ parsed.key?(:checksum) ? parsed.merge(check_digit: parsed[:checksum]) : parsed
188
+ end
189
+
141
190
  # @return [Hash]
142
191
  def components
143
192
  {}
data/lib/sec_id/cei.rb CHANGED
@@ -4,7 +4,7 @@ module SecID
4
4
  # CUSIP Entity Identifier (CEI) - a 10-character alphanumeric code that identifies
5
5
  # legal entities in the syndicated loan market.
6
6
  #
7
- # Format: 1 alpha + 1 digit + 7 alphanumeric + 1 check digit
7
+ # Format: 1 alpha + 1 digit + 7 alphanumeric + 1 checksum
8
8
  #
9
9
  # @see https://www.cusip.com/identifiers.html
10
10
  #
@@ -28,7 +28,7 @@ module SecID
28
28
  (?<prefix>[A-Z])
29
29
  (?<numeric>[0-9])
30
30
  (?<entity_id>[A-Z0-9]{7}))
31
- (?<check_digit>\d)?
31
+ (?<checksum>\d)?
32
32
  \z/x
33
33
 
34
34
  # @return [String, nil] the first character (alphabetic)
@@ -47,12 +47,12 @@ module SecID
47
47
  @prefix = cei_parts[:prefix]
48
48
  @numeric = cei_parts[:numeric]
49
49
  @entity_id = cei_parts[:entity_id]
50
- @check_digit = cei_parts[:check_digit]&.to_i
50
+ @checksum = cei_parts[:checksum]&.to_i
51
51
  end
52
52
 
53
- # @return [Integer] the calculated check digit (0-9)
53
+ # @return [Integer] the calculated checksum (0-9)
54
54
  # @raise [InvalidFormatError] if the CEI format is invalid
55
- def calculate_check_digit
55
+ def calculate_checksum
56
56
  validate_format_for_calculation!
57
57
  mod10(luhn_sum_double_add_double(reversed_digits_single(identifier)))
58
58
  end
@@ -60,7 +60,7 @@ module SecID
60
60
  # Generates a random CEI body: 1 letter + 1 digit + 7 alphanumeric characters.
61
61
  #
62
62
  # @param random [Random] source of randomness
63
- # @return [String] a 9-character CEI body without check digit
63
+ # @return [String] a 9-character CEI body without checksum
64
64
  def self.generate_body(random)
65
65
  "#{random_string(ALPHA, 1, random: random)}#{random_string(DIGITS, 1, random: random)}" \
66
66
  "#{random_string(ALPHANUMERIC, 7, random: random)}"
@@ -70,6 +70,6 @@ module SecID
70
70
  private
71
71
 
72
72
  # @return [Hash]
73
- def components = { prefix:, numeric:, entity_id:, check_digit: }
73
+ def components = { prefix:, numeric:, entity_id:, checksum: }
74
74
  end
75
75
  end
data/lib/sec_id/cik.rb CHANGED
@@ -4,7 +4,7 @@ module SecID
4
4
  # Central Index Key (CIK) - SEC identifier for entities filing with the SEC.
5
5
  # A 1-10 digit number that uniquely identifies entities in SEC systems.
6
6
  #
7
- # @note CIK identifiers have no check digit and validation is based solely on format.
7
+ # @note CIK identifiers have no checksum and validation is based solely on format.
8
8
  #
9
9
  # @see https://en.wikipedia.org/wiki/Central_Index_Key
10
10
  #
@@ -1,26 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SecID
4
- # Provides check-digit validation and calculation for securities identifiers.
5
- # Include this module in classes that have a check digit as part of their format.
4
+ # Provides checksum validation and calculation for securities identifiers.
5
+ # Include this module in classes that have a checksum as part of their format.
6
6
  #
7
7
  # Including classes must implement:
8
- # - `calculate_check_digit` method that returns the calculated check digit value
8
+ # - `calculate_checksum` method that returns the calculated checksum value
9
9
  #
10
10
  # This module provides:
11
11
  # - Character-to-digit mapping constants
12
- # - Luhn algorithm variants for check-digit calculation
13
- # - `valid?` override that validates format and check digit
12
+ # - Luhn algorithm variants for checksum calculation
13
+ # - `valid?` override that validates format and checksum
14
14
  # - `restore` method returning full identifier string without mutation
15
- # - `restore!` method to calculate and set the check digit, returning `self`
16
- # - `check_digit` attribute
17
- # - Class-level convenience methods: `restore`, `restore!`, `check_digit`
15
+ # - `restore!` method to calculate and set the checksum, returning `self`
16
+ # - `checksum` attribute
17
+ # - Class-level convenience methods: `restore`, `restore!`, `checksum`
18
18
  #
19
19
  # @example Including in an identifier class
20
20
  # class MyIdentifier < Base
21
21
  # include Checkable
22
22
  #
23
- # def calculate_check_digit
23
+ # def calculate_checksum
24
24
  # validate_format_for_calculation!
25
25
  # mod10(luhn_sum_standard(reversed_digits_multi(identifier)))
26
26
  # end
@@ -30,7 +30,7 @@ module SecID
30
30
  module Checkable
31
31
  # Character-to-digit mapping for Luhn algorithm variants.
32
32
  # Maps alphanumeric characters to digit arrays for multi-digit expansion.
33
- # Used by ISIN for check-digit calculation.
33
+ # Used by ISIN for checksum calculation.
34
34
  CHAR_TO_DIGITS = {
35
35
  '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4,
36
36
  '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9,
@@ -44,7 +44,7 @@ module SecID
44
44
 
45
45
  # Character-to-digit mapping for single-digit conversion.
46
46
  # Maps alphanumeric characters to values 0-38 (A=10, B=11, ..., Z=35, *=36, @=37, #=38).
47
- # Used by CUSIP, FIGI, SEDOL, LEI, and IBAN for check-digit calculations.
47
+ # Used by CUSIP, FIGI, SEDOL, LEI, and IBAN for checksum calculations.
48
48
  CHAR_TO_DIGIT = {
49
49
  '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4,
50
50
  '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9,
@@ -58,75 +58,102 @@ module SecID
58
58
 
59
59
  # @api private
60
60
  def self.included(base)
61
- base.attr_reader :check_digit
61
+ base.attr_reader :checksum
62
62
  base.extend(ClassMethods)
63
63
  end
64
64
 
65
65
  # Class methods added when Checkable is included.
66
66
  module ClassMethods
67
- # Returns the full identifier string with correct check digit.
67
+ # Returns the full identifier string with correct checksum.
68
68
  #
69
- # @param id_without_check_digit [String] identifier without or with incorrect check digit
70
- # @return [String] the full identifier with correct check digit
69
+ # @param id_without_checksum [String] identifier without or with incorrect checksum
70
+ # @return [String] the full identifier with correct checksum
71
71
  # @raise [InvalidFormatError] if the identifier format is invalid
72
- def restore(id_without_check_digit)
73
- new(id_without_check_digit).restore
72
+ def restore(id_without_checksum)
73
+ new(id_without_checksum).restore
74
74
  end
75
75
 
76
- # Restores (calculates) the check digit and returns the instance.
76
+ # Restores (calculates) the checksum and returns the instance.
77
77
  #
78
- # @param id_without_check_digit [String] identifier without or with incorrect check digit
79
- # @return [self] the restored instance with correct check digit
78
+ # @param id_without_checksum [String] identifier without or with incorrect checksum
79
+ # @return [self] the restored instance with correct checksum
80
80
  # @raise [InvalidFormatError] if the identifier format is invalid
81
- def restore!(id_without_check_digit)
82
- new(id_without_check_digit).restore!
81
+ def restore!(id_without_checksum)
82
+ new(id_without_checksum).restore!
83
83
  end
84
84
 
85
- # @param id [String] the identifier to calculate check digit for
86
- # @return [Integer, String] the calculated check digit
85
+ # @param id [String] the identifier to calculate checksum for
86
+ # @return [Integer, String] the calculated checksum
87
+ # @raise [InvalidFormatError] if the identifier format is invalid
88
+ def checksum(id)
89
+ new(id).calculate_checksum
90
+ end
91
+
92
+ # @deprecated Use {.checksum}. Kept as a v7 bridge; removed in v8.
93
+ #
94
+ # @param id [String] the identifier to calculate the checksum for
95
+ # @return [Integer, String] the calculated checksum
87
96
  # @raise [InvalidFormatError] if the identifier format is invalid
88
97
  def check_digit(id)
89
- new(id).calculate_check_digit
98
+ SecID::Deprecation.warn(old: 'check_digit', new: 'checksum')
99
+ checksum(id)
90
100
  end
91
101
  end
92
102
 
93
- # Validates format and check digit.
103
+ # Validates format and checksum.
94
104
  #
95
105
  # @return [Boolean]
96
106
  def valid?
97
- super && check_digit == calculate_check_digit
107
+ super && checksum == calculate_checksum
98
108
  end
99
109
 
100
- # Returns the full identifier string with correct check digit without mutation.
110
+ # Returns the full identifier string with correct checksum without mutation.
101
111
  #
102
- # @return [String] the full identifier with correct check digit
112
+ # @return [String] the full identifier with correct checksum
103
113
  # @raise [InvalidFormatError] if the identifier format is invalid
104
114
  def restore
105
- "#{identifier}#{calculate_check_digit.to_s.rjust(check_digit_width, '0')}"
115
+ "#{identifier}#{calculate_checksum.to_s.rjust(checksum_width, '0')}"
106
116
  end
107
117
 
108
- # Calculates and sets the check digit, updating full_id.
118
+ # Calculates and sets the checksum, updating full_id.
109
119
  #
110
120
  # @return [self]
111
121
  # @raise [InvalidFormatError] if the identifier format is invalid
112
122
  def restore!
113
- @check_digit = calculate_check_digit
123
+ @checksum = calculate_checksum
114
124
  @full_id = to_s
115
125
  self
116
126
  end
117
127
 
118
- # Subclasses must override this method to implement their check-digit algorithm.
128
+ # Subclasses must override this method to implement their checksum algorithm.
119
129
  #
120
- # @return [Integer, String] the calculated check digit
130
+ # @return [Integer, String] the calculated checksum
121
131
  # @raise [NotImplementedError] if subclass doesn't implement
122
132
  # @raise [InvalidFormatError] if the identifier format is invalid
123
- def calculate_check_digit
133
+ def calculate_checksum
124
134
  raise NotImplementedError
125
135
  end
126
136
 
127
137
  # @return [String]
128
138
  def to_s
129
- "#{identifier}#{check_digit&.to_s&.rjust(check_digit_width, '0')}"
139
+ "#{identifier}#{checksum&.to_s&.rjust(checksum_width, '0')}"
140
+ end
141
+
142
+ # @deprecated Use {#checksum}. Kept as a v7 bridge; removed in v8.
143
+ #
144
+ # @return [Integer, String]
145
+ def check_digit
146
+ SecID::Deprecation.warn(old: 'check_digit', new: 'checksum')
147
+ checksum
148
+ end
149
+
150
+ # @deprecated Use {#calculate_checksum}. Kept as a v7 bridge; removed in v8.
151
+ #
152
+ # @return [Integer, String]
153
+ # @raise [InvalidFormatError] if the identifier format is invalid
154
+ def calculate_check_digit
155
+ SecID::Deprecation.warn(old: 'calculate_check_digit', new: 'calculate_checksum')
156
+ calculate_checksum
130
157
  end
131
158
 
132
159
  private
@@ -188,27 +215,27 @@ module SecID
188
215
  id.each_char.flat_map { |c| CHAR_TO_DIGITS.fetch(c) }.reverse!
189
216
  end
190
217
 
191
- # Returns error codes including check digit validation.
218
+ # Returns error codes including checksum validation.
192
219
  #
193
220
  # @return [Array<Symbol>]
194
221
  def error_codes
195
222
  return detect_errors unless valid_format?
196
- return [:invalid_check_digit] unless check_digit == calculate_check_digit
223
+ return [:invalid_checksum] unless checksum == calculate_checksum
197
224
 
198
225
  []
199
226
  end
200
227
 
201
228
  # @return [Integer]
202
- def check_digit_width
229
+ def checksum_width
203
230
  1
204
231
  end
205
232
 
206
233
  # @param code [Symbol]
207
234
  # @return [String]
208
235
  def validation_message(code)
209
- if code == :invalid_check_digit
210
- fmt = ->(cd) { cd.to_s.rjust(check_digit_width, '0') }
211
- return "Check digit '#{fmt[check_digit]}' is invalid, expected '#{fmt[calculate_check_digit]}'"
236
+ if code == :invalid_checksum
237
+ fmt = ->(cd) { cd.to_s.rjust(checksum_width, '0') }
238
+ return "Checksum '#{fmt[checksum]}' is invalid, expected '#{fmt[calculate_checksum]}'"
212
239
  end
213
240
 
214
241
  super
@@ -219,11 +246,11 @@ module SecID
219
246
  def validate_format_for_calculation!
220
247
  return if valid_format?
221
248
 
222
- raise InvalidFormatError, "#{self.class.name} '#{full_id}' is invalid and check-digit cannot be calculated!"
249
+ raise InvalidFormatError, "#{self.class.name} '#{full_id}' is invalid and checksum cannot be calculated!"
223
250
  end
224
251
 
225
- # @param sum [Integer] the sum to calculate check digit from
226
- # @return [Integer] check digit (0-9)
252
+ # @param sum [Integer] the sum to calculate checksum from
253
+ # @return [Integer] checksum (0-9)
227
254
  def mod10(sum)
228
255
  (10 - (sum % 10)) % 10
229
256
  end
@@ -235,9 +262,26 @@ module SecID
235
262
  end
236
263
 
237
264
  # @param numeric_string [String] numeric string representation
238
- # @return [Integer] check digit value (1-98)
265
+ # @return [Integer] checksum value (1-98)
239
266
  def mod97(numeric_string)
240
267
  98 - (numeric_string.to_i % 97)
241
268
  end
269
+
270
+ # ISO 7064 hybrid MOD 31,30 check character over a base string, parameterized
271
+ # by the type's 30-symbol alphabet. Shared by DTI and UPI.
272
+ #
273
+ # @param base [String] the base string (identifier without check character)
274
+ # @param alphabet [Array<String>] the 30-symbol alphabet ordered by check value (0-29)
275
+ # @param alphabet_value [Hash{String => Integer}] maps each alphabet character to its value
276
+ # @return [String] the single computed check character
277
+ def mod31_30_check_char(base, alphabet, alphabet_value)
278
+ perm = 30
279
+ base.each_char do |c|
280
+ s = (perm + alphabet_value.fetch(c)) % 30
281
+ s = 30 if s.zero?
282
+ perm = (s * 2) % 31
283
+ end
284
+ alphabet.fetch((31 - perm) % 30)
285
+ end
242
286
  end
243
287
  end
@@ -4,9 +4,9 @@ module SecID
4
4
  # Provides generation of new, format-valid identifiers for use as test fixtures.
5
5
  #
6
6
  # Including classes define a class-level `generate_body(random)` returning a valid
7
- # body (the identifier without its check digit). The default {ClassMethods#generate}
8
- # builds an instance from that body and restores the check digit for check-digit types.
9
- # Types whose shape is not "body (+ check digit)" compose the full identifier in
7
+ # body (the identifier without its checksum). The default {ClassMethods#generate}
8
+ # builds an instance from that body and restores the checksum for checksum types.
9
+ # Types whose shape is not "body (+ checksum)" compose the full identifier in
10
10
  # `generate_body` (CFI, FISN) or override `generate` entirely (OCC).
11
11
  #
12
12
  # @note Generated identifiers are valid in format only — they are not real,
@@ -44,12 +44,12 @@ module SecID
44
44
  # @return [self] a generated instance of the identifier type (e.g. {SecID::ISIN}) for which `valid?` is true
45
45
  def generate(random: Random.new)
46
46
  instance = new(generate_body(random))
47
- has_check_digit? ? instance.restore! : instance
47
+ has_checksum? ? instance.restore! : instance
48
48
  end
49
49
 
50
50
  private
51
51
 
52
- # Subclasses must implement this to return a valid body (identifier without check digit),
52
+ # Subclasses must implement this to return a valid body (identifier without checksum),
53
53
  # unless they override {#generate} entirely (as OCC does).
54
54
  #
55
55
  # @param _random [Random] source of randomness
@@ -19,7 +19,7 @@ module SecID
19
19
  #
20
20
  # @param id [String, #to_s] the identifier to normalize
21
21
  # @return [String] the normalized identifier
22
- # @raise [InvalidFormatError, InvalidCheckDigitError, InvalidStructureError]
22
+ # @raise [InvalidFormatError, InvalidChecksumError, InvalidStructureError]
23
23
  def normalize(id)
24
24
  cleaned = id.to_s.strip.gsub(self::SEPARATORS, '')
25
25
  new(cleaned.upcase).normalized
@@ -38,7 +38,7 @@ module SecID
38
38
  # Returns the canonical normalized form of this identifier.
39
39
  #
40
40
  # @return [String]
41
- # @raise [InvalidFormatError, InvalidCheckDigitError, InvalidStructureError]
41
+ # @raise [InvalidFormatError, InvalidChecksumError, InvalidStructureError]
42
42
  def normalized
43
43
  validate!
44
44
  to_s
@@ -46,13 +46,13 @@ module SecID
46
46
 
47
47
  # @!method normalize
48
48
  # @return [String]
49
- # @raise [InvalidFormatError, InvalidCheckDigitError, InvalidStructureError]
49
+ # @raise [InvalidFormatError, InvalidChecksumError, InvalidStructureError]
50
50
  alias normalize normalized
51
51
 
52
52
  # Normalizes this identifier in place, updating {#full_id}.
53
53
  #
54
54
  # @return [self]
55
- # @raise [InvalidFormatError, InvalidCheckDigitError, InvalidStructureError]
55
+ # @raise [InvalidFormatError, InvalidChecksumError, InvalidStructureError]
56
56
  def normalize!
57
57
  @full_id = normalized
58
58
  self
@@ -8,7 +8,7 @@ module SecID
8
8
  module Validatable
9
9
  # Maps error-code symbols to their exception classes; unmapped codes default to InvalidFormatError.
10
10
  ERROR_MAP = {
11
- invalid_check_digit: InvalidCheckDigitError,
11
+ invalid_checksum: InvalidChecksumError,
12
12
  invalid_prefix: InvalidStructureError,
13
13
  invalid_category: InvalidStructureError,
14
14
  invalid_group: InvalidStructureError,
@@ -43,7 +43,7 @@ module SecID
43
43
  #
44
44
  # @param id [String] the identifier to validate
45
45
  # @return [Base] the identifier instance
46
- # @raise [InvalidFormatError, InvalidCheckDigitError, InvalidStructureError]
46
+ # @raise [InvalidFormatError, InvalidChecksumError, InvalidStructureError]
47
47
  def validate!(id)
48
48
  new(id).validate!
49
49
  end
@@ -82,7 +82,7 @@ module SecID
82
82
  # Validates and returns self if valid, raises an exception otherwise.
83
83
  #
84
84
  # @return [self]
85
- # @raise [InvalidFormatError, InvalidCheckDigitError, InvalidStructureError]
85
+ # @raise [InvalidFormatError, InvalidChecksumError, InvalidStructureError]
86
86
  def validate!
87
87
  return self if valid?
88
88
 
@@ -119,7 +119,7 @@ module SecID
119
119
  end
120
120
 
121
121
  # Checks length against ID_LENGTH's three shapes: Range (bounds), Array
122
- # (discrete valid lengths), or Integer (allows an optional check digit).
122
+ # (discrete valid lengths), or Integer (allows an optional checksum).
123
123
  #
124
124
  # @return [Boolean]
125
125
  def valid_length?
@@ -130,7 +130,7 @@ module SecID
130
130
  case id_length
131
131
  when Range then id_length.cover?(len)
132
132
  when Array then id_length.include?(len)
133
- else ((id_length - check_digit_width)..id_length).cover?(len)
133
+ else ((id_length - checksum_width)..id_length).cover?(len)
134
134
  end
135
135
  end
136
136
 
@@ -139,8 +139,8 @@ module SecID
139
139
  full_id.match?(self.class::VALID_CHARS_REGEX)
140
140
  end
141
141
 
142
- # @return [Integer] width of the check digit (0 for non-checkable, overridden in Checkable)
143
- def check_digit_width
142
+ # @return [Integer] width of the checksum (0 for non-checkable, overridden in Checkable)
143
+ def checksum_width
144
144
  0
145
145
  end
146
146
 
data/lib/sec_id/cusip.rb CHANGED
@@ -4,7 +4,7 @@ module SecID
4
4
  # Committee on Uniform Securities Identification Procedures (CUSIP) - a 9-character
5
5
  # alphanumeric code that identifies North American securities.
6
6
  #
7
- # Format: 6-character issuer code (CUSIP-6) + 2-character issue number + 1-digit check digit
7
+ # Format: 6-character issuer code (CUSIP-6) + 2-character issue number + 1-digit checksum
8
8
  #
9
9
  # @see https://en.wikipedia.org/wiki/CUSIP
10
10
  #
@@ -31,7 +31,7 @@ module SecID
31
31
  (?<identifier>
32
32
  (?<cusip6>[A-Z0-9]{5}[A-Z0-9*@#])
33
33
  (?<issue>[A-Z0-9*@#]{2}))
34
- (?<check_digit>\d)?
34
+ (?<checksum>\d)?
35
35
  \z/x
36
36
 
37
37
  # @return [String, nil] the 6-character issuer code
@@ -46,19 +46,19 @@ module SecID
46
46
  @identifier = cusip_parts[:identifier]
47
47
  @cusip6 = cusip_parts[:cusip6]
48
48
  @issue = cusip_parts[:issue]
49
- @check_digit = cusip_parts[:check_digit]&.to_i
49
+ @checksum = cusip_parts[:checksum]&.to_i
50
50
  end
51
51
 
52
52
  # @return [String, nil]
53
53
  def to_pretty_s
54
54
  return nil unless valid?
55
55
 
56
- "#{cusip6} #{issue} #{check_digit}"
56
+ "#{cusip6} #{issue} #{checksum}"
57
57
  end
58
58
 
59
- # @return [Integer] the calculated check digit (0-9)
59
+ # @return [Integer] the calculated checksum (0-9)
60
60
  # @raise [InvalidFormatError] if the CUSIP format is invalid
61
- def calculate_check_digit
61
+ def calculate_checksum
62
62
  validate_format_for_calculation!
63
63
  mod10(luhn_sum_double_add_double(reversed_digits_single(identifier)))
64
64
  end
@@ -66,7 +66,7 @@ module SecID
66
66
  # Generates a random CUSIP body: 8 alphanumeric characters.
67
67
  #
68
68
  # @param random [Random] source of randomness
69
- # @return [String] an 8-character CUSIP body without check digit
69
+ # @return [String] an 8-character CUSIP body without checksum
70
70
  def self.generate_body(random)
71
71
  random_string(ALPHANUMERIC, 8, random: random)
72
72
  end
@@ -91,6 +91,6 @@ module SecID
91
91
  private
92
92
 
93
93
  # @return [Hash]
94
- def components = { cusip6:, issue:, check_digit: }
94
+ def components = { cusip6:, issue:, checksum: }
95
95
  end
96
96
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SecID
4
+ # Emits standardized, silenceable deprecation warnings for the v7 → v8 rename
5
+ # bridge. Stateless: warns on every call with no per-call-site dedup, so the
6
+ # migration signal is never suppressed by default.
7
+ #
8
+ # The warning is written via {Kernel#warn} at Ruby's default verbosity — it is
9
+ # visible unless the process runs with `-W0` / `$VERBOSE = nil`. No `category:`
10
+ # is passed, because `Warning[:deprecated]` defaults to `false` and would hide
11
+ # the bridge warning the rename relies on.
12
+ module Deprecation
13
+ # Emits a standardized deprecation warning for a renamed API name.
14
+ #
15
+ # @param old [String] the deprecated name
16
+ # @param new [String] the canonical replacement name
17
+ # @param removed_in [String] the version that removes the deprecated name
18
+ # @return [void]
19
+ def self.warn(old:, new:, removed_in: 'v8')
20
+ Kernel.warn(
21
+ "SecID: `#{old}` is deprecated and will be removed in #{removed_in}; use `#{new}` instead.",
22
+ uplevel: 2
23
+ )
24
+ end
25
+ end
26
+ end