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/base.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module SecID
|
|
4
4
|
# Base class for securities identifiers that provides a common interface
|
|
5
|
-
# for validation and parsing.
|
|
5
|
+
# for validation, normalization, and parsing.
|
|
6
6
|
#
|
|
7
7
|
# Subclasses must implement:
|
|
8
8
|
# - ID_REGEX constant with named capture groups for parsing
|
|
@@ -39,24 +39,20 @@ module SecId
|
|
|
39
39
|
# end
|
|
40
40
|
# end
|
|
41
41
|
class Base
|
|
42
|
+
include IdentifierMetadata
|
|
43
|
+
include Normalizable
|
|
44
|
+
include Validatable
|
|
45
|
+
|
|
42
46
|
# @return [String] the original input after normalization (stripped and uppercased)
|
|
43
|
-
attr_reader :
|
|
47
|
+
attr_reader :full_id
|
|
44
48
|
|
|
45
49
|
# @return [String, nil] the main identifier portion (without check digit)
|
|
46
50
|
attr_reader :identifier
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
new(id).valid?
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# @param id [String] the identifier to check
|
|
56
|
-
# @return [Boolean]
|
|
57
|
-
def valid_format?(id)
|
|
58
|
-
new(id).valid_format?
|
|
59
|
-
end
|
|
52
|
+
# @api private
|
|
53
|
+
def self.inherited(subclass)
|
|
54
|
+
super
|
|
55
|
+
SecID.__send__(:register_identifier, subclass) if subclass.name&.start_with?('SecID::')
|
|
60
56
|
end
|
|
61
57
|
|
|
62
58
|
# Subclasses must override this method.
|
|
@@ -67,33 +63,51 @@ module SecId
|
|
|
67
63
|
raise NotImplementedError
|
|
68
64
|
end
|
|
69
65
|
|
|
66
|
+
# @param other [Object]
|
|
70
67
|
# @return [Boolean]
|
|
71
|
-
def
|
|
72
|
-
|
|
68
|
+
def ==(other)
|
|
69
|
+
other.class == self.class && comparison_id == other.comparison_id
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
alias eql? ==
|
|
73
|
+
|
|
74
|
+
# @return [Integer]
|
|
75
|
+
def hash
|
|
76
|
+
[self.class, comparison_id].hash
|
|
73
77
|
end
|
|
74
78
|
|
|
75
|
-
#
|
|
79
|
+
# Returns a hash representation of this identifier for serialization.
|
|
76
80
|
#
|
|
77
|
-
# @return [
|
|
78
|
-
def
|
|
79
|
-
|
|
81
|
+
# @return [Hash] hash with :type, :full_id, :normalized, :valid, and :components keys
|
|
82
|
+
def to_h
|
|
83
|
+
{
|
|
84
|
+
type: self.class.short_name.downcase.to_sym,
|
|
85
|
+
full_id: full_id,
|
|
86
|
+
normalized: valid? ? normalized : nil,
|
|
87
|
+
valid: valid?,
|
|
88
|
+
components: components
|
|
89
|
+
}
|
|
80
90
|
end
|
|
81
91
|
|
|
92
|
+
protected
|
|
93
|
+
|
|
82
94
|
# @return [String]
|
|
83
|
-
def
|
|
84
|
-
|
|
95
|
+
def comparison_id
|
|
96
|
+
valid? ? normalized : full_id
|
|
85
97
|
end
|
|
86
|
-
alias to_str to_s
|
|
87
98
|
|
|
88
99
|
private
|
|
89
100
|
|
|
101
|
+
# @return [Hash]
|
|
102
|
+
def components
|
|
103
|
+
{}
|
|
104
|
+
end
|
|
105
|
+
|
|
90
106
|
# @param sec_id_number [String, #to_s] the identifier to parse
|
|
91
|
-
# @param upcase [Boolean] whether to upcase the input
|
|
92
107
|
# @return [MatchData, Hash] the regex match data or empty hash if no match
|
|
93
|
-
def parse(sec_id_number
|
|
94
|
-
@
|
|
95
|
-
@
|
|
96
|
-
@full_number.match(self.class::ID_REGEX) || {}
|
|
108
|
+
def parse(sec_id_number)
|
|
109
|
+
@full_id = sec_id_number.to_s.strip.upcase
|
|
110
|
+
@full_id.match(self.class::ID_REGEX) || {}
|
|
97
111
|
end
|
|
98
112
|
end
|
|
99
113
|
end
|
data/lib/sec_id/cei.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module SecID
|
|
4
4
|
# CUSIP Entity Identifier (CEI) - a 10-character alphanumeric code that identifies
|
|
5
5
|
# legal entities in the syndicated loan market.
|
|
6
6
|
#
|
|
@@ -9,10 +9,15 @@ module SecId
|
|
|
9
9
|
# @see https://www.cusip.com/identifiers.html
|
|
10
10
|
#
|
|
11
11
|
# @example Validate a CEI
|
|
12
|
-
#
|
|
12
|
+
# SecID::CEI.valid?('A0BCDEFGH1') #=> true
|
|
13
13
|
class CEI < Base
|
|
14
14
|
include Checkable
|
|
15
15
|
|
|
16
|
+
FULL_NAME = 'CUSIP Entity Identifier'
|
|
17
|
+
ID_LENGTH = 10
|
|
18
|
+
EXAMPLE = 'A0BCDEFGH1'
|
|
19
|
+
VALID_CHARS_REGEX = /\A[A-Z0-9]+\z/
|
|
20
|
+
|
|
16
21
|
# Regular expression for parsing CEI components.
|
|
17
22
|
ID_REGEX = /\A
|
|
18
23
|
(?<identifier>
|
|
@@ -41,6 +46,13 @@ module SecId
|
|
|
41
46
|
@check_digit = cei_parts[:check_digit]&.to_i
|
|
42
47
|
end
|
|
43
48
|
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# @return [Hash]
|
|
52
|
+
def components = { prefix:, numeric:, entity_id:, check_digit: }
|
|
53
|
+
|
|
54
|
+
public
|
|
55
|
+
|
|
44
56
|
# @return [Integer] the calculated check digit (0-9)
|
|
45
57
|
# @raise [InvalidFormatError] if the CEI format is invalid
|
|
46
58
|
def calculate_check_digit
|
data/lib/sec_id/cfi.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module SecID
|
|
4
4
|
# Classification of Financial Instruments (CFI) - a 6-character alphabetic code
|
|
5
5
|
# that classifies financial instruments per ISO 10962.
|
|
6
6
|
#
|
|
@@ -12,15 +12,20 @@ module SecId
|
|
|
12
12
|
# @see https://en.wikipedia.org/wiki/ISO_10962
|
|
13
13
|
#
|
|
14
14
|
# @example Validate a CFI code
|
|
15
|
-
#
|
|
16
|
-
#
|
|
15
|
+
# SecID::CFI.valid?('ESXXXX') #=> true
|
|
16
|
+
# SecID::CFI.valid?('ESVUFR') #=> true
|
|
17
17
|
#
|
|
18
18
|
# @example Access CFI components
|
|
19
|
-
# cfi =
|
|
19
|
+
# cfi = SecID::CFI.new('ESVUFR')
|
|
20
20
|
# cfi.category #=> :equity
|
|
21
21
|
# cfi.group #=> :common_shares
|
|
22
22
|
# cfi.voting? #=> true
|
|
23
23
|
class CFI < Base
|
|
24
|
+
FULL_NAME = 'Classification of Financial Instruments'
|
|
25
|
+
ID_LENGTH = 6
|
|
26
|
+
EXAMPLE = 'ESVUFR'
|
|
27
|
+
VALID_CHARS_REGEX = /\A[A-Z]+\z/
|
|
28
|
+
|
|
24
29
|
# Regular expression for parsing CFI components.
|
|
25
30
|
ID_REGEX = /\A
|
|
26
31
|
(?<identifier>
|
|
@@ -191,13 +196,6 @@ module SecId
|
|
|
191
196
|
@attr4 = cfi_parts[:attr4]
|
|
192
197
|
end
|
|
193
198
|
|
|
194
|
-
# Validates format including category and group codes.
|
|
195
|
-
#
|
|
196
|
-
# @return [Boolean]
|
|
197
|
-
def valid_format?
|
|
198
|
-
super && valid_category? && valid_group?
|
|
199
|
-
end
|
|
200
|
-
|
|
201
199
|
# Returns the semantic category name.
|
|
202
200
|
#
|
|
203
201
|
# @return [Symbol, nil] category symbol or nil if invalid
|
|
@@ -301,6 +299,37 @@ module SecId
|
|
|
301
299
|
|
|
302
300
|
private
|
|
303
301
|
|
|
302
|
+
# @return [Hash]
|
|
303
|
+
def components = { category_code:, group_code:, attr1:, attr2:, attr3:, attr4: }
|
|
304
|
+
|
|
305
|
+
# @return [Boolean]
|
|
306
|
+
def valid_format?
|
|
307
|
+
super && valid_category? && valid_group?
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# @return [Array<Symbol>]
|
|
311
|
+
def detect_errors
|
|
312
|
+
return super unless identifier
|
|
313
|
+
|
|
314
|
+
errors = []
|
|
315
|
+
errors << :invalid_category unless valid_category?
|
|
316
|
+
errors << :invalid_group unless valid_group?
|
|
317
|
+
errors
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# @param code [Symbol]
|
|
321
|
+
# @return [String]
|
|
322
|
+
def validation_message(code)
|
|
323
|
+
case code
|
|
324
|
+
when :invalid_category
|
|
325
|
+
"Category '#{category_code}' is not a valid CFI category"
|
|
326
|
+
when :invalid_group
|
|
327
|
+
"Group '#{group_code}' is not valid for category '#{category_code}'"
|
|
328
|
+
else
|
|
329
|
+
super
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
304
333
|
# @return [Boolean]
|
|
305
334
|
def valid_category?
|
|
306
335
|
CATEGORIES.key?(category_code)
|
|
@@ -308,7 +337,7 @@ module SecId
|
|
|
308
337
|
|
|
309
338
|
# @return [Boolean]
|
|
310
339
|
def valid_group?
|
|
311
|
-
GROUPS.dig(category_code, group_code)
|
|
340
|
+
!GROUPS.dig(category_code, group_code).nil?
|
|
312
341
|
end
|
|
313
342
|
end
|
|
314
343
|
end
|
data/lib/sec_id/cik.rb
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module SecID
|
|
4
4
|
# Central Index Key (CIK) - SEC identifier for entities filing with the SEC.
|
|
5
5
|
# A 1-10 digit number that uniquely identifies entities in SEC systems.
|
|
6
6
|
#
|
|
7
|
-
# @note CIK identifiers have no check digit
|
|
8
|
-
# returns false and validation is based solely on format.
|
|
7
|
+
# @note CIK identifiers have no check digit and validation is based solely on format.
|
|
9
8
|
#
|
|
10
9
|
# @see https://en.wikipedia.org/wiki/Central_Index_Key
|
|
11
10
|
#
|
|
12
11
|
# @example Validate a CIK
|
|
13
|
-
#
|
|
14
|
-
#
|
|
12
|
+
# SecID::CIK.valid?('0001521365') #=> true
|
|
13
|
+
# SecID::CIK.valid?('1521365') #=> true
|
|
15
14
|
#
|
|
16
15
|
# @example Normalize a CIK to 10 digits
|
|
17
|
-
#
|
|
16
|
+
# SecID::CIK.normalize('1521365') #=> '0001521365'
|
|
18
17
|
class CIK < Base
|
|
19
|
-
|
|
18
|
+
FULL_NAME = 'Central Index Key'
|
|
19
|
+
ID_LENGTH = (1..10)
|
|
20
|
+
EXAMPLE = '0001521365'
|
|
21
|
+
VALID_CHARS_REGEX = /\A[0-9]+\z/
|
|
20
22
|
|
|
21
23
|
# Regular expression for parsing CIK components.
|
|
22
24
|
ID_REGEX = /\A
|
|
@@ -33,23 +35,24 @@ module SecId
|
|
|
33
35
|
@identifier = cik_parts[:identifier]
|
|
34
36
|
end
|
|
35
37
|
|
|
36
|
-
# Normalizes the CIK to a 10-digit zero-padded format.
|
|
37
|
-
# Updates both @full_number and @padding to reflect the normalized state.
|
|
38
|
-
#
|
|
39
38
|
# @return [String] the normalized 10-digit CIK
|
|
40
|
-
# @raise [InvalidFormatError]
|
|
41
|
-
def
|
|
42
|
-
|
|
39
|
+
# @raise [InvalidFormatError]
|
|
40
|
+
def normalized
|
|
41
|
+
validate!
|
|
42
|
+
@identifier.rjust(self.class::ID_LENGTH.max, '0')
|
|
43
|
+
end
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
# @return [self]
|
|
46
|
+
# @raise [InvalidFormatError]
|
|
47
|
+
def normalize!
|
|
48
|
+
super
|
|
49
|
+
@padding = @full_id[0, self.class::ID_LENGTH.max - @identifier.length]
|
|
50
|
+
self
|
|
47
51
|
end
|
|
48
52
|
|
|
49
53
|
# @return [String]
|
|
50
54
|
def to_s
|
|
51
|
-
|
|
55
|
+
full_id
|
|
52
56
|
end
|
|
53
|
-
alias to_str to_s
|
|
54
57
|
end
|
|
55
58
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module SecID
|
|
4
4
|
# Provides check-digit validation and calculation for securities identifiers.
|
|
5
5
|
# Include this module in classes that have a check digit as part of their format.
|
|
6
6
|
#
|
|
@@ -10,10 +10,11 @@ module SecId
|
|
|
10
10
|
# This module provides:
|
|
11
11
|
# - Character-to-digit mapping constants
|
|
12
12
|
# - Luhn algorithm variants for check-digit calculation
|
|
13
|
-
# - `valid?` override that validates
|
|
14
|
-
# - `restore
|
|
13
|
+
# - `valid?` override that validates format and check digit
|
|
14
|
+
# - `restore` method returning full identifier string without mutation
|
|
15
|
+
# - `restore!` method to calculate and set the check digit, returning `self`
|
|
15
16
|
# - `check_digit` attribute
|
|
16
|
-
# - Class-level convenience methods: `restore!`, `check_digit`
|
|
17
|
+
# - Class-level convenience methods: `restore`, `restore!`, `check_digit`
|
|
17
18
|
#
|
|
18
19
|
# @example Including in an identifier class
|
|
19
20
|
# class MyIdentifier < Base
|
|
@@ -63,11 +64,20 @@ module SecId
|
|
|
63
64
|
|
|
64
65
|
# Class methods added when Checkable is included.
|
|
65
66
|
module ClassMethods
|
|
66
|
-
#
|
|
67
|
+
# Returns the full identifier string with correct check digit.
|
|
67
68
|
#
|
|
68
69
|
# @param id_without_check_digit [String] identifier without or with incorrect check digit
|
|
69
70
|
# @return [String] the full identifier with correct check digit
|
|
70
71
|
# @raise [InvalidFormatError] if the identifier format is invalid
|
|
72
|
+
def restore(id_without_check_digit)
|
|
73
|
+
new(id_without_check_digit).restore
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Restores (calculates) the check digit and returns the instance.
|
|
77
|
+
#
|
|
78
|
+
# @param id_without_check_digit [String] identifier without or with incorrect check digit
|
|
79
|
+
# @return [self] the restored instance with correct check digit
|
|
80
|
+
# @raise [InvalidFormatError] if the identifier format is invalid
|
|
71
81
|
def restore!(id_without_check_digit)
|
|
72
82
|
new(id_without_check_digit).restore!
|
|
73
83
|
end
|
|
@@ -84,18 +94,25 @@ module SecId
|
|
|
84
94
|
#
|
|
85
95
|
# @return [Boolean]
|
|
86
96
|
def valid?
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
check_digit == calculate_check_digit
|
|
97
|
+
super && check_digit == calculate_check_digit
|
|
90
98
|
end
|
|
91
99
|
|
|
92
|
-
#
|
|
100
|
+
# Returns the full identifier string with correct check digit without mutation.
|
|
93
101
|
#
|
|
94
102
|
# @return [String] the full identifier with correct check digit
|
|
95
103
|
# @raise [InvalidFormatError] if the identifier format is invalid
|
|
104
|
+
def restore
|
|
105
|
+
"#{identifier}#{calculate_check_digit.to_s.rjust(check_digit_width, '0')}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Calculates and sets the check digit, updating full_id.
|
|
109
|
+
#
|
|
110
|
+
# @return [self]
|
|
111
|
+
# @raise [InvalidFormatError] if the identifier format is invalid
|
|
96
112
|
def restore!
|
|
97
113
|
@check_digit = calculate_check_digit
|
|
98
|
-
@
|
|
114
|
+
@full_id = to_s
|
|
115
|
+
self
|
|
99
116
|
end
|
|
100
117
|
|
|
101
118
|
# Subclasses must override this method to implement their check-digit algorithm.
|
|
@@ -109,9 +126,10 @@ module SecId
|
|
|
109
126
|
|
|
110
127
|
# @return [String]
|
|
111
128
|
def to_s
|
|
112
|
-
"#{identifier}#{check_digit}"
|
|
129
|
+
"#{identifier}#{check_digit&.to_s&.rjust(check_digit_width, '0')}"
|
|
113
130
|
end
|
|
114
|
-
|
|
131
|
+
|
|
132
|
+
private
|
|
115
133
|
|
|
116
134
|
# CUSIP/CEI style: "Double Add Double" algorithm.
|
|
117
135
|
# Processes pairs of digits, doubling the first (even-positioned from right),
|
|
@@ -170,14 +188,38 @@ module SecId
|
|
|
170
188
|
id.each_char.flat_map { |c| CHAR_TO_DIGITS.fetch(c) }.reverse!
|
|
171
189
|
end
|
|
172
190
|
|
|
173
|
-
|
|
191
|
+
# Returns error codes including check digit validation.
|
|
192
|
+
#
|
|
193
|
+
# @return [Array<Symbol>]
|
|
194
|
+
def error_codes
|
|
195
|
+
return detect_errors unless valid_format?
|
|
196
|
+
return [:invalid_check_digit] unless check_digit == calculate_check_digit
|
|
197
|
+
|
|
198
|
+
[]
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# @return [Integer]
|
|
202
|
+
def check_digit_width
|
|
203
|
+
1
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# @param code [Symbol]
|
|
207
|
+
# @return [String]
|
|
208
|
+
def validation_message(code)
|
|
209
|
+
if code == :invalid_check_digit
|
|
210
|
+
fmt = ->(cd) { cd.to_s.rjust(check_digit_width, '0') }
|
|
211
|
+
return "Check digit '#{fmt[check_digit]}' is invalid, expected '#{fmt[calculate_check_digit]}'"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
super
|
|
215
|
+
end
|
|
174
216
|
|
|
175
217
|
# @raise [InvalidFormatError] if valid_format? returns false
|
|
176
218
|
# @return [void]
|
|
177
219
|
def validate_format_for_calculation!
|
|
178
220
|
return if valid_format?
|
|
179
221
|
|
|
180
|
-
raise InvalidFormatError, "#{self.class.name} '#{
|
|
222
|
+
raise InvalidFormatError, "#{self.class.name} '#{full_id}' is invalid and check-digit cannot be calculated!"
|
|
181
223
|
end
|
|
182
224
|
|
|
183
225
|
# @param sum [Integer] the sum to calculate check digit from
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SecID
|
|
4
|
+
# Provides class-level metadata methods for identifier types.
|
|
5
|
+
#
|
|
6
|
+
# Including classes must define constants: `FULL_NAME`, `ID_LENGTH`, `EXAMPLE`.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# SecID::ISIN.short_name #=> "ISIN"
|
|
10
|
+
# SecID::ISIN.full_name #=> "International Securities Identification Number"
|
|
11
|
+
# SecID::ISIN.has_check_digit? #=> true
|
|
12
|
+
module IdentifierMetadata
|
|
13
|
+
# @api private
|
|
14
|
+
def self.included(base)
|
|
15
|
+
base.extend(ClassMethods)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Class methods added when IdentifierMetadata is included.
|
|
19
|
+
module ClassMethods
|
|
20
|
+
# Returns the unqualified class name (e.g. "ISIN", "CUSIP").
|
|
21
|
+
#
|
|
22
|
+
# @return [String]
|
|
23
|
+
def short_name
|
|
24
|
+
@short_name ||= name.split('::').last
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Returns the full human-readable standard name.
|
|
28
|
+
#
|
|
29
|
+
# @return [String]
|
|
30
|
+
def full_name
|
|
31
|
+
self::FULL_NAME
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Returns the fixed length or valid length range for identifiers of this type.
|
|
35
|
+
#
|
|
36
|
+
# @return [Integer, Range]
|
|
37
|
+
def id_length
|
|
38
|
+
self::ID_LENGTH
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns a representative valid identifier string.
|
|
42
|
+
#
|
|
43
|
+
# @return [String]
|
|
44
|
+
def example
|
|
45
|
+
self::EXAMPLE
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [Boolean] true if this identifier type uses a check digit
|
|
49
|
+
def has_check_digit?
|
|
50
|
+
return @has_check_digit if defined?(@has_check_digit)
|
|
51
|
+
|
|
52
|
+
@has_check_digit = ancestors.include?(SecID::Checkable)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
4
|
-
# Provides
|
|
5
|
-
# Include this module in classes that implement an instance-level normalize! method.
|
|
3
|
+
module SecID
|
|
4
|
+
# Provides normalization methods for identifier types.
|
|
6
5
|
#
|
|
7
|
-
#
|
|
8
|
-
# class MyIdentifier < Base
|
|
9
|
-
# include Normalizable
|
|
10
|
-
#
|
|
11
|
-
# def normalize!
|
|
12
|
-
# # implementation
|
|
13
|
-
# end
|
|
14
|
-
# end
|
|
15
|
-
#
|
|
16
|
-
# MyIdentifier.normalize!('ABC123') #=> normalized string
|
|
6
|
+
# Including classes may override `SEPARATORS` (default `/[\s-]/`) and `#normalized`.
|
|
17
7
|
module Normalizable
|
|
8
|
+
SEPARATORS = /[\s-]/
|
|
9
|
+
|
|
18
10
|
# @api private
|
|
19
11
|
def self.included(base)
|
|
20
12
|
base.extend(ClassMethods)
|
|
@@ -26,10 +18,62 @@ module SecId
|
|
|
26
18
|
#
|
|
27
19
|
# @param id [String, #to_s] the identifier to normalize
|
|
28
20
|
# @return [String] the normalized identifier
|
|
29
|
-
# @raise [InvalidFormatError
|
|
30
|
-
def normalize
|
|
31
|
-
|
|
21
|
+
# @raise [InvalidFormatError, InvalidCheckDigitError, InvalidStructureError]
|
|
22
|
+
def normalize(id)
|
|
23
|
+
cleaned = id.to_s.strip.gsub(self::SEPARATORS, '')
|
|
24
|
+
new(cleaned.upcase).normalized
|
|
32
25
|
end
|
|
26
|
+
|
|
27
|
+
# Returns a human-readable formatted string, or nil if invalid.
|
|
28
|
+
#
|
|
29
|
+
# @param id [String, #to_s] the identifier to format
|
|
30
|
+
# @return [String, nil]
|
|
31
|
+
def to_pretty_s(id)
|
|
32
|
+
cleaned = id.to_s.strip.gsub(self::SEPARATORS, '')
|
|
33
|
+
new(cleaned.upcase).to_pretty_s
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns the canonical normalized form of this identifier.
|
|
38
|
+
#
|
|
39
|
+
# @return [String]
|
|
40
|
+
# @raise [InvalidFormatError, InvalidCheckDigitError, InvalidStructureError]
|
|
41
|
+
def normalized
|
|
42
|
+
validate!
|
|
43
|
+
to_s
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @!method normalize
|
|
47
|
+
# @return [String]
|
|
48
|
+
# @raise [InvalidFormatError, InvalidCheckDigitError, InvalidStructureError]
|
|
49
|
+
alias normalize normalized
|
|
50
|
+
|
|
51
|
+
# Normalizes this identifier in place, updating {#full_id}.
|
|
52
|
+
#
|
|
53
|
+
# @return [self]
|
|
54
|
+
# @raise [InvalidFormatError, InvalidCheckDigitError, InvalidStructureError]
|
|
55
|
+
def normalize!
|
|
56
|
+
@full_id = normalized
|
|
57
|
+
self
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Returns a human-readable formatted string, or nil if invalid.
|
|
61
|
+
#
|
|
62
|
+
# @return [String, nil]
|
|
63
|
+
def to_pretty_s
|
|
64
|
+
return nil unless valid?
|
|
65
|
+
|
|
66
|
+
to_s
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @return [String]
|
|
70
|
+
def to_s
|
|
71
|
+
identifier.to_s
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @return [String]
|
|
75
|
+
def to_str
|
|
76
|
+
to_s
|
|
33
77
|
end
|
|
34
78
|
end
|
|
35
79
|
end
|