sec_id 4.3.0 → 4.4.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.
data/lib/sec_id/cfi.rb ADDED
@@ -0,0 +1,314 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SecId
4
+ # Classification of Financial Instruments (CFI) - a 6-character alphabetic code
5
+ # that classifies financial instruments per ISO 10962.
6
+ #
7
+ # Format: 6 uppercase letters A-Z
8
+ # - Position 1: Category code (14 valid values)
9
+ # - Position 2: Group code (varies by category)
10
+ # - Positions 3-6: Attribute codes (A-Z, with X meaning "not applicable")
11
+ #
12
+ # @see https://en.wikipedia.org/wiki/ISO_10962
13
+ #
14
+ # @example Validate a CFI code
15
+ # SecId::CFI.valid?('ESXXXX') #=> true
16
+ # SecId::CFI.valid?('ESVUFR') #=> true
17
+ #
18
+ # @example Access CFI components
19
+ # cfi = SecId::CFI.new('ESVUFR')
20
+ # cfi.category #=> :equity
21
+ # cfi.group #=> :common_shares
22
+ # cfi.voting? #=> true
23
+ class CFI < Base
24
+ # Regular expression for parsing CFI components.
25
+ ID_REGEX = /\A
26
+ (?<identifier>
27
+ (?<category_code>[A-Z])
28
+ (?<group_code>[A-Z])
29
+ (?<attr1>[A-Z])
30
+ (?<attr2>[A-Z])
31
+ (?<attr3>[A-Z])
32
+ (?<attr4>[A-Z]))
33
+ \z/x
34
+
35
+ # Category codes per ISO 10962.
36
+ CATEGORIES = {
37
+ 'E' => :equity,
38
+ 'C' => :collective_investment_vehicles,
39
+ 'D' => :debt_instruments,
40
+ 'R' => :entitlements,
41
+ 'O' => :listed_options,
42
+ 'F' => :futures,
43
+ 'S' => :swaps,
44
+ 'H' => :non_listed_options,
45
+ 'I' => :spot,
46
+ 'J' => :forwards,
47
+ 'K' => :strategies,
48
+ 'L' => :financing,
49
+ 'T' => :referential_instruments,
50
+ 'M' => :miscellaneous
51
+ }.freeze
52
+
53
+ # Group codes per category per ISO 10962.
54
+ GROUPS = {
55
+ 'E' => { # Equity
56
+ 'S' => :common_shares,
57
+ 'P' => :preferred_shares,
58
+ 'C' => :convertible_common_shares,
59
+ 'F' => :convertible_preferred_shares,
60
+ 'L' => :limited_partnership_units,
61
+ 'D' => :depositary_receipts,
62
+ 'Y' => :structured_instruments,
63
+ 'M' => :miscellaneous
64
+ },
65
+ 'C' => { # Collective Investment Vehicles
66
+ 'I' => :standard_investment_funds,
67
+ 'H' => :hedge_funds,
68
+ 'B' => :real_estate_investment_trusts,
69
+ 'E' => :exchange_traded_funds,
70
+ 'S' => :pension_funds,
71
+ 'F' => :funds_of_funds,
72
+ 'P' => :private_equity_funds,
73
+ 'M' => :miscellaneous
74
+ },
75
+ 'D' => { # Debt Instruments
76
+ 'B' => :bonds,
77
+ 'C' => :convertible_bonds,
78
+ 'W' => :bonds_with_warrants,
79
+ 'T' => :medium_term_notes,
80
+ 'Y' => :money_market_instruments,
81
+ 'S' => :structured_instruments,
82
+ 'E' => :mortgage_backed_securities,
83
+ 'G' => :asset_backed_securities,
84
+ 'A' => :municipal_bonds,
85
+ 'N' => :municipal_notes,
86
+ 'D' => :depositary_receipts,
87
+ 'M' => :miscellaneous
88
+ },
89
+ 'R' => { # Entitlements (Rights)
90
+ 'A' => :allotment_rights,
91
+ 'S' => :subscription_rights,
92
+ 'P' => :purchase_rights,
93
+ 'W' => :warrants,
94
+ 'F' => :mini_future_certificates,
95
+ 'D' => :depositary_receipts,
96
+ 'M' => :miscellaneous
97
+ },
98
+ 'O' => { # Listed Options
99
+ 'C' => :call_options,
100
+ 'P' => :put_options,
101
+ 'M' => :miscellaneous
102
+ },
103
+ 'F' => { # Futures
104
+ 'F' => :financial_futures,
105
+ 'C' => :commodities_futures,
106
+ 'M' => :miscellaneous
107
+ },
108
+ 'S' => { # Swaps
109
+ 'R' => :rates,
110
+ 'T' => :commodities,
111
+ 'E' => :equity,
112
+ 'C' => :credit,
113
+ 'F' => :foreign_exchange,
114
+ 'M' => :miscellaneous
115
+ },
116
+ 'H' => { # Non-Listed (Complex) Options
117
+ 'C' => :call_options,
118
+ 'P' => :put_options,
119
+ 'M' => :miscellaneous
120
+ },
121
+ 'I' => { # Spot
122
+ 'F' => :foreign_exchange,
123
+ 'T' => :commodities,
124
+ 'M' => :miscellaneous
125
+ },
126
+ 'J' => { # Forwards
127
+ 'F' => :foreign_exchange,
128
+ 'R' => :rates,
129
+ 'T' => :commodities,
130
+ 'E' => :equity,
131
+ 'C' => :credit,
132
+ 'M' => :miscellaneous
133
+ },
134
+ 'K' => { # Strategies
135
+ 'R' => :rates,
136
+ 'T' => :commodities,
137
+ 'E' => :equity,
138
+ 'C' => :credit,
139
+ 'F' => :foreign_exchange,
140
+ 'Y' => :mixed,
141
+ 'M' => :miscellaneous
142
+ },
143
+ 'L' => { # Financing
144
+ 'S' => :loan_lease,
145
+ 'R' => :repurchase_agreements,
146
+ 'P' => :securities_lending,
147
+ 'M' => :miscellaneous
148
+ },
149
+ 'T' => { # Referential Instruments
150
+ 'I' => :currencies,
151
+ 'C' => :commodities,
152
+ 'R' => :interest_rates,
153
+ 'N' => :indices,
154
+ 'B' => :baskets,
155
+ 'D' => :stock_dividends,
156
+ 'M' => :miscellaneous
157
+ },
158
+ 'M' => { # Miscellaneous
159
+ 'C' => :combined_instruments,
160
+ 'M' => :miscellaneous
161
+ }
162
+ }.freeze
163
+
164
+ # @return [String, nil] the category code (position 1)
165
+ attr_reader :category_code
166
+
167
+ # @return [String, nil] the group code (position 2)
168
+ attr_reader :group_code
169
+
170
+ # @return [String, nil] attribute 1 (position 3)
171
+ attr_reader :attr1
172
+
173
+ # @return [String, nil] attribute 2 (position 4)
174
+ attr_reader :attr2
175
+
176
+ # @return [String, nil] attribute 3 (position 5)
177
+ attr_reader :attr3
178
+
179
+ # @return [String, nil] attribute 4 (position 6)
180
+ attr_reader :attr4
181
+
182
+ # @param cfi [String] the CFI string to parse
183
+ def initialize(cfi)
184
+ cfi_parts = parse(cfi)
185
+ @identifier = cfi_parts[:identifier]
186
+ @category_code = cfi_parts[:category_code]
187
+ @group_code = cfi_parts[:group_code]
188
+ @attr1 = cfi_parts[:attr1]
189
+ @attr2 = cfi_parts[:attr2]
190
+ @attr3 = cfi_parts[:attr3]
191
+ @attr4 = cfi_parts[:attr4]
192
+ end
193
+
194
+ # Validates format including category and group codes.
195
+ #
196
+ # @return [Boolean]
197
+ def valid_format?
198
+ super && valid_category? && valid_group?
199
+ end
200
+
201
+ # Returns the semantic category name.
202
+ #
203
+ # @return [Symbol, nil] category symbol or nil if invalid
204
+ def category
205
+ CATEGORIES[category_code]
206
+ end
207
+
208
+ # Returns the semantic group name.
209
+ #
210
+ # @return [Symbol, nil] group symbol or nil if invalid
211
+ def group
212
+ GROUPS.dig(category_code, group_code)
213
+ end
214
+
215
+ # @return [Boolean] true if category is equity
216
+ def equity?
217
+ category_code == 'E'
218
+ end
219
+
220
+ # Voting rights (position 3 = V). Only meaningful for equity.
221
+ #
222
+ # @return [Boolean]
223
+ def voting?
224
+ equity? && attr1 == 'V'
225
+ end
226
+
227
+ # Non-voting (position 3 = N). Only meaningful for equity.
228
+ #
229
+ # @return [Boolean]
230
+ def non_voting?
231
+ equity? && attr1 == 'N'
232
+ end
233
+
234
+ # Restricted voting (position 3 = R). Only meaningful for equity.
235
+ #
236
+ # @return [Boolean]
237
+ def restricted_voting?
238
+ equity? && attr1 == 'R'
239
+ end
240
+
241
+ # Enhanced voting (position 3 = E). Only meaningful for equity.
242
+ #
243
+ # @return [Boolean]
244
+ def enhanced_voting?
245
+ equity? && attr1 == 'E'
246
+ end
247
+
248
+ # Ownership restrictions exist (position 4 = T). Only meaningful for equity.
249
+ #
250
+ # @return [Boolean]
251
+ def restrictions?
252
+ equity? && attr2 == 'T'
253
+ end
254
+
255
+ # No ownership restrictions (position 4 = U). Only meaningful for equity.
256
+ #
257
+ # @return [Boolean]
258
+ def no_restrictions?
259
+ equity? && attr2 == 'U'
260
+ end
261
+
262
+ # Fully paid shares (position 5 = F). Only meaningful for equity.
263
+ #
264
+ # @return [Boolean]
265
+ def fully_paid?
266
+ equity? && attr3 == 'F'
267
+ end
268
+
269
+ # Nil paid shares (position 5 = O). Only meaningful for equity.
270
+ #
271
+ # @return [Boolean]
272
+ def nil_paid?
273
+ equity? && attr3 == 'O'
274
+ end
275
+
276
+ # Partly paid shares (position 5 = P). Only meaningful for equity.
277
+ #
278
+ # @return [Boolean]
279
+ def partly_paid?
280
+ equity? && attr3 == 'P'
281
+ end
282
+
283
+ # Bearer form (position 6 = B). Only meaningful for equity.
284
+ #
285
+ # @return [Boolean]
286
+ def bearer?
287
+ equity? && attr4 == 'B'
288
+ end
289
+
290
+ # Registered form (position 6 = R). Only meaningful for equity.
291
+ #
292
+ # @return [Boolean]
293
+ def registered?
294
+ equity? && attr4 == 'R'
295
+ end
296
+
297
+ # @return [String]
298
+ def to_s
299
+ identifier.to_s
300
+ end
301
+
302
+ private
303
+
304
+ # @return [Boolean]
305
+ def valid_category?
306
+ CATEGORIES.key?(category_code)
307
+ end
308
+
309
+ # @return [Boolean]
310
+ def valid_group?
311
+ GROUPS.dig(category_code, group_code) != nil
312
+ end
313
+ end
314
+ end
data/lib/sec_id/cik.rb CHANGED
@@ -31,12 +31,6 @@ module SecId
31
31
  cik_parts = parse(cik)
32
32
  @padding = cik_parts[:padding]
33
33
  @identifier = cik_parts[:identifier]
34
- @check_digit = nil
35
- end
36
-
37
- # @return [Boolean] always false
38
- def has_check_digit?
39
- false
40
34
  end
41
35
 
42
36
  # Normalizes the CIK to a 10-digit zero-padded format.
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
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.
6
+ #
7
+ # Including classes must implement:
8
+ # - `calculate_check_digit` method that returns the calculated check digit value
9
+ #
10
+ # This module provides:
11
+ # - Character-to-digit mapping constants
12
+ # - Luhn algorithm variants for check-digit calculation
13
+ # - `valid?` override that validates the check digit
14
+ # - `restore!` method to calculate and set the check digit
15
+ # - `check_digit` attribute
16
+ # - Class-level convenience methods: `restore!`, `check_digit`
17
+ #
18
+ # @example Including in an identifier class
19
+ # class MyIdentifier < Base
20
+ # include Checkable
21
+ #
22
+ # def calculate_check_digit
23
+ # validate_format_for_calculation!
24
+ # mod10(luhn_sum_standard(reversed_digits_multi(identifier)))
25
+ # end
26
+ # end
27
+ #
28
+ # @see https://en.wikipedia.org/wiki/Luhn_algorithm
29
+ module Checkable
30
+ # Character-to-digit mapping for Luhn algorithm variants.
31
+ # Maps alphanumeric characters to digit arrays for multi-digit expansion.
32
+ # Used by ISIN for check-digit calculation.
33
+ CHAR_TO_DIGITS = {
34
+ '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4,
35
+ '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9,
36
+ 'A' => [1, 0], 'B' => [1, 1], 'C' => [1, 2], 'D' => [1, 3], 'E' => [1, 4],
37
+ 'F' => [1, 5], 'G' => [1, 6], 'H' => [1, 7], 'I' => [1, 8], 'J' => [1, 9],
38
+ 'K' => [2, 0], 'L' => [2, 1], 'M' => [2, 2], 'N' => [2, 3], 'O' => [2, 4],
39
+ 'P' => [2, 5], 'Q' => [2, 6], 'R' => [2, 7], 'S' => [2, 8], 'T' => [2, 9],
40
+ 'U' => [3, 0], 'V' => [3, 1], 'W' => [3, 2], 'X' => [3, 3], 'Y' => [3, 4], 'Z' => [3, 5],
41
+ '*' => [3, 6], '@' => [3, 7], '#' => [3, 8]
42
+ }.freeze
43
+
44
+ # Character-to-digit mapping for single-digit conversion.
45
+ # Maps alphanumeric characters to values 0-38 (A=10, B=11, ..., Z=35, *=36, @=37, #=38).
46
+ # Used by CUSIP, FIGI, SEDOL, LEI, and IBAN for check-digit calculations.
47
+ CHAR_TO_DIGIT = {
48
+ '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4,
49
+ '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9,
50
+ 'A' => 10, 'B' => 11, 'C' => 12, 'D' => 13, 'E' => 14,
51
+ 'F' => 15, 'G' => 16, 'H' => 17, 'I' => 18, 'J' => 19,
52
+ 'K' => 20, 'L' => 21, 'M' => 22, 'N' => 23, 'O' => 24,
53
+ 'P' => 25, 'Q' => 26, 'R' => 27, 'S' => 28, 'T' => 29,
54
+ 'U' => 30, 'V' => 31, 'W' => 32, 'X' => 33, 'Y' => 34, 'Z' => 35,
55
+ '*' => 36, '@' => 37, '#' => 38
56
+ }.freeze
57
+
58
+ # @api private
59
+ def self.included(base)
60
+ base.attr_reader :check_digit
61
+ base.extend(ClassMethods)
62
+ end
63
+
64
+ # Class methods added when Checkable is included.
65
+ module ClassMethods
66
+ # Restores (calculates) the check digit and returns the full identifier.
67
+ #
68
+ # @param id_without_check_digit [String] identifier without or with incorrect check digit
69
+ # @return [String] the full identifier with correct check digit
70
+ # @raise [InvalidFormatError] if the identifier format is invalid
71
+ def restore!(id_without_check_digit)
72
+ new(id_without_check_digit).restore!
73
+ end
74
+
75
+ # @param id [String] the identifier to calculate check digit for
76
+ # @return [Integer] the calculated check digit
77
+ # @raise [InvalidFormatError] if the identifier format is invalid
78
+ def check_digit(id)
79
+ new(id).calculate_check_digit
80
+ end
81
+ end
82
+
83
+ # Validates format and check digit.
84
+ #
85
+ # @return [Boolean]
86
+ def valid?
87
+ return false unless valid_format?
88
+
89
+ check_digit == calculate_check_digit
90
+ end
91
+
92
+ # Calculates and sets the check digit, updating full_number.
93
+ #
94
+ # @return [String] the full identifier with correct check digit
95
+ # @raise [InvalidFormatError] if the identifier format is invalid
96
+ def restore!
97
+ @check_digit = calculate_check_digit
98
+ @full_number = to_s
99
+ end
100
+
101
+ # Subclasses must override this method to implement their check-digit algorithm.
102
+ #
103
+ # @return [Integer] the calculated check digit
104
+ # @raise [NotImplementedError] if subclass doesn't implement
105
+ # @raise [InvalidFormatError] if the identifier format is invalid
106
+ def calculate_check_digit
107
+ raise NotImplementedError
108
+ end
109
+
110
+ # @return [String]
111
+ def to_s
112
+ "#{identifier}#{check_digit}"
113
+ end
114
+ alias to_str to_s
115
+
116
+ # CUSIP/CEI style: "Double Add Double" algorithm.
117
+ # Processes pairs of digits, doubling the first (even-positioned from right),
118
+ # then summing both digit's div10mod10 values.
119
+ #
120
+ # @param digits [Array<Integer>] reversed array of digit values
121
+ # @return [Integer] the Luhn sum
122
+ def luhn_sum_double_add_double(digits)
123
+ digits.each_slice(2).reduce(0) do |sum, (even, odd)|
124
+ double_even = (even || 0) * 2
125
+ sum + div10mod10(double_even) + div10mod10(odd || 0)
126
+ end
127
+ end
128
+
129
+ # FIGI style: index-based doubling algorithm.
130
+ # Doubles odd-indexed digits (from right), then sums div10mod10 values.
131
+ #
132
+ # @param digits [Array<Integer>] reversed array of digit values
133
+ # @return [Integer] the Luhn sum
134
+ def luhn_sum_indexed(digits)
135
+ digits.each_with_index.reduce(0) do |sum, (digit, index)|
136
+ digit *= 2 if index.odd?
137
+ sum + div10mod10(digit)
138
+ end
139
+ end
140
+
141
+ # ISIN style: standard Luhn with subtract-9 for values > 9.
142
+ # Processes pairs of digits, doubling the first (even-positioned from right),
143
+ # subtracting 9 if result > 9.
144
+ #
145
+ # @param digits [Array<Integer>] reversed array of digit values
146
+ # @return [Integer] the Luhn sum
147
+ def luhn_sum_standard(digits)
148
+ digits.each_slice(2).reduce(0) do |sum, (even, odd)|
149
+ double_even = (even || 0) * 2
150
+ double_even -= 9 if double_even > 9
151
+ sum + double_even + (odd || 0)
152
+ end
153
+ end
154
+
155
+ # Converts identifier characters to reversed digit array using single-digit mapping.
156
+ # Used by CUSIP, CEI, FIGI, and SEDOL.
157
+ #
158
+ # @param id [String] the identifier string
159
+ # @return [Array<Integer>] reversed array of digit values
160
+ def reversed_digits_single(id)
161
+ id.each_char.map { |c| CHAR_TO_DIGIT.fetch(c) }.reverse!
162
+ end
163
+
164
+ # Converts identifier characters to reversed digit array using multi-digit mapping.
165
+ # Used by ISIN where letters expand to two digits.
166
+ #
167
+ # @param id [String] the identifier string
168
+ # @return [Array<Integer>] reversed array of digit values
169
+ def reversed_digits_multi(id)
170
+ id.each_char.flat_map { |c| CHAR_TO_DIGITS.fetch(c) }.reverse!
171
+ end
172
+
173
+ private
174
+
175
+ # @raise [InvalidFormatError] if valid_format? returns false
176
+ # @return [void]
177
+ def validate_format_for_calculation!
178
+ return if valid_format?
179
+
180
+ raise InvalidFormatError, "#{self.class.name} '#{full_number}' is invalid and check-digit cannot be calculated!"
181
+ end
182
+
183
+ # @param sum [Integer] the sum to calculate check digit from
184
+ # @return [Integer] check digit (0-9)
185
+ def mod10(sum)
186
+ (10 - (sum % 10)) % 10
187
+ end
188
+
189
+ # @param number [Integer] number to split
190
+ # @return [Integer] sum of tens and units digits
191
+ def div10mod10(number)
192
+ (number / 10) + (number % 10)
193
+ end
194
+
195
+ # @param numeric_string [String] numeric string representation
196
+ # @return [Integer] check digit value (1-98)
197
+ def mod97(numeric_string)
198
+ 98 - (numeric_string.to_i % 97)
199
+ end
200
+ end
201
+ end
data/lib/sec_id/cusip.rb CHANGED
@@ -15,6 +15,8 @@ module SecId
15
15
  # cusip = SecId::CUSIP.new('037833100')
16
16
  # cusip.to_isin('US') #=> #<SecId::ISIN>
17
17
  class CUSIP < Base
18
+ include Checkable
19
+
18
20
  # Regular expression for parsing CUSIP components.
19
21
  ID_REGEX = /\A
20
22
  (?<identifier>
@@ -42,7 +44,7 @@ module SecId
42
44
  # @raise [InvalidFormatError] if the CUSIP format is invalid
43
45
  def calculate_check_digit
44
46
  validate_format_for_calculation!
45
- mod10(modified_luhn_sum)
47
+ mod10(luhn_sum_double_add_double(reversed_digits_single(identifier)))
46
48
  end
47
49
 
48
50
  # @param country_code [String] the ISO 3166-1 alpha-2 country code (must be CGS country)
@@ -63,21 +65,5 @@ module SecId
63
65
  def cins?
64
66
  cusip6[0] < '0' || cusip6[0] > '9'
65
67
  end
66
-
67
- private
68
-
69
- # @return [Integer] the modified Luhn sum
70
- # @see https://en.wikipedia.org/wiki/Luhn_algorithm
71
- def modified_luhn_sum
72
- reversed_id_digits.each_slice(2).reduce(0) do |sum, (even, odd)|
73
- double_even = (even || 0) * 2
74
- sum + div10mod10(double_even) + div10mod10(odd || 0)
75
- end
76
- end
77
-
78
- # @return [Array<Integer>] the reversed digit array
79
- def reversed_id_digits
80
- identifier.each_char.map(&method(:char_to_digit)).reverse!
81
- end
82
68
  end
83
69
  end
data/lib/sec_id/figi.rb CHANGED
@@ -17,6 +17,8 @@ module SecId
17
17
  # @example Restore check digit
18
18
  # SecId::FIGI.restore!('BBG000BLNQ1') #=> 'BBG000BLNQ16'
19
19
  class FIGI < Base
20
+ include Checkable
21
+
20
22
  # Regular expression for parsing FIGI components.
21
23
  # The third character must be 'G'. Excludes vowels from valid characters.
22
24
  ID_REGEX = /\A
@@ -54,24 +56,7 @@ module SecId
54
56
  # @raise [InvalidFormatError] if the FIGI format is invalid
55
57
  def calculate_check_digit
56
58
  validate_format_for_calculation!
57
- mod10(modified_luhn_sum)
58
- end
59
-
60
- private
61
-
62
- # https://en.wikipedia.org/wiki/Luhn_algorithm
63
- #
64
- # @return [Integer] the modified Luhn sum
65
- def modified_luhn_sum
66
- reversed_id_digits.each_with_index.reduce(0) do |sum, (digit, index)|
67
- digit *= 2 if index.odd?
68
- sum + div10mod10(digit)
69
- end
70
- end
71
-
72
- # @return [Array<Integer>] the identifier digits in reverse order
73
- def reversed_id_digits
74
- identifier.each_char.map(&method(:char_to_digit)).reverse!
59
+ mod10(luhn_sum_indexed(reversed_digits_single(identifier)))
75
60
  end
76
61
  end
77
62
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SecId
4
+ # Financial Instrument Short Name (FISN) - a human-readable short name for financial
5
+ # instruments per ISO 18774.
6
+ #
7
+ # Format: Issuer Name/Abbreviated Instrument Description
8
+ # - Total length: 1-35 characters
9
+ # - Issuer: 1-15 characters (uppercase A-Z, digits 0-9, space)
10
+ # - Separator: forward slash (/)
11
+ # - Description: 1-19 characters (uppercase A-Z, digits 0-9, space)
12
+ #
13
+ # @see https://en.wikipedia.org/wiki/ISO_18774
14
+ #
15
+ # @example Validate a FISN
16
+ # SecId::FISN.valid?('APPLE INC/SH') #=> true
17
+ # SecId::FISN.valid?('apple inc/sh') #=> true (normalized to uppercase)
18
+ #
19
+ # @example Access FISN components
20
+ # fisn = SecId::FISN.new('APPLE INC/SH')
21
+ # fisn.issuer #=> 'APPLE INC'
22
+ # fisn.description #=> 'SH'
23
+ class FISN < Base
24
+ # Regular expression for parsing FISN components.
25
+ # Issuer: 1-15 chars, Description: 1-19 chars, Total: max 35 chars
26
+ ID_REGEX = %r{\A
27
+ (?<identifier>
28
+ (?<issuer>[A-Z0-9 ]{1,15})
29
+ /
30
+ (?<description>[A-Z0-9 ]{1,19}))
31
+ \z}x
32
+
33
+ # @return [String, nil] the issuer name portion (before the slash)
34
+ attr_reader :issuer
35
+
36
+ # @return [String, nil] the abbreviated instrument description (after the slash)
37
+ attr_reader :description
38
+
39
+ # @param fisn [String] the FISN string to parse
40
+ def initialize(fisn)
41
+ fisn_parts = parse(fisn)
42
+ @identifier = fisn_parts[:identifier]
43
+ @issuer = fisn_parts[:issuer]
44
+ @description = fisn_parts[:description]
45
+ end
46
+
47
+ # @return [String]
48
+ def to_s
49
+ identifier.to_s
50
+ end
51
+ end
52
+ end
data/lib/sec_id/iban.rb CHANGED
@@ -18,6 +18,7 @@ module SecId
18
18
  # @example Restore check digits
19
19
  # SecId::IBAN.restore!('DE00370400440532013000') #=> 'DE89370400440532013000'
20
20
  class IBAN < Base
21
+ include Checkable
21
22
  include IBANCountryRules
22
23
 
23
24
  # Regular expression for parsing IBAN components.
@@ -152,7 +153,7 @@ module SecId
152
153
 
153
154
  # @return [String] the numeric string representation
154
155
  def numeric_string_for_check
155
- "#{bban}#{country_code}00".each_char.map { |char| char_to_digit(char) }.join
156
+ "#{bban}#{country_code}00".each_char.map { |char| CHAR_TO_DIGIT.fetch(char) }.join
156
157
  end
157
158
  end
158
159
  end