sec_id 4.4.1 → 5.1.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 +59 -3
- data/MIGRATION.md +257 -0
- data/README.md +286 -148
- data/lib/sec_id/base.rb +43 -29
- data/lib/sec_id/cei.rb +14 -2
- data/lib/sec_id/cfi.rb +41 -12
- data/lib/sec_id/cik.rb +21 -18
- data/lib/sec_id/concerns/checkable.rb +56 -14
- data/lib/sec_id/concerns/identifier_metadata.rb +56 -0
- data/lib/sec_id/concerns/normalizable.rb +60 -16
- data/lib/sec_id/concerns/validatable.rb +158 -0
- data/lib/sec_id/cusip.rb +24 -8
- data/lib/sec_id/detector.rb +156 -0
- data/lib/sec_id/errors.rb +67 -0
- data/lib/sec_id/figi.rb +38 -8
- data/lib/sec_id/fisn.rb +15 -4
- data/lib/sec_id/iban/country_rules.rb +4 -2
- data/lib/sec_id/iban.rb +59 -12
- data/lib/sec_id/isin.rb +25 -6
- data/lib/sec_id/lei.rb +22 -9
- data/lib/sec_id/occ.rb +50 -25
- data/lib/sec_id/sedol.rb +12 -7
- data/lib/sec_id/valoren.rb +29 -22
- data/lib/sec_id/version.rb +2 -2
- data/lib/sec_id/wkn.rb +10 -7
- data/lib/sec_id.rb +127 -6
- data/sec_id.gemspec +6 -3
- metadata +11 -3
data/lib/sec_id/isin.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module SecID
|
|
4
4
|
# International Securities Identification Number (ISIN) - a 12-character alphanumeric code
|
|
5
5
|
# that uniquely identifies a security globally.
|
|
6
6
|
#
|
|
@@ -9,13 +9,18 @@ module SecId
|
|
|
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
14
|
# @example Restore check digit
|
|
15
|
-
#
|
|
15
|
+
# SecID::ISIN.restore!('US594918104') #=> #<SecID::ISIN>
|
|
16
16
|
class ISIN < Base
|
|
17
17
|
include Checkable
|
|
18
18
|
|
|
19
|
+
FULL_NAME = 'International Securities Identification Number'
|
|
20
|
+
ID_LENGTH = 12
|
|
21
|
+
EXAMPLE = 'US5949181045'
|
|
22
|
+
VALID_CHARS_REGEX = /\A[A-Z0-9]+\z/
|
|
23
|
+
|
|
19
24
|
# Regular expression for parsing ISIN components.
|
|
20
25
|
ID_REGEX = /\A
|
|
21
26
|
(?<identifier>
|
|
@@ -71,6 +76,13 @@ module SecId
|
|
|
71
76
|
@check_digit = isin_parts[:check_digit]&.to_i
|
|
72
77
|
end
|
|
73
78
|
|
|
79
|
+
# @return [String, nil]
|
|
80
|
+
def to_pretty_s
|
|
81
|
+
return nil unless valid?
|
|
82
|
+
|
|
83
|
+
"#{country_code} #{nsin} #{check_digit}"
|
|
84
|
+
end
|
|
85
|
+
|
|
74
86
|
# @return [Integer] the calculated check digit (0-9)
|
|
75
87
|
# @raise [InvalidFormatError] if the ISIN format is invalid
|
|
76
88
|
def calculate_check_digit
|
|
@@ -130,6 +142,13 @@ module SecId
|
|
|
130
142
|
Valoren.new(nsin)
|
|
131
143
|
end
|
|
132
144
|
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
# @return [Hash]
|
|
148
|
+
def components = { country_code:, nsin:, check_digit: }
|
|
149
|
+
|
|
150
|
+
public
|
|
151
|
+
|
|
133
152
|
# Returns the type of NSIN embedded in this ISIN.
|
|
134
153
|
#
|
|
135
154
|
# @return [Symbol] :cusip, :sedol, :wkn, :valoren, or :generic
|
|
@@ -148,9 +167,9 @@ module SecId
|
|
|
148
167
|
|
|
149
168
|
case nsin_type
|
|
150
169
|
when :cusip then to_cusip
|
|
151
|
-
when :sedol then
|
|
152
|
-
when :wkn then
|
|
153
|
-
when :valoren then
|
|
170
|
+
when :sedol then to_sedol
|
|
171
|
+
when :wkn then to_wkn
|
|
172
|
+
when :valoren then to_valoren
|
|
154
173
|
else nsin # :generic - return raw string
|
|
155
174
|
end
|
|
156
175
|
end
|
data/lib/sec_id/lei.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
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
|
#
|
|
@@ -10,13 +10,18 @@ module SecId
|
|
|
10
10
|
# @see https://www.gleif.org/en/about-lei/iso-17442-the-lei-code-structure
|
|
11
11
|
#
|
|
12
12
|
# @example Validate a LEI
|
|
13
|
-
#
|
|
13
|
+
# SecID::LEI.valid?('529900T8BM49AURSDO55') #=> true
|
|
14
14
|
#
|
|
15
15
|
# @example Calculate check digit
|
|
16
|
-
#
|
|
16
|
+
# SecID::LEI.check_digit('529900T8BM49AURSDO') #=> 55
|
|
17
17
|
class LEI < Base
|
|
18
18
|
include Checkable
|
|
19
19
|
|
|
20
|
+
FULL_NAME = 'Legal Entity Identifier'
|
|
21
|
+
ID_LENGTH = 20
|
|
22
|
+
EXAMPLE = '7LTWFZYICNSX8D621K86'
|
|
23
|
+
VALID_CHARS_REGEX = /\A[0-9A-Z]+\z/
|
|
24
|
+
|
|
20
25
|
# Regular expression for parsing LEI components.
|
|
21
26
|
ID_REGEX = /\A
|
|
22
27
|
(?<identifier>
|
|
@@ -45,6 +50,13 @@ module SecId
|
|
|
45
50
|
@check_digit = lei_parts[:check_digit]&.to_i
|
|
46
51
|
end
|
|
47
52
|
|
|
53
|
+
# @return [String, nil]
|
|
54
|
+
def to_pretty_s
|
|
55
|
+
return nil unless valid?
|
|
56
|
+
|
|
57
|
+
to_s.scan(/.{1,4}/).join(' ')
|
|
58
|
+
end
|
|
59
|
+
|
|
48
60
|
# @return [Integer] the calculated 2-digit check digit (1-98)
|
|
49
61
|
# @raise [InvalidFormatError] if the LEI format is invalid
|
|
50
62
|
def calculate_check_digit
|
|
@@ -52,14 +64,15 @@ module SecId
|
|
|
52
64
|
mod97("#{numeric_identifier}00")
|
|
53
65
|
end
|
|
54
66
|
|
|
55
|
-
|
|
56
|
-
def to_s
|
|
57
|
-
return full_number unless check_digit
|
|
67
|
+
private
|
|
58
68
|
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
# @return [Hash]
|
|
70
|
+
def components = { lou_id:, reserved:, entity_id:, check_digit: }
|
|
61
71
|
|
|
62
|
-
|
|
72
|
+
# @return [Integer]
|
|
73
|
+
def check_digit_width
|
|
74
|
+
2
|
|
75
|
+
end
|
|
63
76
|
|
|
64
77
|
# @return [String] the numeric string representation
|
|
65
78
|
def numeric_identifier
|
data/lib/sec_id/occ.rb
CHANGED
|
@@ -2,24 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
require 'date'
|
|
4
4
|
|
|
5
|
-
module
|
|
5
|
+
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
|
|
10
|
-
# and
|
|
9
|
+
# @note OCC identifiers have no check digit and validation includes both format
|
|
10
|
+
# and date parseability checks.
|
|
11
11
|
#
|
|
12
12
|
# @see https://en.wikipedia.org/wiki/Option_symbol#The_OCC_Option_Symbol
|
|
13
13
|
# @see https://web.archive.org/web/20120507220143/http://www.theocc.com/components/docs/initiatives/symbology/symbology_initiative_v1_8.pdf
|
|
14
14
|
#
|
|
15
15
|
# @example Validate an OCC symbol
|
|
16
|
-
#
|
|
16
|
+
# SecID::OCC.valid?('AAPL 210917C00150000') #=> true
|
|
17
17
|
#
|
|
18
18
|
# @example Build an OCC symbol from components
|
|
19
|
-
# occ =
|
|
19
|
+
# occ = SecID::OCC.build(underlying: 'AAPL', date: '2021-09-17', type: 'C', strike: 150.0)
|
|
20
20
|
# occ.to_s #=> 'AAPL 210917C00150000'
|
|
21
21
|
class OCC < Base
|
|
22
|
-
|
|
22
|
+
FULL_NAME = 'OCC Option Symbol'
|
|
23
|
+
ID_LENGTH = (16..21)
|
|
24
|
+
EXAMPLE = 'AAPL 210917C00150000'
|
|
25
|
+
VALID_CHARS_REGEX = /\A[A-Z0-9 ]+\z/
|
|
26
|
+
SEPARATORS = /-/
|
|
23
27
|
|
|
24
28
|
# Regular expression for parsing OCC symbol components.
|
|
25
29
|
ID_REGEX = /\A
|
|
@@ -81,7 +85,7 @@ module SecId
|
|
|
81
85
|
case strike
|
|
82
86
|
when Numeric
|
|
83
87
|
format('%08d', (strike * 1000).to_i)
|
|
84
|
-
when
|
|
88
|
+
when /\A\d{8}\z/
|
|
85
89
|
strike
|
|
86
90
|
else
|
|
87
91
|
raise ArgumentError, 'Strike must be numeric or an 8-char string!'
|
|
@@ -91,7 +95,7 @@ module SecId
|
|
|
91
95
|
|
|
92
96
|
# @param symbol [String] the OCC symbol string to parse
|
|
93
97
|
def initialize(symbol)
|
|
94
|
-
symbol_parts = parse(symbol
|
|
98
|
+
symbol_parts = parse(symbol)
|
|
95
99
|
@identifier = symbol_parts[:initial]
|
|
96
100
|
@underlying = symbol_parts[:underlying]
|
|
97
101
|
@date_str = symbol_parts[:date]
|
|
@@ -99,14 +103,11 @@ module SecId
|
|
|
99
103
|
@strike_mills = symbol_parts[:strike_mills]
|
|
100
104
|
end
|
|
101
105
|
|
|
102
|
-
# Normalizes the OCC symbol to standard format with 6-char padded underlying and 8-digit strike.
|
|
103
|
-
#
|
|
104
106
|
# @return [String] the normalized OCC symbol
|
|
105
|
-
# @raise [InvalidFormatError]
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
@full_number = self.class.compose_symbol(underlying, date_str, type, strike_mills)
|
|
107
|
+
# @raise [InvalidFormatError, InvalidStructureError]
|
|
108
|
+
def normalized
|
|
109
|
+
validate!
|
|
110
|
+
self.class.compose_symbol(underlying, date_str, type, strike_mills)
|
|
110
111
|
end
|
|
111
112
|
|
|
112
113
|
# @return [Boolean]
|
|
@@ -116,29 +117,53 @@ module SecId
|
|
|
116
117
|
|
|
117
118
|
# @return [Date, nil] the parsed date or nil if invalid
|
|
118
119
|
def date
|
|
119
|
-
return
|
|
120
|
+
return @date if defined?(@date)
|
|
121
|
+
return unless date_str
|
|
120
122
|
|
|
121
|
-
@date
|
|
123
|
+
@date = Date.strptime(date_str, '%y%m%d')
|
|
122
124
|
rescue ArgumentError
|
|
123
|
-
nil
|
|
125
|
+
@date = nil
|
|
124
126
|
end
|
|
125
127
|
alias date_obj date
|
|
126
128
|
|
|
127
|
-
# @return [Float] strike price in dollars
|
|
129
|
+
# @return [Float, nil] strike price in dollars
|
|
128
130
|
def strike
|
|
129
|
-
@strike
|
|
131
|
+
return @strike if defined?(@strike)
|
|
132
|
+
|
|
133
|
+
@strike = strike_mills&.then { |m| m.to_i / 1000.0 }
|
|
130
134
|
end
|
|
131
135
|
|
|
132
136
|
# @return [String]
|
|
133
137
|
def to_s
|
|
134
|
-
|
|
138
|
+
full_id
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @return [String, nil]
|
|
142
|
+
def to_pretty_s
|
|
143
|
+
return nil unless valid?
|
|
144
|
+
|
|
145
|
+
"#{underlying} #{date_str} #{type} #{strike_mills}"
|
|
135
146
|
end
|
|
136
|
-
alias to_str to_s
|
|
137
147
|
|
|
138
|
-
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
# @return [Hash]
|
|
151
|
+
def components = { underlying:, date_str:, type:, strike_mills: }
|
|
152
|
+
|
|
153
|
+
# @return [Array<Symbol>]
|
|
154
|
+
def error_codes
|
|
155
|
+
return detect_errors unless valid_format?
|
|
156
|
+
return [:invalid_date] if date.nil?
|
|
157
|
+
|
|
158
|
+
[]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# @param code [Symbol]
|
|
139
162
|
# @return [String]
|
|
140
|
-
def
|
|
141
|
-
|
|
163
|
+
def validation_message(code)
|
|
164
|
+
return "Date '#{date_str}' cannot be parsed" if code == :invalid_date
|
|
165
|
+
|
|
166
|
+
super
|
|
142
167
|
end
|
|
143
168
|
end
|
|
144
169
|
end
|
data/lib/sec_id/sedol.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
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
|
#
|
|
@@ -10,13 +10,18 @@ module SecId
|
|
|
10
10
|
# @see https://en.wikipedia.org/wiki/SEDOL
|
|
11
11
|
#
|
|
12
12
|
# @example Validate a SEDOL
|
|
13
|
-
#
|
|
13
|
+
# SecID::SEDOL.valid?('B19GKT4') #=> true
|
|
14
14
|
#
|
|
15
15
|
# @example Calculate check digit
|
|
16
|
-
#
|
|
16
|
+
# SecID::SEDOL.check_digit('B19GKT') #=> 4
|
|
17
17
|
class SEDOL < Base
|
|
18
18
|
include Checkable
|
|
19
19
|
|
|
20
|
+
FULL_NAME = 'Stock Exchange Daily Official List'
|
|
21
|
+
ID_LENGTH = 7
|
|
22
|
+
EXAMPLE = 'B0YBKJ7'
|
|
23
|
+
VALID_CHARS_REGEX = /\A[0-9BCDFGHJKLMNPQRSTVWXYZ]+\z/
|
|
24
|
+
|
|
20
25
|
# Regular expression for parsing SEDOL components.
|
|
21
26
|
# Excludes vowels (A, E, I, O, U) from valid characters.
|
|
22
27
|
ID_REGEX = /\A
|
|
@@ -45,10 +50,7 @@ module SecId
|
|
|
45
50
|
raise InvalidFormatError, "'#{country_code}' is not a valid SEDOL country code!"
|
|
46
51
|
end
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
isin = ISIN.new("#{country_code}00#{sedol_with_check_digit}")
|
|
50
|
-
isin.restore!
|
|
51
|
-
isin
|
|
53
|
+
ISIN.new("#{country_code}00#{restore}").restore!
|
|
52
54
|
end
|
|
53
55
|
|
|
54
56
|
# @return [Integer] the calculated check digit (0-9)
|
|
@@ -60,6 +62,9 @@ module SecId
|
|
|
60
62
|
|
|
61
63
|
private
|
|
62
64
|
|
|
65
|
+
# @return [Hash]
|
|
66
|
+
def components = { check_digit: }
|
|
67
|
+
|
|
63
68
|
# NOTE: Not idiomatic Ruby, but optimized for performance.
|
|
64
69
|
#
|
|
65
70
|
# @return [Integer] the weighted sum
|
data/lib/sec_id/valoren.rb
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module SecID
|
|
4
4
|
# Valoren (Swiss Security Number) - a numeric identifier for securities
|
|
5
5
|
# in Switzerland, Liechtenstein, and Belgium.
|
|
6
6
|
#
|
|
7
7
|
# Format: 5-9 numeric digits
|
|
8
8
|
#
|
|
9
|
-
# @note Valoren identifiers have no check digit
|
|
10
|
-
# returns false and validation is based solely on format.
|
|
9
|
+
# @note Valoren identifiers have no check digit and validation is based solely on format.
|
|
11
10
|
#
|
|
12
11
|
# @see https://en.wikipedia.org/wiki/Valoren_number
|
|
13
12
|
#
|
|
14
13
|
# @example Validate a Valoren
|
|
15
|
-
#
|
|
16
|
-
#
|
|
14
|
+
# SecID::Valoren.valid?('3886335') #=> true
|
|
15
|
+
# SecID::Valoren.valid?('003886335') #=> true
|
|
17
16
|
#
|
|
18
17
|
# @example Normalize a Valoren to 9 digits
|
|
19
|
-
#
|
|
18
|
+
# SecID::Valoren.normalize('3886335') #=> '003886335'
|
|
20
19
|
class Valoren < Base
|
|
21
|
-
|
|
20
|
+
FULL_NAME = 'Valoren Number'
|
|
21
|
+
ID_LENGTH = (5..9)
|
|
22
|
+
EXAMPLE = '3886335'
|
|
23
|
+
VALID_CHARS_REGEX = /\A[0-9]+\z/
|
|
22
24
|
|
|
23
25
|
# Regular expression for parsing Valoren components.
|
|
24
26
|
ID_REGEX = /\A
|
|
@@ -38,6 +40,13 @@ module SecId
|
|
|
38
40
|
@identifier = valoren_parts[:identifier]
|
|
39
41
|
end
|
|
40
42
|
|
|
43
|
+
# @return [String, nil]
|
|
44
|
+
def to_pretty_s
|
|
45
|
+
return nil unless valid?
|
|
46
|
+
|
|
47
|
+
identifier.reverse.scan(/.{1,3}/).join(' ').reverse
|
|
48
|
+
end
|
|
49
|
+
|
|
41
50
|
# @param country_code [String] the ISO 3166-1 alpha-2 country code (default: 'CH')
|
|
42
51
|
# @return [ISIN] a new ISIN instance with calculated check digit
|
|
43
52
|
# @raise [InvalidFormatError] if the country code is not CH or LI
|
|
@@ -46,29 +55,27 @@ module SecId
|
|
|
46
55
|
raise InvalidFormatError, "'#{country_code}' is not a valid Valoren country code!"
|
|
47
56
|
end
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
isin = ISIN.new(country_code + full_number)
|
|
51
|
-
isin.restore!
|
|
52
|
-
isin
|
|
58
|
+
ISIN.new(country_code + normalized).restore!
|
|
53
59
|
end
|
|
54
60
|
|
|
55
|
-
# Normalizes the Valoren to a 9-digit zero-padded format.
|
|
56
|
-
# Updates both @full_number and @padding to reflect the normalized state.
|
|
57
|
-
#
|
|
58
61
|
# @return [String] the normalized 9-digit Valoren
|
|
59
|
-
# @raise [InvalidFormatError]
|
|
60
|
-
def
|
|
61
|
-
|
|
62
|
+
# @raise [InvalidFormatError]
|
|
63
|
+
def normalized
|
|
64
|
+
validate!
|
|
65
|
+
@identifier.rjust(self.class::ID_LENGTH.max, '0')
|
|
66
|
+
end
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
# @return [self]
|
|
69
|
+
# @raise [InvalidFormatError]
|
|
70
|
+
def normalize!
|
|
71
|
+
super
|
|
72
|
+
@padding = @full_id[0, self.class::ID_LENGTH.max - @identifier.length]
|
|
73
|
+
self
|
|
66
74
|
end
|
|
67
75
|
|
|
68
76
|
# @return [String]
|
|
69
77
|
def to_s
|
|
70
|
-
|
|
78
|
+
full_id
|
|
71
79
|
end
|
|
72
|
-
alias to_str to_s
|
|
73
80
|
end
|
|
74
81
|
end
|
data/lib/sec_id/version.rb
CHANGED
data/lib/sec_id/wkn.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module SecID
|
|
4
4
|
# Wertpapierkennnummer (WKN) - a 6-character alphanumeric code
|
|
5
5
|
# used to identify securities in Germany.
|
|
6
6
|
#
|
|
@@ -10,9 +10,14 @@ module SecId
|
|
|
10
10
|
# @see https://en.wikipedia.org/wiki/Wertpapierkennnummer
|
|
11
11
|
#
|
|
12
12
|
# @example Validate a WKN
|
|
13
|
-
#
|
|
14
|
-
#
|
|
13
|
+
# SecID::WKN.valid?('514000') #=> true
|
|
14
|
+
# SecID::WKN.valid?('CBK100') #=> true
|
|
15
15
|
class WKN < Base
|
|
16
|
+
FULL_NAME = 'Wertpapierkennnummer'
|
|
17
|
+
ID_LENGTH = 6
|
|
18
|
+
EXAMPLE = '514000'
|
|
19
|
+
VALID_CHARS_REGEX = /\A[0-9A-HJ-NP-Z]+\z/
|
|
20
|
+
|
|
16
21
|
# Regular expression for parsing WKN components.
|
|
17
22
|
# Excludes letters I and O to avoid confusion with 1 and 0.
|
|
18
23
|
ID_REGEX = /\A
|
|
@@ -31,11 +36,9 @@ module SecId
|
|
|
31
36
|
# @raise [InvalidFormatError] if the WKN format is invalid
|
|
32
37
|
def to_isin(country_code = 'DE')
|
|
33
38
|
raise InvalidFormatError, "'#{country_code}' is not a valid WKN country code!" unless country_code == 'DE'
|
|
34
|
-
raise InvalidFormatError, "WKN '#{
|
|
39
|
+
raise InvalidFormatError, "WKN '#{full_id}' is invalid!" unless valid_format?
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
isin.restore!
|
|
38
|
-
isin
|
|
41
|
+
ISIN.new("#{country_code}000#{identifier}").restore!
|
|
39
42
|
end
|
|
40
43
|
end
|
|
41
44
|
end
|
data/lib/sec_id.rb
CHANGED
|
@@ -1,10 +1,136 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'set'
|
|
4
3
|
require 'sec_id/version'
|
|
4
|
+
|
|
5
|
+
module SecID
|
|
6
|
+
# Base error class for all SecID errors.
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
|
|
9
|
+
# Raised for invalid format, length, or characters.
|
|
10
|
+
class InvalidFormatError < Error; end
|
|
11
|
+
|
|
12
|
+
# Raised when the check digit does not match the calculated value.
|
|
13
|
+
class InvalidCheckDigitError < Error; end
|
|
14
|
+
|
|
15
|
+
# Raised for type-specific structural errors (invalid prefix, category, group, BBAN, or date).
|
|
16
|
+
class InvalidStructureError < Error; end
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
# Looks up an identifier class by its symbol key.
|
|
20
|
+
#
|
|
21
|
+
# @param key [Symbol] identifier type (e.g. :isin, :cusip)
|
|
22
|
+
# @return [Class] the identifier class
|
|
23
|
+
# @raise [ArgumentError] if key is unknown
|
|
24
|
+
def [](key)
|
|
25
|
+
identifier_map.fetch(key) do
|
|
26
|
+
raise ArgumentError, "Unknown identifier type: #{key.inspect}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns all registered identifier classes in load order.
|
|
31
|
+
#
|
|
32
|
+
# @return [Array<Class>]
|
|
33
|
+
def identifiers
|
|
34
|
+
identifier_list.dup
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Detects all identifier types that match the given string.
|
|
38
|
+
#
|
|
39
|
+
# @param str [String, nil] the identifier string to detect
|
|
40
|
+
# @return [Array<Symbol>] matching type symbols sorted by specificity
|
|
41
|
+
def detect(str)
|
|
42
|
+
detector.call(str)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Checks whether the string is a valid identifier.
|
|
46
|
+
#
|
|
47
|
+
# @param str [String, nil] the identifier string to validate
|
|
48
|
+
# @param types [Array<Symbol>, nil] restrict to specific types (e.g. [:isin, :cusip])
|
|
49
|
+
# @return [Boolean]
|
|
50
|
+
# @raise [ArgumentError] if any key in types is unknown
|
|
51
|
+
def valid?(str, types: nil)
|
|
52
|
+
return detect(str).any? if types.nil?
|
|
53
|
+
|
|
54
|
+
types.any? { |key| self[key].valid?(str) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Parses a string into the most specific matching identifier instance.
|
|
58
|
+
#
|
|
59
|
+
# @param str [String, nil] the identifier string to parse
|
|
60
|
+
# @param types [Array<Symbol>, nil] restrict to specific types (e.g. [:isin, :cusip])
|
|
61
|
+
# @return [SecID::Base, nil] a valid identifier instance, or nil if no match
|
|
62
|
+
# @raise [ArgumentError] if any key in types is unknown
|
|
63
|
+
def parse(str, types: nil)
|
|
64
|
+
types.nil? ? parse_any(str) : parse_from(str, types)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Parses a string into the most specific matching identifier instance, raising on failure.
|
|
68
|
+
#
|
|
69
|
+
# @param str [String, nil] the identifier string to parse
|
|
70
|
+
# @param types [Array<Symbol>, nil] restrict to specific types (e.g. [:isin, :cusip])
|
|
71
|
+
# @return [SecID::Base] a valid identifier instance
|
|
72
|
+
# @raise [InvalidFormatError] if no matching identifier type is found
|
|
73
|
+
# @raise [ArgumentError] if any key in types is unknown
|
|
74
|
+
def parse!(str, types: nil)
|
|
75
|
+
parse(str, types: types) || raise(InvalidFormatError, parse_error_message(str, types))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# @param klass [Class] the identifier class to register
|
|
81
|
+
# @return [void]
|
|
82
|
+
def register_identifier(klass)
|
|
83
|
+
key = klass.name.split('::').last.downcase.to_sym
|
|
84
|
+
identifier_map[key] = klass
|
|
85
|
+
identifier_list << klass
|
|
86
|
+
@detector = nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @return [SecID::Base, nil]
|
|
90
|
+
def parse_any(str)
|
|
91
|
+
key = detect(str).first
|
|
92
|
+
key && self[key].new(str)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @return [SecID::Base, nil]
|
|
96
|
+
def parse_from(str, types)
|
|
97
|
+
types.each do |key|
|
|
98
|
+
instance = self[key].new(str)
|
|
99
|
+
return instance if instance.valid?
|
|
100
|
+
end
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @return [String]
|
|
105
|
+
def parse_error_message(str, types)
|
|
106
|
+
base = "No matching identifier type found for #{str.to_s.strip.inspect}"
|
|
107
|
+
types ? "#{base} among #{types.inspect}" : base
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @return [Detector]
|
|
111
|
+
def detector
|
|
112
|
+
@detector ||= Detector.new(identifier_list)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @return [Hash{Symbol => Class}]
|
|
116
|
+
def identifier_map
|
|
117
|
+
@identifier_map ||= {}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# @return [Array<Class>]
|
|
121
|
+
def identifier_list
|
|
122
|
+
@identifier_list ||= []
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
require 'sec_id/errors'
|
|
128
|
+
require 'sec_id/concerns/identifier_metadata'
|
|
5
129
|
require 'sec_id/concerns/normalizable'
|
|
130
|
+
require 'sec_id/concerns/validatable'
|
|
6
131
|
require 'sec_id/concerns/checkable'
|
|
7
132
|
require 'sec_id/base'
|
|
133
|
+
require 'sec_id/detector'
|
|
8
134
|
require 'sec_id/isin'
|
|
9
135
|
require 'sec_id/cusip'
|
|
10
136
|
require 'sec_id/sedol'
|
|
@@ -18,8 +144,3 @@ require 'sec_id/valoren'
|
|
|
18
144
|
require 'sec_id/cei'
|
|
19
145
|
require 'sec_id/cfi'
|
|
20
146
|
require 'sec_id/fisn'
|
|
21
|
-
|
|
22
|
-
module SecId
|
|
23
|
-
class Error < StandardError; end
|
|
24
|
-
class InvalidFormatError < Error; end
|
|
25
|
-
end
|
data/sec_id.gemspec
CHANGED
|
@@ -6,7 +6,7 @@ require 'sec_id/version'
|
|
|
6
6
|
|
|
7
7
|
Gem::Specification.new do |spec|
|
|
8
8
|
spec.name = 'sec_id'
|
|
9
|
-
spec.version =
|
|
9
|
+
spec.version = SecID::VERSION
|
|
10
10
|
spec.authors = ['Leonid Svyatov']
|
|
11
11
|
spec.email = ['leonid@svyatov.ru']
|
|
12
12
|
|
|
@@ -17,10 +17,13 @@ Gem::Specification.new do |spec|
|
|
|
17
17
|
spec.homepage = 'https://github.com/svyatov/sec_id'
|
|
18
18
|
spec.license = 'MIT'
|
|
19
19
|
|
|
20
|
-
spec.required_ruby_version = '>= 3.
|
|
20
|
+
spec.required_ruby_version = '>= 3.2.0'
|
|
21
21
|
|
|
22
22
|
spec.require_paths = ['lib']
|
|
23
|
-
spec.files = Dir['lib/**/*.rb'] + %w[CHANGELOG.md LICENSE.txt README.md sec_id.gemspec]
|
|
23
|
+
spec.files = Dir['lib/**/*.rb'] + %w[CHANGELOG.md LICENSE.txt MIGRATION.md README.md sec_id.gemspec]
|
|
24
24
|
|
|
25
25
|
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
26
|
+
spec.metadata['source_code_uri'] = 'https://github.com/svyatov/sec_id'
|
|
27
|
+
spec.metadata['changelog_uri'] = 'https://github.com/svyatov/sec_id/blob/main/CHANGELOG.md'
|
|
28
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/svyatov/sec_id/issues'
|
|
26
29
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sec_id
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 5.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Leonid Svyatov
|
|
@@ -20,6 +20,7 @@ extra_rdoc_files: []
|
|
|
20
20
|
files:
|
|
21
21
|
- CHANGELOG.md
|
|
22
22
|
- LICENSE.txt
|
|
23
|
+
- MIGRATION.md
|
|
23
24
|
- README.md
|
|
24
25
|
- lib/sec_id.rb
|
|
25
26
|
- lib/sec_id/base.rb
|
|
@@ -27,8 +28,12 @@ files:
|
|
|
27
28
|
- lib/sec_id/cfi.rb
|
|
28
29
|
- lib/sec_id/cik.rb
|
|
29
30
|
- lib/sec_id/concerns/checkable.rb
|
|
31
|
+
- lib/sec_id/concerns/identifier_metadata.rb
|
|
30
32
|
- lib/sec_id/concerns/normalizable.rb
|
|
33
|
+
- lib/sec_id/concerns/validatable.rb
|
|
31
34
|
- lib/sec_id/cusip.rb
|
|
35
|
+
- lib/sec_id/detector.rb
|
|
36
|
+
- lib/sec_id/errors.rb
|
|
32
37
|
- lib/sec_id/figi.rb
|
|
33
38
|
- lib/sec_id/fisn.rb
|
|
34
39
|
- lib/sec_id/iban.rb
|
|
@@ -46,6 +51,9 @@ licenses:
|
|
|
46
51
|
- MIT
|
|
47
52
|
metadata:
|
|
48
53
|
rubygems_mfa_required: 'true'
|
|
54
|
+
source_code_uri: https://github.com/svyatov/sec_id
|
|
55
|
+
changelog_uri: https://github.com/svyatov/sec_id/blob/main/CHANGELOG.md
|
|
56
|
+
bug_tracker_uri: https://github.com/svyatov/sec_id/issues
|
|
49
57
|
rdoc_options: []
|
|
50
58
|
require_paths:
|
|
51
59
|
- lib
|
|
@@ -53,14 +61,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
53
61
|
requirements:
|
|
54
62
|
- - ">="
|
|
55
63
|
- !ruby/object:Gem::Version
|
|
56
|
-
version: 3.
|
|
64
|
+
version: 3.2.0
|
|
57
65
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
58
66
|
requirements:
|
|
59
67
|
- - ">="
|
|
60
68
|
- !ruby/object:Gem::Version
|
|
61
69
|
version: '0'
|
|
62
70
|
requirements: []
|
|
63
|
-
rubygems_version: 4.0.
|
|
71
|
+
rubygems_version: 4.0.6
|
|
64
72
|
specification_version: 4
|
|
65
73
|
summary: Validate securities identification numbers with ease!
|
|
66
74
|
test_files: []
|