sec_id 4.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +67 -21
- data/README.md +286 -99
- data/lib/sec_id/base.rb +64 -67
- data/lib/sec_id/cei.rb +51 -0
- data/lib/sec_id/cfi.rb +314 -0
- data/lib/sec_id/cik.rb +29 -33
- data/lib/sec_id/concerns/checkable.rb +201 -0
- data/lib/sec_id/concerns/normalizable.rb +35 -0
- data/lib/sec_id/cusip.rb +30 -22
- data/lib/sec_id/figi.rb +29 -20
- data/lib/sec_id/fisn.rb +52 -0
- data/lib/sec_id/iban/country_rules.rb +266 -0
- data/lib/sec_id/iban.rb +159 -0
- data/lib/sec_id/isin.rb +116 -23
- data/lib/sec_id/lei.rb +69 -0
- data/lib/sec_id/occ.rb +106 -65
- data/lib/sec_id/sedol.rb +43 -6
- data/lib/sec_id/valoren.rb +74 -0
- data/lib/sec_id/version.rb +1 -1
- data/lib/sec_id/wkn.rb +41 -0
- data/lib/sec_id.rb +11 -2
- data/sec_id.gemspec +3 -1
- metadata +15 -4
|
@@ -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
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SecId
|
|
4
|
+
# Provides normalize! class method delegation for identifiers that support normalization.
|
|
5
|
+
# Include this module in classes that implement an instance-level normalize! method.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# class MyIdentifier < Base
|
|
9
|
+
# include Normalizable
|
|
10
|
+
#
|
|
11
|
+
# def normalize!
|
|
12
|
+
# # implementation
|
|
13
|
+
# end
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# MyIdentifier.normalize!('ABC123') #=> normalized string
|
|
17
|
+
module Normalizable
|
|
18
|
+
# @api private
|
|
19
|
+
def self.included(base)
|
|
20
|
+
base.extend(ClassMethods)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Class methods added when Normalizable is included.
|
|
24
|
+
module ClassMethods
|
|
25
|
+
# Normalizes the identifier to its canonical format.
|
|
26
|
+
#
|
|
27
|
+
# @param id [String, #to_s] the identifier to normalize
|
|
28
|
+
# @return [String] the normalized identifier
|
|
29
|
+
# @raise [InvalidFormatError] if the identifier format is invalid
|
|
30
|
+
def normalize!(id)
|
|
31
|
+
new(id).normalize!
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/sec_id/cusip.rb
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SecId
|
|
4
|
-
#
|
|
4
|
+
# Committee on Uniform Securities Identification Procedures (CUSIP) - a 9-character
|
|
5
|
+
# alphanumeric code that identifies North American securities.
|
|
6
|
+
#
|
|
7
|
+
# Format: 6-character issuer code (CUSIP-6) + 2-character issue number + 1-digit check digit
|
|
8
|
+
#
|
|
9
|
+
# @see https://en.wikipedia.org/wiki/CUSIP
|
|
10
|
+
#
|
|
11
|
+
# @example Validate a CUSIP
|
|
12
|
+
# SecId::CUSIP.valid?('037833100') #=> true
|
|
13
|
+
#
|
|
14
|
+
# @example Convert to ISIN
|
|
15
|
+
# cusip = SecId::CUSIP.new('037833100')
|
|
16
|
+
# cusip.to_isin('US') #=> #<SecId::ISIN>
|
|
5
17
|
class CUSIP < Base
|
|
18
|
+
include Checkable
|
|
19
|
+
|
|
20
|
+
# Regular expression for parsing CUSIP components.
|
|
6
21
|
ID_REGEX = /\A
|
|
7
22
|
(?<identifier>
|
|
8
23
|
(?<cusip6>[A-Z0-9]{5}[A-Z0-9*@#])
|
|
@@ -10,8 +25,13 @@ module SecId
|
|
|
10
25
|
(?<check_digit>\d)?
|
|
11
26
|
\z/x
|
|
12
27
|
|
|
13
|
-
|
|
28
|
+
# @return [String, nil] the 6-character issuer code
|
|
29
|
+
attr_reader :cusip6
|
|
30
|
+
|
|
31
|
+
# @return [String, nil] the 2-character issue number
|
|
32
|
+
attr_reader :issue
|
|
14
33
|
|
|
34
|
+
# @param cusip [String] the CUSIP string to parse
|
|
15
35
|
def initialize(cusip)
|
|
16
36
|
cusip_parts = parse cusip
|
|
17
37
|
@identifier = cusip_parts[:identifier]
|
|
@@ -20,14 +40,16 @@ module SecId
|
|
|
20
40
|
@check_digit = cusip_parts[:check_digit]&.to_i
|
|
21
41
|
end
|
|
22
42
|
|
|
43
|
+
# @return [Integer] the calculated check digit (0-9)
|
|
44
|
+
# @raise [InvalidFormatError] if the CUSIP format is invalid
|
|
23
45
|
def calculate_check_digit
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
mod10(modified_luhn_sum)
|
|
46
|
+
validate_format_for_calculation!
|
|
47
|
+
mod10(luhn_sum_double_add_double(reversed_digits_single(identifier)))
|
|
29
48
|
end
|
|
30
49
|
|
|
50
|
+
# @param country_code [String] the ISO 3166-1 alpha-2 country code (must be CGS country)
|
|
51
|
+
# @return [ISIN] a new ISIN instance
|
|
52
|
+
# @raise [InvalidFormatError] if the country code is not a CGS country
|
|
31
53
|
def to_isin(country_code)
|
|
32
54
|
unless ISIN::CGS_COUNTRY_CODES.include?(country_code)
|
|
33
55
|
raise(InvalidFormatError, "'#{country_code}' is not a CGS country code!")
|
|
@@ -39,23 +61,9 @@ module SecId
|
|
|
39
61
|
isin
|
|
40
62
|
end
|
|
41
63
|
|
|
42
|
-
#
|
|
64
|
+
# @return [Boolean] true if first character is a letter (CINS identifier)
|
|
43
65
|
def cins?
|
|
44
66
|
cusip6[0] < '0' || cusip6[0] > '9'
|
|
45
67
|
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
# https://en.wikipedia.org/wiki/Luhn_algorithm
|
|
50
|
-
def modified_luhn_sum
|
|
51
|
-
reversed_id_digits.each_slice(2).reduce(0) do |sum, (even, odd)|
|
|
52
|
-
double_even = (even || 0) * 2
|
|
53
|
-
sum + div10mod10(double_even) + div10mod10(odd || 0)
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def reversed_id_digits
|
|
58
|
-
identifier.each_char.map(&method(:char_to_digit)).reverse!
|
|
59
|
-
end
|
|
60
68
|
end
|
|
61
69
|
end
|
data/lib/sec_id/figi.rb
CHANGED
|
@@ -3,7 +3,24 @@
|
|
|
3
3
|
require 'set'
|
|
4
4
|
|
|
5
5
|
module SecId
|
|
6
|
+
# Financial Instrument Global Identifier (FIGI) - a 12-character alphanumeric code
|
|
7
|
+
# that uniquely identifies financial instruments.
|
|
8
|
+
#
|
|
9
|
+
# Format: 2-character prefix + 'G' + 8-character random part + 1-digit check digit
|
|
10
|
+
# Note: FIGI excludes vowels (A, E, I, O, U) from valid characters.
|
|
11
|
+
#
|
|
12
|
+
# @see https://en.wikipedia.org/wiki/Financial_Instrument_Global_Identifier
|
|
13
|
+
#
|
|
14
|
+
# @example Validate a FIGI
|
|
15
|
+
# SecId::FIGI.valid?('BBG000BLNQ16') #=> true
|
|
16
|
+
#
|
|
17
|
+
# @example Restore check digit
|
|
18
|
+
# SecId::FIGI.restore!('BBG000BLNQ1') #=> 'BBG000BLNQ16'
|
|
6
19
|
class FIGI < Base
|
|
20
|
+
include Checkable
|
|
21
|
+
|
|
22
|
+
# Regular expression for parsing FIGI components.
|
|
23
|
+
# The third character must be 'G'. Excludes vowels from valid characters.
|
|
7
24
|
ID_REGEX = /\A
|
|
8
25
|
(?<identifier>
|
|
9
26
|
(?<prefix>[B-DF-HJ-NP-TV-Z0-9]{2})
|
|
@@ -12,10 +29,16 @@ module SecId
|
|
|
12
29
|
(?<check_digit>\d)?
|
|
13
30
|
\z/x
|
|
14
31
|
|
|
32
|
+
# Country-code prefixes that are restricted from use in FIGI.
|
|
15
33
|
RESTRICTED_PREFIXES = Set.new %w[BS BM GG GB GH KY VG]
|
|
16
34
|
|
|
17
|
-
|
|
35
|
+
# @return [String, nil] the 2-character prefix
|
|
36
|
+
attr_reader :prefix
|
|
37
|
+
|
|
38
|
+
# @return [String, nil] the 8-character random part
|
|
39
|
+
attr_reader :random_part
|
|
18
40
|
|
|
41
|
+
# @param figi [String] the FIGI string to parse
|
|
19
42
|
def initialize(figi)
|
|
20
43
|
figi_parts = parse figi
|
|
21
44
|
@identifier = figi_parts[:identifier]
|
|
@@ -24,30 +47,16 @@ module SecId
|
|
|
24
47
|
@check_digit = figi_parts[:check_digit]&.to_i
|
|
25
48
|
end
|
|
26
49
|
|
|
50
|
+
# @return [Boolean]
|
|
27
51
|
def valid_format?
|
|
28
52
|
!identifier.nil? && !RESTRICTED_PREFIXES.include?(prefix)
|
|
29
53
|
end
|
|
30
54
|
|
|
55
|
+
# @return [Integer] the calculated check digit (0-9)
|
|
56
|
+
# @raise [InvalidFormatError] if the FIGI format is invalid
|
|
31
57
|
def calculate_check_digit
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
mod10(modified_luhn_sum)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
private
|
|
40
|
-
|
|
41
|
-
# https://en.wikipedia.org/wiki/Luhn_algorithm
|
|
42
|
-
def modified_luhn_sum
|
|
43
|
-
reversed_id_digits.each_with_index.reduce(0) do |sum, (digit, index)|
|
|
44
|
-
digit *= 2 if index.odd?
|
|
45
|
-
sum + digit.divmod(10).sum
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def reversed_id_digits
|
|
50
|
-
identifier.each_char.map(&method(:char_to_digit)).reverse!
|
|
58
|
+
validate_format_for_calculation!
|
|
59
|
+
mod10(luhn_sum_indexed(reversed_digits_single(identifier)))
|
|
51
60
|
end
|
|
52
61
|
end
|
|
53
62
|
end
|
data/lib/sec_id/fisn.rb
ADDED
|
@@ -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
|