sec_id 5.1.0 → 6.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.
- checksums.yaml +4 -4
- data/.yardopts +6 -0
- data/CHANGELOG.md +38 -0
- data/MIGRATION.md +101 -0
- data/README.md +262 -15
- data/lib/sec_id/active_model.rb +144 -0
- data/lib/sec_id/base.rb +41 -1
- data/lib/sec_id/bic/country_codes.rb +46 -0
- data/lib/sec_id/bic.rb +123 -0
- data/lib/sec_id/cei.rb +19 -7
- data/lib/sec_id/cfi/attribute_set.rb +49 -0
- data/lib/sec_id/cfi/classification.rb +107 -0
- data/lib/sec_id/cfi/field.rb +56 -0
- data/lib/sec_id/cfi/tables.rb +1033 -0
- data/lib/sec_id/cfi.rb +153 -208
- data/lib/sec_id/cik.rb +13 -0
- data/lib/sec_id/concerns/checkable.rb +2 -2
- data/lib/sec_id/concerns/generatable.rb +71 -0
- data/lib/sec_id/concerns/normalizable.rb +1 -0
- data/lib/sec_id/concerns/validatable.rb +14 -3
- data/lib/sec_id/cusip.rb +18 -7
- data/lib/sec_id/deep_freeze.rb +18 -0
- data/lib/sec_id/detector.rb +28 -7
- data/lib/sec_id/dti.rb +104 -0
- data/lib/sec_id/errors.rb +7 -0
- data/lib/sec_id/figi.rb +18 -0
- data/lib/sec_id/fisn.rb +31 -0
- data/lib/sec_id/iban/country_rules.rb +7 -6
- data/lib/sec_id/iban.rb +26 -1
- data/lib/sec_id/isin.rb +20 -15
- data/lib/sec_id/lei.rb +13 -0
- data/lib/sec_id/occ.rb +21 -0
- data/lib/sec_id/railtie.rb +14 -0
- data/lib/sec_id/scanner.rb +140 -0
- data/lib/sec_id/sedol.rb +16 -0
- data/lib/sec_id/valoren.rb +13 -0
- data/lib/sec_id/version.rb +2 -1
- data/lib/sec_id/wkn.rb +16 -0
- data/lib/sec_id.rb +112 -32
- data/sec_id.gemspec +11 -5
- data/sig/manifest.yaml +8 -0
- data/sig/sec_id/base.rbs +72 -0
- data/sig/sec_id/bic/country_codes.rbs +5 -0
- data/sig/sec_id/bic.rbs +30 -0
- data/sig/sec_id/cei.rbs +26 -0
- data/sig/sec_id/cfi/attribute_set.rbs +68 -0
- data/sig/sec_id/cfi/classification.rbs +23 -0
- data/sig/sec_id/cfi/field.rbs +337 -0
- data/sig/sec_id/cfi/tables.rbs +68 -0
- data/sig/sec_id/cfi.rbs +56 -0
- data/sig/sec_id/cik.rbs +19 -0
- data/sig/sec_id/concerns/checkable.rbs +41 -0
- data/sig/sec_id/concerns/generatable.rbs +26 -0
- data/sig/sec_id/concerns/normalizable.rbs +34 -0
- data/sig/sec_id/concerns/validatable.rbs +42 -0
- data/sig/sec_id/cusip.rbs +29 -0
- data/sig/sec_id/deep_freeze.rbs +6 -0
- data/sig/sec_id/detector.rbs +35 -0
- data/sig/sec_id/dti.rbs +27 -0
- data/sig/sec_id/errors.rbs +28 -0
- data/sig/sec_id/figi.rbs +31 -0
- data/sig/sec_id/fisn.rbs +24 -0
- data/sig/sec_id/iban/country_rules.rbs +10 -0
- data/sig/sec_id/iban.rbs +54 -0
- data/sig/sec_id/isin.rbs +40 -0
- data/sig/sec_id/lei.rbs +33 -0
- data/sig/sec_id/occ.rbs +45 -0
- data/sig/sec_id/scanner.rbs +46 -0
- data/sig/sec_id/sedol.rbs +34 -0
- data/sig/sec_id/valoren.rbs +29 -0
- data/sig/sec_id/version.rbs +3 -0
- data/sig/sec_id/wkn.rbs +15 -0
- data/sig/sec_id.rbs +58 -0
- metadata +57 -7
- data/lib/sec_id/concerns/identifier_metadata.rb +0 -56
data/lib/sec_id/sedol.rb
CHANGED
|
@@ -17,9 +17,13 @@ module SecID
|
|
|
17
17
|
class SEDOL < Base
|
|
18
18
|
include Checkable
|
|
19
19
|
|
|
20
|
+
# Human-readable name of the standard.
|
|
20
21
|
FULL_NAME = 'Stock Exchange Daily Official List'
|
|
22
|
+
# Valid length(s) of a normalized identifier.
|
|
21
23
|
ID_LENGTH = 7
|
|
24
|
+
# A representative valid identifier.
|
|
22
25
|
EXAMPLE = 'B0YBKJ7'
|
|
26
|
+
# Pattern matching the identifier's permitted character set.
|
|
23
27
|
VALID_CHARS_REGEX = /\A[0-9BCDFGHJKLMNPQRSTVWXYZ]+\z/
|
|
24
28
|
|
|
25
29
|
# Regular expression for parsing SEDOL components.
|
|
@@ -32,6 +36,9 @@ module SecID
|
|
|
32
36
|
# Weights applied to each character position in the check digit calculation.
|
|
33
37
|
CHARACTER_WEIGHTS = [1, 3, 1, 7, 3, 9].freeze
|
|
34
38
|
|
|
39
|
+
# Characters valid in a SEDOL body (alphanumeric excluding vowels).
|
|
40
|
+
GENERATE_CHARSET = ALPHANUMERIC.grep(VALID_CHARS_REGEX).freeze
|
|
41
|
+
|
|
35
42
|
# @param sedol [String] the SEDOL string to parse
|
|
36
43
|
def initialize(sedol)
|
|
37
44
|
sedol_parts = parse sedol
|
|
@@ -60,6 +67,15 @@ module SecID
|
|
|
60
67
|
mod10(weighted_sum)
|
|
61
68
|
end
|
|
62
69
|
|
|
70
|
+
# Generates a random SEDOL body: 6 characters excluding vowels.
|
|
71
|
+
#
|
|
72
|
+
# @param random [Random] source of randomness
|
|
73
|
+
# @return [String] a 6-character SEDOL body without check digit
|
|
74
|
+
def self.generate_body(random)
|
|
75
|
+
random_string(GENERATE_CHARSET, 6, random: random)
|
|
76
|
+
end
|
|
77
|
+
private_class_method :generate_body
|
|
78
|
+
|
|
63
79
|
private
|
|
64
80
|
|
|
65
81
|
# @return [Hash]
|
data/lib/sec_id/valoren.rb
CHANGED
|
@@ -17,9 +17,13 @@ module SecID
|
|
|
17
17
|
# @example Normalize a Valoren to 9 digits
|
|
18
18
|
# SecID::Valoren.normalize('3886335') #=> '003886335'
|
|
19
19
|
class Valoren < Base
|
|
20
|
+
# Human-readable name of the standard.
|
|
20
21
|
FULL_NAME = 'Valoren Number'
|
|
22
|
+
# Valid length(s) of a normalized identifier.
|
|
21
23
|
ID_LENGTH = (5..9)
|
|
24
|
+
# A representative valid identifier.
|
|
22
25
|
EXAMPLE = '3886335'
|
|
26
|
+
# Pattern matching the identifier's permitted character set.
|
|
23
27
|
VALID_CHARS_REGEX = /\A[0-9]+\z/
|
|
24
28
|
|
|
25
29
|
# Regular expression for parsing Valoren components.
|
|
@@ -77,5 +81,14 @@ module SecID
|
|
|
77
81
|
def to_s
|
|
78
82
|
full_id
|
|
79
83
|
end
|
|
84
|
+
|
|
85
|
+
# Generates a random Valoren: an integer rendered without leading zeros.
|
|
86
|
+
#
|
|
87
|
+
# @param random [Random] source of randomness
|
|
88
|
+
# @return [String] a 5-9 digit Valoren
|
|
89
|
+
def self.generate_body(random)
|
|
90
|
+
random.rand(10_000..999_999_999).to_s
|
|
91
|
+
end
|
|
92
|
+
private_class_method :generate_body
|
|
80
93
|
end
|
|
81
94
|
end
|
data/lib/sec_id/version.rb
CHANGED
data/lib/sec_id/wkn.rb
CHANGED
|
@@ -13,9 +13,13 @@ module SecID
|
|
|
13
13
|
# SecID::WKN.valid?('514000') #=> true
|
|
14
14
|
# SecID::WKN.valid?('CBK100') #=> true
|
|
15
15
|
class WKN < Base
|
|
16
|
+
# Human-readable name of the standard.
|
|
16
17
|
FULL_NAME = 'Wertpapierkennnummer'
|
|
18
|
+
# Valid length(s) of a normalized identifier.
|
|
17
19
|
ID_LENGTH = 6
|
|
20
|
+
# A representative valid identifier.
|
|
18
21
|
EXAMPLE = '514000'
|
|
22
|
+
# Pattern matching the identifier's permitted character set.
|
|
19
23
|
VALID_CHARS_REGEX = /\A[0-9A-HJ-NP-Z]+\z/
|
|
20
24
|
|
|
21
25
|
# Regular expression for parsing WKN components.
|
|
@@ -24,6 +28,9 @@ module SecID
|
|
|
24
28
|
(?<identifier>[0-9A-HJ-NP-Z]{6})
|
|
25
29
|
\z/x
|
|
26
30
|
|
|
31
|
+
# Characters valid in a WKN (alphanumeric excluding I and O).
|
|
32
|
+
GENERATE_CHARSET = ALPHANUMERIC.grep(VALID_CHARS_REGEX).freeze
|
|
33
|
+
|
|
27
34
|
# @param wkn [String] the WKN string to parse
|
|
28
35
|
def initialize(wkn)
|
|
29
36
|
wkn_parts = parse(wkn)
|
|
@@ -40,5 +47,14 @@ module SecID
|
|
|
40
47
|
|
|
41
48
|
ISIN.new("#{country_code}000#{identifier}").restore!
|
|
42
49
|
end
|
|
50
|
+
|
|
51
|
+
# Generates a random WKN: 6 characters excluding I and O.
|
|
52
|
+
#
|
|
53
|
+
# @param random [Random] source of randomness
|
|
54
|
+
# @return [String] a 6-character WKN
|
|
55
|
+
def self.generate_body(random)
|
|
56
|
+
random_string(GENERATE_CHARSET, 6, random: random)
|
|
57
|
+
end
|
|
58
|
+
private_class_method :generate_body
|
|
43
59
|
end
|
|
44
60
|
end
|
data/lib/sec_id.rb
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
require 'sec_id/version'
|
|
4
4
|
|
|
5
|
+
# Toolkit for securities identifiers — validate, normalize, parse, detect, convert,
|
|
6
|
+
# generate, and classify ISIN, CUSIP, CEI, SEDOL, FIGI, LEI, IBAN, CIK, OCC, WKN,
|
|
7
|
+
# Valoren, CFI, FISN, BIC, and DTI.
|
|
8
|
+
#
|
|
9
|
+
# @see https://github.com/svyatov/sec_id
|
|
5
10
|
module SecID
|
|
6
11
|
# Base error class for all SecID errors.
|
|
7
12
|
class Error < StandardError; end
|
|
@@ -15,6 +20,9 @@ module SecID
|
|
|
15
20
|
# Raised for type-specific structural errors (invalid prefix, category, group, BBAN, or date).
|
|
16
21
|
class InvalidStructureError < Error; end
|
|
17
22
|
|
|
23
|
+
# Raised when multiple identifier types match and on_ambiguous: :raise is used.
|
|
24
|
+
class AmbiguousMatchError < Error; end
|
|
25
|
+
|
|
18
26
|
class << self
|
|
19
27
|
# Looks up an identifier class by its symbol key.
|
|
20
28
|
#
|
|
@@ -49,50 +57,103 @@ module SecID
|
|
|
49
57
|
# @return [Boolean]
|
|
50
58
|
# @raise [ArgumentError] if any key in types is unknown
|
|
51
59
|
def valid?(str, types: nil)
|
|
52
|
-
return
|
|
60
|
+
return detector.matches?(str) if types.nil?
|
|
53
61
|
|
|
54
62
|
types.any? { |key| self[key].valid?(str) }
|
|
55
63
|
end
|
|
56
64
|
|
|
57
|
-
#
|
|
58
|
-
#
|
|
65
|
+
# @param text [String, nil] the text to scan
|
|
66
|
+
# @param types [Array<Symbol>, nil] restrict to specific types
|
|
67
|
+
# @return [Array<Match>]
|
|
68
|
+
# @raise [ArgumentError] if any key in types is unknown
|
|
69
|
+
def extract(text, types: nil)
|
|
70
|
+
scan(text, types: types).to_a
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @param text [String, nil] the text to scan
|
|
74
|
+
# @param types [Array<Symbol>, nil] restrict to specific types
|
|
75
|
+
# @return [Enumerator<Match>] if no block given
|
|
76
|
+
# @yieldparam match [Match]
|
|
77
|
+
# @raise [ArgumentError] if any key in types is unknown
|
|
78
|
+
def scan(text, types: nil, &)
|
|
79
|
+
classes = types&.map { |key| self[key] }
|
|
80
|
+
scanner.call(text, classes: classes, &)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @param str [String, nil] the identifier string to explain
|
|
84
|
+
# @param types [Array<Symbol>, nil] restrict to specific types
|
|
85
|
+
# @return [Hash] hash with :input and :candidates keys
|
|
86
|
+
def explain(str, types: nil)
|
|
87
|
+
input = str.to_s.strip
|
|
88
|
+
target_keys = types || identifier_list.map { |k| k.short_name.downcase.to_sym }
|
|
89
|
+
candidates = target_keys.map do |key|
|
|
90
|
+
instance = self[key].new(input)
|
|
91
|
+
{ type: key, valid: instance.valid?, errors: instance.errors.details }
|
|
92
|
+
end
|
|
93
|
+
{ input: input, candidates: candidates }
|
|
94
|
+
end
|
|
95
|
+
|
|
59
96
|
# @param str [String, nil] the identifier string to parse
|
|
60
97
|
# @param types [Array<Symbol>, nil] restrict to specific types (e.g. [:isin, :cusip])
|
|
61
|
-
# @
|
|
62
|
-
# @
|
|
63
|
-
|
|
64
|
-
|
|
98
|
+
# @param on_ambiguous [:first, :raise, :all] how to handle multiple matches
|
|
99
|
+
# @return [SecID::Base, nil, Array<SecID::Base>] depends on on_ambiguous mode
|
|
100
|
+
# @raise [AmbiguousMatchError] when on_ambiguous: :raise and multiple types match
|
|
101
|
+
def parse(str, types: nil, on_ambiguous: :first)
|
|
102
|
+
case on_ambiguous
|
|
103
|
+
when :first then types.nil? ? parse_any(str) : parse_from(str, types)
|
|
104
|
+
when :raise then parse_strict(str, types)
|
|
105
|
+
when :all then parse_all(str, types)
|
|
106
|
+
else raise ArgumentError, "Unknown on_ambiguous mode: #{on_ambiguous.inspect}"
|
|
107
|
+
end
|
|
65
108
|
end
|
|
66
109
|
|
|
67
|
-
# Parses a string into the most specific matching identifier instance, raising on failure.
|
|
68
|
-
#
|
|
69
110
|
# @param str [String, nil] the identifier string to parse
|
|
70
111
|
# @param types [Array<Symbol>, nil] restrict to specific types (e.g. [:isin, :cusip])
|
|
71
|
-
# @
|
|
112
|
+
# @param on_ambiguous [:first, :raise, :all] how to handle multiple matches
|
|
113
|
+
# @return [SecID::Base, Array<SecID::Base>] depends on on_ambiguous mode
|
|
72
114
|
# @raise [InvalidFormatError] if no matching identifier type is found
|
|
73
|
-
# @raise [
|
|
74
|
-
def parse!(str, types: nil)
|
|
75
|
-
parse(str, types: types
|
|
115
|
+
# @raise [AmbiguousMatchError] when on_ambiguous: :raise and multiple types match
|
|
116
|
+
def parse!(str, types: nil, on_ambiguous: :first)
|
|
117
|
+
result = parse(str, types: types, on_ambiguous: on_ambiguous)
|
|
118
|
+
|
|
119
|
+
if on_ambiguous == :all
|
|
120
|
+
raise(InvalidFormatError, parse_error_message(str, types)) if result.empty?
|
|
121
|
+
|
|
122
|
+
return result
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
result || raise(InvalidFormatError, parse_error_message(str, types))
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Generates a new, format-valid identifier instance of the given type.
|
|
129
|
+
#
|
|
130
|
+
# @note Generated identifiers are valid in format only — they are not real,
|
|
131
|
+
# registered securities.
|
|
132
|
+
#
|
|
133
|
+
# @param key [Symbol] identifier type (e.g. :isin, :cusip)
|
|
134
|
+
# @param random [Random] seedable source for reproducible output
|
|
135
|
+
# @return [SecID::Base] a generated, format-valid instance of the type mapped to `key`
|
|
136
|
+
# (e.g. {SecID::ISIN} for `:isin`)
|
|
137
|
+
# @raise [ArgumentError] if key is unknown
|
|
138
|
+
def generate(key, random: Random.new)
|
|
139
|
+
self[key].generate(random: random)
|
|
76
140
|
end
|
|
77
141
|
|
|
78
142
|
private
|
|
79
143
|
|
|
80
|
-
# @param klass [Class] the identifier class to register
|
|
81
144
|
# @return [void]
|
|
82
145
|
def register_identifier(klass)
|
|
83
146
|
key = klass.name.split('::').last.downcase.to_sym
|
|
84
147
|
identifier_map[key] = klass
|
|
85
148
|
identifier_list << klass
|
|
86
149
|
@detector = nil
|
|
150
|
+
@scanner = nil
|
|
87
151
|
end
|
|
88
152
|
|
|
89
|
-
# @return [SecID::Base, nil]
|
|
90
153
|
def parse_any(str)
|
|
91
|
-
|
|
92
|
-
key && self[key].new(str)
|
|
154
|
+
detector.first_match(str)
|
|
93
155
|
end
|
|
94
156
|
|
|
95
|
-
# @return [SecID::Base, nil]
|
|
96
157
|
def parse_from(str, types)
|
|
97
158
|
types.each do |key|
|
|
98
159
|
instance = self[key].new(str)
|
|
@@ -101,36 +162,49 @@ module SecID
|
|
|
101
162
|
nil
|
|
102
163
|
end
|
|
103
164
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
165
|
+
def parse_strict(str, types)
|
|
166
|
+
candidates = resolve_candidates(str, types)
|
|
167
|
+
raise AmbiguousMatchError, ambiguous_message(str, candidates) if candidates.size > 1
|
|
168
|
+
|
|
169
|
+
candidates.first && self[candidates.first].new(str)
|
|
108
170
|
end
|
|
109
171
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
@detector ||= Detector.new(identifier_list)
|
|
172
|
+
def parse_all(str, types)
|
|
173
|
+
resolve_candidates(str, types).map { |key| self[key].new(str) }
|
|
113
174
|
end
|
|
114
175
|
|
|
115
|
-
# @return [
|
|
116
|
-
def
|
|
117
|
-
|
|
176
|
+
# @return [Array<Symbol>]
|
|
177
|
+
def resolve_candidates(str, types)
|
|
178
|
+
types ? types.select { |key| self[key].valid?(str) } : detect(str)
|
|
118
179
|
end
|
|
119
180
|
|
|
120
|
-
# @return [
|
|
121
|
-
def
|
|
122
|
-
|
|
181
|
+
# @return [String]
|
|
182
|
+
def ambiguous_message(str, candidates)
|
|
183
|
+
"Ambiguous identifier #{str.to_s.strip.inspect}: matches #{candidates.inspect}"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# @return [String]
|
|
187
|
+
def parse_error_message(str, types)
|
|
188
|
+
base = "No matching identifier type found for #{str.to_s.strip.inspect}"
|
|
189
|
+
types ? "#{base} among #{types.inspect}" : base
|
|
123
190
|
end
|
|
191
|
+
|
|
192
|
+
def detector = @detector ||= Detector.new(identifier_list)
|
|
193
|
+
def scanner = @scanner ||= Scanner.new(identifier_list)
|
|
194
|
+
def identifier_map = @identifier_map ||= {}
|
|
195
|
+
def identifier_list = @identifier_list ||= []
|
|
124
196
|
end
|
|
125
197
|
end
|
|
126
198
|
|
|
199
|
+
require 'sec_id/deep_freeze'
|
|
127
200
|
require 'sec_id/errors'
|
|
128
|
-
require 'sec_id/concerns/identifier_metadata'
|
|
129
201
|
require 'sec_id/concerns/normalizable'
|
|
130
202
|
require 'sec_id/concerns/validatable'
|
|
131
203
|
require 'sec_id/concerns/checkable'
|
|
204
|
+
require 'sec_id/concerns/generatable'
|
|
132
205
|
require 'sec_id/base'
|
|
133
206
|
require 'sec_id/detector'
|
|
207
|
+
require 'sec_id/scanner'
|
|
134
208
|
require 'sec_id/isin'
|
|
135
209
|
require 'sec_id/cusip'
|
|
136
210
|
require 'sec_id/sedol'
|
|
@@ -144,3 +218,9 @@ require 'sec_id/valoren'
|
|
|
144
218
|
require 'sec_id/cei'
|
|
145
219
|
require 'sec_id/cfi'
|
|
146
220
|
require 'sec_id/fisn'
|
|
221
|
+
require 'sec_id/bic'
|
|
222
|
+
require 'sec_id/dti'
|
|
223
|
+
|
|
224
|
+
# Auto-activate the ActiveModel validator inside Rails; a no-op everywhere else (keeps the
|
|
225
|
+
# default require zero-dependency). See SecID::Railtie and lib/sec_id/active_model.rb.
|
|
226
|
+
require 'sec_id/railtie' if defined?(Rails::Railtie)
|
data/sec_id.gemspec
CHANGED
|
@@ -10,19 +10,25 @@ Gem::Specification.new do |spec|
|
|
|
10
10
|
spec.authors = ['Leonid Svyatov']
|
|
11
11
|
spec.email = ['leonid@svyatov.ru']
|
|
12
12
|
|
|
13
|
-
spec.summary = '
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
'and
|
|
13
|
+
spec.summary = 'A Ruby toolkit for securities identifiers — validate, parse, normalize, detect, convert, ' \
|
|
14
|
+
'generate, and classify.'
|
|
15
|
+
spec.description = 'Validate, normalize, parse, convert, generate, and classify securities identifiers. ' \
|
|
16
|
+
'Auto-detect identifier type from any string. Calculate and restore check digits. Generate ' \
|
|
17
|
+
'format-valid identifiers as test fixtures. Decode CFI codes to full ISO 10962:2021 ' \
|
|
18
|
+
'classifications. Includes an opt-in ActiveModel/Rails validator that adds no runtime ' \
|
|
19
|
+
'dependency. Supports ISIN, CUSIP, CEI, SEDOL, FIGI, LEI, IBAN, CIK, OCC, WKN, Valoren, ' \
|
|
20
|
+
'CFI, FISN, BIC, and DTI.'
|
|
17
21
|
spec.homepage = 'https://github.com/svyatov/sec_id'
|
|
18
22
|
spec.license = 'MIT'
|
|
19
23
|
|
|
20
24
|
spec.required_ruby_version = '>= 3.2.0'
|
|
21
25
|
|
|
22
26
|
spec.require_paths = ['lib']
|
|
23
|
-
spec.files = Dir['lib/**/*.rb'] +
|
|
27
|
+
spec.files = Dir['lib/**/*.rb'] + Dir['sig/**/*'] +
|
|
28
|
+
%w[.yardopts CHANGELOG.md LICENSE.txt MIGRATION.md README.md sec_id.gemspec]
|
|
24
29
|
|
|
25
30
|
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
31
|
+
spec.metadata['documentation_uri'] = 'https://rubydoc.info/gems/sec_id'
|
|
26
32
|
spec.metadata['source_code_uri'] = 'https://github.com/svyatov/sec_id'
|
|
27
33
|
spec.metadata['changelog_uri'] = 'https://github.com/svyatov/sec_id/blob/main/CHANGELOG.md'
|
|
28
34
|
spec.metadata['bug_tracker_uri'] = 'https://github.com/svyatov/sec_id/issues'
|
data/sig/manifest.yaml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Standard-library signatures these RBS files reference. Consumers' RBS/Steep
|
|
2
|
+
# load these automatically when they depend on sec_id. (The gem has no runtime
|
|
3
|
+
# gem dependencies, so none are auto-derived from the gemspec.)
|
|
4
|
+
#
|
|
5
|
+
# Set (ISIN, SEDOL, FIGI, Valoren, BIC) is a core class in rbs 4.0 — always
|
|
6
|
+
# loaded, so it needs no manifest entry.
|
|
7
|
+
dependencies:
|
|
8
|
+
- name: date # Date in OCC
|
data/sig/sec_id/base.rbs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module SecID
|
|
2
|
+
# Accepted raw input for parsing/validating an identifier: a String, an Integer
|
|
3
|
+
# (Valoren/CIK/CUSIP accept numeric input), or nil.
|
|
4
|
+
type input = String | Integer | nil
|
|
5
|
+
|
|
6
|
+
# The class-object surface the concern ClassMethods modules call through `self`
|
|
7
|
+
# once extended onto an identifier class (they construct instances via `new`).
|
|
8
|
+
interface _IdentifierClass
|
|
9
|
+
def new: (SecID::input) -> Base
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Base class for securities identifiers, providing a common interface for
|
|
13
|
+
# validation, normalization, parsing, and generation.
|
|
14
|
+
class Base
|
|
15
|
+
include Normalizable
|
|
16
|
+
include Validatable
|
|
17
|
+
include Generatable
|
|
18
|
+
extend Normalizable::ClassMethods
|
|
19
|
+
extend Validatable::ClassMethods
|
|
20
|
+
extend Generatable::ClassMethods
|
|
21
|
+
|
|
22
|
+
# Metadata constants each subclass overrides with its concrete shape. Declared
|
|
23
|
+
# here (as the widest shape) so the shared concern method bodies type-check.
|
|
24
|
+
FULL_NAME: String
|
|
25
|
+
ID_LENGTH: Integer | ::Range[Integer] | ::Array[Integer]
|
|
26
|
+
EXAMPLE: String
|
|
27
|
+
VALID_CHARS_REGEX: ::Regexp
|
|
28
|
+
ID_REGEX: ::Regexp
|
|
29
|
+
|
|
30
|
+
attr_reader full_id: String
|
|
31
|
+
# Reader is nilable (nil for invalid input, checked by `valid_format?`); the ivar is
|
|
32
|
+
# typed `untyped` because subclasses use `@identifier` as non-nil after validation and
|
|
33
|
+
# RBS forbids narrowing an inherited ivar's type.
|
|
34
|
+
def identifier: () -> String?
|
|
35
|
+
|
|
36
|
+
def self.short_name: () -> String
|
|
37
|
+
def self.full_name: () -> String
|
|
38
|
+
def self.id_length: () -> (Integer | ::Range[Integer] | ::Array[Integer])
|
|
39
|
+
def self.length_values: () -> (::Array[Integer] | ::Range[Integer])
|
|
40
|
+
def self.length_specificity: () -> (Integer | Float | nil)
|
|
41
|
+
def self.example: () -> String
|
|
42
|
+
def self.has_check_digit?: () -> bool
|
|
43
|
+
def self.inherited: (singleton(Base) subclass) -> void
|
|
44
|
+
|
|
45
|
+
def initialize: (SecID::input _sec_id_number) -> void
|
|
46
|
+
|
|
47
|
+
def ==: (Base other) -> bool
|
|
48
|
+
alias eql? ==
|
|
49
|
+
def hash: () -> Integer
|
|
50
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
51
|
+
def as_json: (*untyped) -> Hash[Symbol, untyped]
|
|
52
|
+
|
|
53
|
+
# Check-digit surface. Only the types that `include Checkable` implement these
|
|
54
|
+
# (they raise/NoMethodError otherwise); declared on Base so `Generatable#generate`
|
|
55
|
+
# and `Checkable::ClassMethods` type-check without narrowing on `has_check_digit?`.
|
|
56
|
+
def restore: () -> String
|
|
57
|
+
def restore!: () -> self
|
|
58
|
+
def calculate_check_digit: () -> (Integer | String)
|
|
59
|
+
|
|
60
|
+
def comparison_id: () -> String
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def components: () -> Hash[Symbol, untyped]
|
|
65
|
+
def parse: (SecID::input sec_id_number) -> (::MatchData | ::Hash[Symbol, untyped])
|
|
66
|
+
|
|
67
|
+
@full_id: String
|
|
68
|
+
@identifier: untyped
|
|
69
|
+
self.@short_name: String
|
|
70
|
+
self.@has_check_digit: bool
|
|
71
|
+
end
|
|
72
|
+
end
|
data/sig/sec_id/bic.rbs
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module SecID
|
|
2
|
+
# Business Identifier Code (BIC / SWIFT code).
|
|
3
|
+
class BIC < Base
|
|
4
|
+
FULL_NAME: String
|
|
5
|
+
ID_LENGTH: Array[Integer]
|
|
6
|
+
EXAMPLE: String
|
|
7
|
+
VALID_CHARS_REGEX: ::Regexp
|
|
8
|
+
ID_REGEX: ::Regexp
|
|
9
|
+
|
|
10
|
+
def self.countries: () -> Array[String]
|
|
11
|
+
|
|
12
|
+
attr_reader bank_code: String?
|
|
13
|
+
attr_reader country_code: String?
|
|
14
|
+
attr_reader location_code: String?
|
|
15
|
+
attr_reader branch_code: String?
|
|
16
|
+
|
|
17
|
+
def initialize: (SecID::input bic) -> void
|
|
18
|
+
private def self.generate_body: (Random random) -> String
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def components: () -> Hash[Symbol, untyped]
|
|
23
|
+
def valid_format?: () -> bool
|
|
24
|
+
def detect_errors: () -> Array[Symbol]
|
|
25
|
+
def validation_message: (Symbol code) -> String?
|
|
26
|
+
def recognized_country?: () -> bool
|
|
27
|
+
|
|
28
|
+
self.@countries: Array[String]
|
|
29
|
+
end
|
|
30
|
+
end
|
data/sig/sec_id/cei.rbs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module SecID
|
|
2
|
+
# CUSIP Entity Identifier (CEI).
|
|
3
|
+
class CEI < Base
|
|
4
|
+
include Checkable
|
|
5
|
+
extend Checkable::ClassMethods
|
|
6
|
+
|
|
7
|
+
FULL_NAME: String
|
|
8
|
+
ID_LENGTH: Integer
|
|
9
|
+
EXAMPLE: String
|
|
10
|
+
VALID_CHARS_REGEX: ::Regexp
|
|
11
|
+
ID_REGEX: ::Regexp
|
|
12
|
+
|
|
13
|
+
attr_reader prefix: String?
|
|
14
|
+
attr_reader numeric: String?
|
|
15
|
+
attr_reader entity_id: String?
|
|
16
|
+
|
|
17
|
+
def initialize: (SecID::input cei) -> void
|
|
18
|
+
def calculate_check_digit: () -> Integer
|
|
19
|
+
def self.check_digit: (SecID::input id) -> Integer
|
|
20
|
+
private def self.generate_body: (Random random) -> String
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def components: () -> Hash[Symbol, untyped]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Generated by `rake sig:cfi` from SecID::CFI::Tables. Do not edit by hand.
|
|
2
|
+
module SecID
|
|
3
|
+
class CFI < Base
|
|
4
|
+
# The ordered collection of decoded attribute Fields.
|
|
5
|
+
class AttributeSet
|
|
6
|
+
include Enumerable[Field]
|
|
7
|
+
|
|
8
|
+
# `fields` is typed `untyped` because `#initialize` calls
|
|
9
|
+
# `define_singleton_method(field.meaning)` and `field.meaning` is nilable.
|
|
10
|
+
def initialize: (untyped fields) -> void
|
|
11
|
+
def each: () { (Field) -> void } -> self
|
|
12
|
+
| () -> Enumerator[Field, self]
|
|
13
|
+
def []: (Symbol meaning) -> Field?
|
|
14
|
+
def empty?: () -> bool
|
|
15
|
+
def to_h: () -> Hash[untyped, untyped]
|
|
16
|
+
def as_json: (*untyped) -> Hash[Symbol, untyped]
|
|
17
|
+
|
|
18
|
+
# Per-meaning readers — the union of every attribute meaning. Each instance
|
|
19
|
+
# only answers the meanings actually present; an absent one raises
|
|
20
|
+
# NoMethodError at runtime. Use #[] for the presence-uncertain path.
|
|
21
|
+
def asset_class: () -> Field
|
|
22
|
+
def assets: () -> Field
|
|
23
|
+
def barrier_dependency: () -> Field
|
|
24
|
+
def calculation_frequency: () -> Field
|
|
25
|
+
def call_put: () -> Field
|
|
26
|
+
def closed_open: () -> Field
|
|
27
|
+
def components: () -> Field
|
|
28
|
+
def composition: () -> Field
|
|
29
|
+
def currency: () -> Field
|
|
30
|
+
def delivery: () -> Field
|
|
31
|
+
def distribution: () -> Field
|
|
32
|
+
def distribution_policy: () -> Field
|
|
33
|
+
def equity_type: () -> Field
|
|
34
|
+
def exercise_style: () -> Field
|
|
35
|
+
def form: () -> Field
|
|
36
|
+
def further_grouping: () -> Field
|
|
37
|
+
def guarantee: () -> Field
|
|
38
|
+
def income: () -> Field
|
|
39
|
+
def interest_rate_type: () -> Field
|
|
40
|
+
def interest_type: () -> Field
|
|
41
|
+
def investment_strategy: () -> Field
|
|
42
|
+
def issuer_type: () -> Field
|
|
43
|
+
def long_short: () -> Field
|
|
44
|
+
def notional: () -> Field
|
|
45
|
+
def option_style: () -> Field
|
|
46
|
+
def ownership_restrictions: () -> Field
|
|
47
|
+
def payment_status: () -> Field
|
|
48
|
+
def redemption: () -> Field
|
|
49
|
+
def repayment: () -> Field
|
|
50
|
+
def return_trigger: () -> Field
|
|
51
|
+
def return_type: () -> Field
|
|
52
|
+
def security_type: () -> Field
|
|
53
|
+
def standardized: () -> Field
|
|
54
|
+
def strategy_style: () -> Field
|
|
55
|
+
def termination: () -> Field
|
|
56
|
+
def type: () -> Field
|
|
57
|
+
def underlying: () -> Field
|
|
58
|
+
def underlying_assets: () -> Field
|
|
59
|
+
def underlying_fund_type: () -> Field
|
|
60
|
+
def valuation: () -> Field
|
|
61
|
+
def voting_right: () -> Field
|
|
62
|
+
def warrant_type: () -> Field
|
|
63
|
+
def weighting: () -> Field
|
|
64
|
+
|
|
65
|
+
@fields: Array[Field]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module SecID
|
|
2
|
+
class CFI < Base
|
|
3
|
+
# The decoded ISO 10962:2021 classification of a valid CFI, built by {CFI#decode}.
|
|
4
|
+
class Classification
|
|
5
|
+
attr_reader category: Field
|
|
6
|
+
attr_reader group: Field
|
|
7
|
+
attr_reader attributes: AttributeSet
|
|
8
|
+
|
|
9
|
+
def initialize: (String category_code, String group_code, Array[String] letters) -> void
|
|
10
|
+
def to_s: () -> String
|
|
11
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
12
|
+
def as_json: (*untyped) -> Hash[Symbol, untyped]
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def build_category: (String code) -> Field
|
|
17
|
+
def build_group: (String category_code, String code) -> Field
|
|
18
|
+
def build_attributes: (String category_code, String group_code, Array[String] letters) -> AttributeSet
|
|
19
|
+
def build_attribute: (untyped position, String letter) -> Field
|
|
20
|
+
def decode_value: (untyped value_map, String letter) -> [Symbol, String]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|