sec_id 4.4.1 → 5.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/CHANGELOG.md +49 -3
- data/MIGRATION.md +257 -0
- data/README.md +234 -142
- data/lib/sec_id/base.rb +14 -38
- data/lib/sec_id/cei.rb +7 -2
- data/lib/sec_id/cfi.rb +38 -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 +42 -16
- data/lib/sec_id/concerns/validatable.rb +158 -0
- data/lib/sec_id/cusip.rb +10 -8
- data/lib/sec_id/detector.rb +156 -0
- data/lib/sec_id/errors.rb +67 -0
- data/lib/sec_id/figi.rb +30 -10
- data/lib/sec_id/fisn.rb +10 -4
- data/lib/sec_id/iban/country_rules.rb +4 -2
- data/lib/sec_id/iban.rb +44 -12
- data/lib/sec_id/isin.rb +11 -6
- data/lib/sec_id/lei.rb +12 -9
- data/lib/sec_id/occ.rb +40 -25
- data/lib/sec_id/sedol.rb +9 -7
- data/lib/sec_id/valoren.rb +22 -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/cusip.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module SecID
|
|
4
4
|
# Committee on Uniform Securities Identification Procedures (CUSIP) - a 9-character
|
|
5
5
|
# alphanumeric code that identifies North American securities.
|
|
6
6
|
#
|
|
@@ -9,14 +9,19 @@ module SecId
|
|
|
9
9
|
# @see https://en.wikipedia.org/wiki/CUSIP
|
|
10
10
|
#
|
|
11
11
|
# @example Validate a CUSIP
|
|
12
|
-
#
|
|
12
|
+
# SecID::CUSIP.valid?('037833100') #=> true
|
|
13
13
|
#
|
|
14
14
|
# @example Convert to ISIN
|
|
15
|
-
# cusip =
|
|
16
|
-
# cusip.to_isin('US') #=> #<
|
|
15
|
+
# cusip = SecID::CUSIP.new('037833100')
|
|
16
|
+
# cusip.to_isin('US') #=> #<SecID::ISIN>
|
|
17
17
|
class CUSIP < Base
|
|
18
18
|
include Checkable
|
|
19
19
|
|
|
20
|
+
FULL_NAME = 'Committee on Uniform Securities Identification Procedures'
|
|
21
|
+
ID_LENGTH = 9
|
|
22
|
+
EXAMPLE = '037833100'
|
|
23
|
+
VALID_CHARS_REGEX = /\A[A-Z0-9*@#]+\z/
|
|
24
|
+
|
|
20
25
|
# Regular expression for parsing CUSIP components.
|
|
21
26
|
ID_REGEX = /\A
|
|
22
27
|
(?<identifier>
|
|
@@ -55,10 +60,7 @@ module SecId
|
|
|
55
60
|
raise(InvalidFormatError, "'#{country_code}' is not a CGS country code!")
|
|
56
61
|
end
|
|
57
62
|
|
|
58
|
-
|
|
59
|
-
isin = ISIN.new(country_code + cusip_with_check_digit)
|
|
60
|
-
isin.restore!
|
|
61
|
-
isin
|
|
63
|
+
ISIN.new(country_code + restore).restore!
|
|
62
64
|
end
|
|
63
65
|
|
|
64
66
|
# @return [Boolean] true if first character is a letter (CINS identifier)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SecID
|
|
4
|
+
# Detects which identifier types match a given string using a three-stage
|
|
5
|
+
# pipeline that eliminates most candidates before calling `valid?`.
|
|
6
|
+
#
|
|
7
|
+
# Stage 1 — Special-character dispatch (O(1)):
|
|
8
|
+
# Strings containing `/`, ` `, or `*@#` route to the only types accepting those chars.
|
|
9
|
+
#
|
|
10
|
+
# Stage 2 — Length lookup (O(1) hash access):
|
|
11
|
+
# Pre-computed table maps each possible length to candidate classes.
|
|
12
|
+
#
|
|
13
|
+
# Stage 3 — Charset pre-filter:
|
|
14
|
+
# Survivors are filtered by their VALID_CHARS_REGEX before calling `valid?`.
|
|
15
|
+
#
|
|
16
|
+
# Typical result: 1-2 `valid?` calls instead of 13.
|
|
17
|
+
#
|
|
18
|
+
# @api private
|
|
19
|
+
class Detector
|
|
20
|
+
# @param identifier_list [Array<Class>] registered identifier classes
|
|
21
|
+
def initialize(identifier_list)
|
|
22
|
+
@classes = identifier_list.dup.freeze
|
|
23
|
+
precompute
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Detects all matching identifier types for the given string.
|
|
27
|
+
#
|
|
28
|
+
# @param str [String, nil] the identifier string to detect
|
|
29
|
+
# @return [Array<Symbol>] matching type symbols sorted by specificity
|
|
30
|
+
def call(str)
|
|
31
|
+
input = str.to_s.strip
|
|
32
|
+
return [] if input.empty?
|
|
33
|
+
|
|
34
|
+
upcased = input.upcase
|
|
35
|
+
candidates = filter_candidates(upcased)
|
|
36
|
+
validate_and_sort(input, candidates)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Runs stages 1-3 to narrow candidate classes.
|
|
42
|
+
#
|
|
43
|
+
# @param upcased [String]
|
|
44
|
+
# @return [Array<Class>]
|
|
45
|
+
def filter_candidates(upcased)
|
|
46
|
+
candidates = stage1_special_chars(upcased) || stage2_length(upcased.length)
|
|
47
|
+
return candidates if candidates.empty?
|
|
48
|
+
|
|
49
|
+
stage3_charset(upcased, candidates)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Validates candidates and returns sorted symbol keys.
|
|
53
|
+
#
|
|
54
|
+
# @param input [String]
|
|
55
|
+
# @param candidates [Array<Class>]
|
|
56
|
+
# @return [Array<Symbol>]
|
|
57
|
+
def validate_and_sort(input, candidates)
|
|
58
|
+
matches = candidates.select { |klass| klass.valid?(input) }
|
|
59
|
+
matches.sort_by! { |klass| @priority_for[klass] }
|
|
60
|
+
matches.map! { |klass| @key_for[klass] }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @return [void]
|
|
64
|
+
def precompute
|
|
65
|
+
build_discriminator_sets
|
|
66
|
+
build_length_table
|
|
67
|
+
build_priority_table
|
|
68
|
+
build_key_table
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Classifies types by which special characters their VALID_CHARS_REGEX accepts.
|
|
72
|
+
#
|
|
73
|
+
# @return [void]
|
|
74
|
+
def build_discriminator_sets
|
|
75
|
+
@slash_types = @classes.select { |k| accepts_char?(k, '/') }
|
|
76
|
+
space_types = @classes.select { |k| accepts_char?(k, ' ') }
|
|
77
|
+
@space_only_types = space_types - @slash_types
|
|
78
|
+
@special_types = @classes.select { |k| accepts_char?(k, '*') }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Builds a Hash mapping each possible length to the classes that accept it.
|
|
82
|
+
#
|
|
83
|
+
# @return [void]
|
|
84
|
+
def build_length_table
|
|
85
|
+
@candidates_by_length = Hash.new { |h, k| h[k] = [] }
|
|
86
|
+
@classes.each do |klass|
|
|
87
|
+
id_length = klass::ID_LENGTH
|
|
88
|
+
lengths = id_length.is_a?(Range) ? id_length : [id_length]
|
|
89
|
+
lengths.each { |len| @candidates_by_length[len] << klass }
|
|
90
|
+
end
|
|
91
|
+
@candidates_by_length.each_value(&:freeze)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Builds composite sort keys: check-digit types first, then smaller range, then load order.
|
|
95
|
+
#
|
|
96
|
+
# @return [void]
|
|
97
|
+
def build_priority_table
|
|
98
|
+
@priority_for = {}
|
|
99
|
+
@classes.each_with_index do |klass, index|
|
|
100
|
+
check_digit_rank = klass.has_check_digit? ? 0 : 1
|
|
101
|
+
id_length = klass::ID_LENGTH
|
|
102
|
+
range_size = id_length.is_a?(Range) ? id_length.size : 1
|
|
103
|
+
@priority_for[klass] = [check_digit_rank, range_size, index].freeze
|
|
104
|
+
end
|
|
105
|
+
@priority_for.freeze
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Maps each class to its registry symbol key.
|
|
109
|
+
#
|
|
110
|
+
# @return [void]
|
|
111
|
+
def build_key_table
|
|
112
|
+
@key_for = {}
|
|
113
|
+
@classes.each { |klass| @key_for[klass] = klass.short_name.downcase.to_sym }
|
|
114
|
+
@key_for.freeze
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Stage 1: route strings with special characters to the only types that accept them.
|
|
118
|
+
# Returns nil if no special chars found (fall through to stage 2).
|
|
119
|
+
#
|
|
120
|
+
# @param upcased [String]
|
|
121
|
+
# @return [Array<Class>, nil]
|
|
122
|
+
def stage1_special_chars(upcased)
|
|
123
|
+
return @slash_types if upcased.include?('/')
|
|
124
|
+
return @space_only_types if upcased.include?(' ')
|
|
125
|
+
return @special_types if upcased.match?(/[*@#]/)
|
|
126
|
+
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Stage 2: look up candidates by string length.
|
|
131
|
+
#
|
|
132
|
+
# @param length [Integer]
|
|
133
|
+
# @return [Array<Class>]
|
|
134
|
+
def stage2_length(length)
|
|
135
|
+
@candidates_by_length[length] || []
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Stage 3: filter candidates by character set.
|
|
139
|
+
#
|
|
140
|
+
# @param upcased [String]
|
|
141
|
+
# @param candidates [Array<Class>]
|
|
142
|
+
# @return [Array<Class>]
|
|
143
|
+
def stage3_charset(upcased, candidates)
|
|
144
|
+
candidates.select { |klass| upcased.match?(klass::VALID_CHARS_REGEX) }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Tests whether a class's VALID_CHARS_REGEX accepts a given character.
|
|
148
|
+
#
|
|
149
|
+
# @param klass [Class]
|
|
150
|
+
# @param char [String] single character
|
|
151
|
+
# @return [Boolean]
|
|
152
|
+
def accepts_char?(klass, char)
|
|
153
|
+
char.match?(klass::VALID_CHARS_REGEX)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SecID
|
|
4
|
+
# Immutable value object representing validation errors for an identifier.
|
|
5
|
+
# Follows Rails/ActiveModel conventions: use {#details} for structured error data
|
|
6
|
+
# and {#messages} for human-readable strings.
|
|
7
|
+
#
|
|
8
|
+
# @example No errors
|
|
9
|
+
# errors = SecID::Errors.new([])
|
|
10
|
+
# errors.none? #=> true
|
|
11
|
+
# errors.empty? #=> true
|
|
12
|
+
# errors.messages #=> []
|
|
13
|
+
#
|
|
14
|
+
# @example With errors
|
|
15
|
+
# err = [{ error: :invalid_length, message: "Expected 12 characters, got 5" }]
|
|
16
|
+
# errors = SecID::Errors.new(err)
|
|
17
|
+
# errors.none? #=> false
|
|
18
|
+
# errors.details #=> [{ error: :invalid_length, message: "..." }]
|
|
19
|
+
# errors.messages #=> ["Expected 12 characters, got 5"]
|
|
20
|
+
class Errors
|
|
21
|
+
# @return [Array<Hash{Symbol => Symbol, String}>] array of error hashes with :error and :message keys
|
|
22
|
+
attr_reader :details
|
|
23
|
+
|
|
24
|
+
# @param errors [Array<Hash{Symbol => Symbol, String}>] array of error hashes
|
|
25
|
+
def initialize(errors)
|
|
26
|
+
@details = errors.freeze
|
|
27
|
+
freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [Array<String>] human-readable error messages
|
|
31
|
+
def messages
|
|
32
|
+
@details.map { |e| e[:message] }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [Boolean] true when there are errors
|
|
36
|
+
def any?
|
|
37
|
+
!@details.empty?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [Boolean] true when there are no errors
|
|
41
|
+
def empty?
|
|
42
|
+
@details.empty?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @!method none?
|
|
46
|
+
# @return [Boolean] true when there are no errors
|
|
47
|
+
alias none? empty?
|
|
48
|
+
|
|
49
|
+
# @return [Integer] number of errors
|
|
50
|
+
def size
|
|
51
|
+
@details.size
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Yields each error detail hash to the block.
|
|
55
|
+
#
|
|
56
|
+
# @yieldparam detail [Hash{Symbol => Symbol, String}]
|
|
57
|
+
# @return [Enumerator, self]
|
|
58
|
+
def each(&)
|
|
59
|
+
@details.each(&)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [Array<String>] alias for {#messages}
|
|
63
|
+
def to_a
|
|
64
|
+
messages
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/sec_id/figi.rb
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
module SecId
|
|
3
|
+
module SecID
|
|
6
4
|
# Financial Instrument Global Identifier (FIGI) - a 12-character alphanumeric code
|
|
7
5
|
# that uniquely identifies financial instruments.
|
|
8
6
|
#
|
|
@@ -12,13 +10,18 @@ module SecId
|
|
|
12
10
|
# @see https://en.wikipedia.org/wiki/Financial_Instrument_Global_Identifier
|
|
13
11
|
#
|
|
14
12
|
# @example Validate a FIGI
|
|
15
|
-
#
|
|
13
|
+
# SecID::FIGI.valid?('BBG000BLNQ16') #=> true
|
|
16
14
|
#
|
|
17
15
|
# @example Restore check digit
|
|
18
|
-
#
|
|
16
|
+
# SecID::FIGI.restore!('BBG000BLNQ1') #=> #<SecID::FIGI>
|
|
19
17
|
class FIGI < Base
|
|
20
18
|
include Checkable
|
|
21
19
|
|
|
20
|
+
FULL_NAME = 'Financial Instrument Global Identifier'
|
|
21
|
+
ID_LENGTH = 12
|
|
22
|
+
EXAMPLE = 'BBG000BLNNH6'
|
|
23
|
+
VALID_CHARS_REGEX = /\A[B-DF-HJ-NP-TV-Z0-9]+\z/
|
|
24
|
+
|
|
22
25
|
# Regular expression for parsing FIGI components.
|
|
23
26
|
# The third character must be 'G'. Excludes vowels from valid characters.
|
|
24
27
|
ID_REGEX = /\A
|
|
@@ -47,16 +50,33 @@ module SecId
|
|
|
47
50
|
@check_digit = figi_parts[:check_digit]&.to_i
|
|
48
51
|
end
|
|
49
52
|
|
|
50
|
-
# @return [Boolean]
|
|
51
|
-
def valid_format?
|
|
52
|
-
!identifier.nil? && !RESTRICTED_PREFIXES.include?(prefix)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
53
|
# @return [Integer] the calculated check digit (0-9)
|
|
56
54
|
# @raise [InvalidFormatError] if the FIGI format is invalid
|
|
57
55
|
def calculate_check_digit
|
|
58
56
|
validate_format_for_calculation!
|
|
59
57
|
mod10(luhn_sum_indexed(reversed_digits_single(identifier)))
|
|
60
58
|
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# @return [Boolean]
|
|
63
|
+
def valid_format?
|
|
64
|
+
!identifier.nil? && !RESTRICTED_PREFIXES.include?(prefix)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @return [Array<Symbol>]
|
|
68
|
+
def detect_errors
|
|
69
|
+
return [:invalid_prefix] if identifier && RESTRICTED_PREFIXES.include?(prefix)
|
|
70
|
+
|
|
71
|
+
super
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @param code [Symbol]
|
|
75
|
+
# @return [String]
|
|
76
|
+
def validation_message(code)
|
|
77
|
+
return "Prefix '#{prefix}' is restricted" if code == :invalid_prefix
|
|
78
|
+
|
|
79
|
+
super
|
|
80
|
+
end
|
|
61
81
|
end
|
|
62
82
|
end
|
data/lib/sec_id/fisn.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module SecID
|
|
4
4
|
# Financial Instrument Short Name (FISN) - a human-readable short name for financial
|
|
5
5
|
# instruments per ISO 18774.
|
|
6
6
|
#
|
|
@@ -13,14 +13,20 @@ module SecId
|
|
|
13
13
|
# @see https://en.wikipedia.org/wiki/ISO_18774
|
|
14
14
|
#
|
|
15
15
|
# @example Validate a FISN
|
|
16
|
-
#
|
|
17
|
-
#
|
|
16
|
+
# SecID::FISN.valid?('APPLE INC/SH') #=> true
|
|
17
|
+
# SecID::FISN.valid?('apple inc/sh') #=> true (normalized to uppercase)
|
|
18
18
|
#
|
|
19
19
|
# @example Access FISN components
|
|
20
|
-
# fisn =
|
|
20
|
+
# fisn = SecID::FISN.new('APPLE INC/SH')
|
|
21
21
|
# fisn.issuer #=> 'APPLE INC'
|
|
22
22
|
# fisn.description #=> 'SH'
|
|
23
23
|
class FISN < Base
|
|
24
|
+
FULL_NAME = 'Financial Instrument Short Name'
|
|
25
|
+
ID_LENGTH = (3..35)
|
|
26
|
+
EXAMPLE = 'APPLE INC/SH'
|
|
27
|
+
VALID_CHARS_REGEX = %r{\A[A-Z0-9 /]+\z}
|
|
28
|
+
SEPARATORS = /-/
|
|
29
|
+
|
|
24
30
|
# Regular expression for parsing FISN components.
|
|
25
31
|
# Issuer: 1-15 chars, Description: 1-19 chars, Total: max 35 chars
|
|
26
32
|
ID_REGEX = %r{\A
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
4
|
-
# Country-specific BBAN validation rules for IBAN
|
|
3
|
+
module SecID
|
|
4
|
+
# Country-specific BBAN validation rules for IBAN.
|
|
5
|
+
#
|
|
6
|
+
# @api private
|
|
5
7
|
# rubocop:disable Metrics/ModuleLength
|
|
6
8
|
module IBANCountryRules
|
|
7
9
|
# Country-specific BBAN rules for EU/EEA countries
|
data/lib/sec_id/iban.rb
CHANGED
|
@@ -2,25 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'iban/country_rules'
|
|
4
4
|
|
|
5
|
-
module
|
|
5
|
+
module SecID
|
|
6
6
|
# International Bank Account Number (IBAN) - an international standard for identifying
|
|
7
7
|
# bank accounts across national borders (ISO 13616).
|
|
8
8
|
#
|
|
9
9
|
# Format: 2-letter country code + 2-digit check digits + BBAN (Basic Bank Account Number, 11-30 chars)
|
|
10
|
-
# Note: Unlike other
|
|
10
|
+
# Note: Unlike other SecID identifiers, the check digits are in positions 3-4, not at the end.
|
|
11
11
|
#
|
|
12
12
|
# @see https://en.wikipedia.org/wiki/International_Bank_Account_Number
|
|
13
13
|
# @see https://www.iban.com/structure
|
|
14
14
|
#
|
|
15
15
|
# @example Validate an IBAN
|
|
16
|
-
#
|
|
16
|
+
# SecID::IBAN.valid?('DE89370400440532013000') #=> true
|
|
17
17
|
#
|
|
18
18
|
# @example Restore check digits
|
|
19
|
-
#
|
|
19
|
+
# SecID::IBAN.restore!('DE00370400440532013000') #=> #<SecID::IBAN>
|
|
20
20
|
class IBAN < Base
|
|
21
21
|
include Checkable
|
|
22
22
|
include IBANCountryRules
|
|
23
23
|
|
|
24
|
+
FULL_NAME = 'International Bank Account Number'
|
|
25
|
+
ID_LENGTH = (15..34)
|
|
26
|
+
EXAMPLE = 'GB29NWBK60161331926819'
|
|
27
|
+
VALID_CHARS_REGEX = /\A[A-Z0-9]+\z/
|
|
28
|
+
|
|
24
29
|
# Regular expression for parsing IBAN components.
|
|
25
30
|
# Note: Check digit positioning is handled in initialize, not in the regex.
|
|
26
31
|
ID_REGEX = /\A
|
|
@@ -60,6 +65,13 @@ module SecId
|
|
|
60
65
|
extract_bban_components if valid_format?
|
|
61
66
|
end
|
|
62
67
|
|
|
68
|
+
# @return [String]
|
|
69
|
+
# @raise [InvalidFormatError] if the IBAN format is invalid
|
|
70
|
+
def restore
|
|
71
|
+
cd = calculate_check_digit
|
|
72
|
+
"#{country_code}#{cd.to_s.rjust(2, '0')}#{bban}"
|
|
73
|
+
end
|
|
74
|
+
|
|
63
75
|
# @return [Integer] the calculated 2-digit check value (1-98)
|
|
64
76
|
# @raise [InvalidFormatError] if the IBAN format is invalid
|
|
65
77
|
def calculate_check_digit
|
|
@@ -67,13 +79,6 @@ module SecId
|
|
|
67
79
|
mod97(numeric_string_for_check)
|
|
68
80
|
end
|
|
69
81
|
|
|
70
|
-
# @return [Boolean]
|
|
71
|
-
def valid_format?
|
|
72
|
-
return false unless identifier
|
|
73
|
-
|
|
74
|
-
valid_bban_format?
|
|
75
|
-
end
|
|
76
|
-
|
|
77
82
|
# @return [Boolean]
|
|
78
83
|
def valid_bban_format?
|
|
79
84
|
return false unless bban
|
|
@@ -96,13 +101,40 @@ module SecId
|
|
|
96
101
|
|
|
97
102
|
# @return [String]
|
|
98
103
|
def to_s
|
|
99
|
-
return
|
|
104
|
+
return full_id unless check_digit
|
|
100
105
|
|
|
101
106
|
"#{country_code}#{check_digit.to_s.rjust(2, '0')}#{bban}"
|
|
102
107
|
end
|
|
103
108
|
|
|
104
109
|
private
|
|
105
110
|
|
|
111
|
+
# @return [Integer]
|
|
112
|
+
def check_digit_width
|
|
113
|
+
2
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# @return [Boolean]
|
|
117
|
+
def valid_format?
|
|
118
|
+
return false unless identifier
|
|
119
|
+
|
|
120
|
+
valid_bban_format?
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# @return [Array<Symbol>]
|
|
124
|
+
def detect_errors
|
|
125
|
+
return [:invalid_bban] if identifier && !valid_bban_format?
|
|
126
|
+
|
|
127
|
+
super
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# @param code [Symbol]
|
|
131
|
+
# @return [String]
|
|
132
|
+
def validation_message(code)
|
|
133
|
+
return "BBAN format is invalid for country '#{country_code}'" if code == :invalid_bban
|
|
134
|
+
|
|
135
|
+
super
|
|
136
|
+
end
|
|
137
|
+
|
|
106
138
|
# @param rest [String] the IBAN string after country code
|
|
107
139
|
# @return [void]
|
|
108
140
|
def extract_check_digit_and_bban(rest)
|
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>
|
|
@@ -148,9 +153,9 @@ module SecId
|
|
|
148
153
|
|
|
149
154
|
case nsin_type
|
|
150
155
|
when :cusip then to_cusip
|
|
151
|
-
when :sedol then
|
|
152
|
-
when :wkn then
|
|
153
|
-
when :valoren then
|
|
156
|
+
when :sedol then to_sedol
|
|
157
|
+
when :wkn then to_wkn
|
|
158
|
+
when :valoren then to_valoren
|
|
154
159
|
else nsin # :generic - return raw string
|
|
155
160
|
end
|
|
156
161
|
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>
|
|
@@ -52,15 +57,13 @@ module SecId
|
|
|
52
57
|
mod97("#{numeric_identifier}00")
|
|
53
58
|
end
|
|
54
59
|
|
|
55
|
-
|
|
56
|
-
def to_s
|
|
57
|
-
return full_number unless check_digit
|
|
60
|
+
private
|
|
58
61
|
|
|
59
|
-
|
|
62
|
+
# @return [Integer]
|
|
63
|
+
def check_digit_width
|
|
64
|
+
2
|
|
60
65
|
end
|
|
61
66
|
|
|
62
|
-
private
|
|
63
|
-
|
|
64
67
|
# @return [String] the numeric string representation
|
|
65
68
|
def numeric_identifier
|
|
66
69
|
identifier.each_char.map { |char| CHAR_TO_DIGIT.fetch(char) }.join
|