sec_id 4.0.0 → 4.2.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 +31 -0
- data/README.md +86 -4
- data/lib/sec_id/cik.rb +59 -0
- data/lib/sec_id/cusip.rb +24 -10
- data/lib/sec_id/figi.rb +53 -0
- data/lib/sec_id/isin.rb +27 -10
- data/lib/sec_id/occ.rb +103 -0
- data/lib/sec_id/sedol.rb +4 -2
- data/lib/sec_id/version.rb +1 -1
- data/lib/sec_id.rb +3 -0
- data/sec_id.gemspec +2 -4
- metadata +7 -16
- data/.github/dependabot.yml +0 -11
- data/.github/workflows/main.yml +0 -39
- data/.gitignore +0 -12
- data/.rspec +0 -3
- data/.rubocop.yml +0 -39
- data/Gemfile +0 -20
- data/Rakefile +0 -13
- data/bin/console +0 -15
- data/bin/setup +0 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 58a56c99692f66ae9ff3be10064ab29e729f34d6877456a6c998ee1a3509478d
|
|
4
|
+
data.tar.gz: ed1ead1827a544b8c48894739916781ea62337aa75e26f7263dd54c08f70b074
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 437a5b95d0e14cead4d0392dbaa2da4fbc7c3325a01252f17032ca50e9dc3609a6dfe8e73947535de2e2fe228c460c33847635f90c0d99fddf6ff32c66117fbb
|
|
7
|
+
data.tar.gz: d2e44281f824c033add9074633432291794789b4f1b89e50b5977b1dfd34efd68e5dd2883c7c9076cf5e30316b6a8e487853185824f988827cad61e44e2c38c9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [4.2.0] - 2025-01-12
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- OCC support ([@wtn](https://github.com/wtn), [#93](https://github.com/svyatov/sec_id/pull/93))
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- CUSIP#cins? usage example in README ([@wtn](https://github.com/wtn), [#91](https://github.com/svyatov/sec_id/pull/91))
|
|
12
|
+
|
|
13
|
+
### Updated
|
|
14
|
+
|
|
15
|
+
- Separate CIK from Base for cleaner architecture ([@wtn](https://github.com/wtn), [#92](https://github.com/svyatov/sec_id/pull/92))
|
|
16
|
+
- Use rubocop-rspec plugin ([@wtn](https://github.com/wtn), [#90](https://github.com/svyatov/sec_id/pull/90))
|
|
17
|
+
- Replace CodeClimate with Codecov
|
|
18
|
+
- Add permissions to CI workflow
|
|
19
|
+
- Clean up gemspec: update description and simplify files list
|
|
20
|
+
|
|
21
|
+
## [4.1.0] - 2024-09-23
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- FIGI support ([@wtn](https://github.com/wtn), [#84](https://github.com/svyatov/sec_id/pull/84))
|
|
26
|
+
- CIK support ([@wtn](https://github.com/wtn), [#85](https://github.com/svyatov/sec_id/pull/85))
|
|
27
|
+
- Convert between CUSIPs and ISINs ([@wtn](https://github.com/wtn), [#86](https://github.com/svyatov/sec_id/pull/86), [#88](https://github.com/svyatov/sec_id/pull/88))
|
|
28
|
+
- CINS check method for CUSIPs ([@wtn](https://github.com/wtn), [#87](https://github.com/svyatov/sec_id/pull/87))
|
|
29
|
+
|
|
30
|
+
### Updated
|
|
31
|
+
|
|
32
|
+
- Small internal refactorings
|
|
33
|
+
|
|
3
34
|
## [4.0.0] - 2024-07-09
|
|
4
35
|
|
|
5
36
|
### Breaking changes
|
data/README.md
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# SecId
|
|
2
2
|
[](https://badge.fury.io/rb/sec_id)
|
|
3
|
+
[](https://codecov.io/gh/svyatov/sec_id)
|
|
3
4
|

|
|
4
|
-
[](https://codeclimate.com/github/svyatov/sec_id/maintainability)
|
|
5
|
-
[](https://codeclimate.com/github/svyatov/sec_id/test_coverage)
|
|
6
5
|
|
|
7
6
|
Validate securities identification numbers with ease!
|
|
8
7
|
|
|
@@ -11,7 +10,10 @@ Check-digit calculation is also available.
|
|
|
11
10
|
Currently supported standards:
|
|
12
11
|
[ISIN](https://en.wikipedia.org/wiki/International_Securities_Identification_Number),
|
|
13
12
|
[CUSIP](https://en.wikipedia.org/wiki/CUSIP),
|
|
14
|
-
[SEDOL](https://en.wikipedia.org/wiki/SEDOL)
|
|
13
|
+
[SEDOL](https://en.wikipedia.org/wiki/SEDOL),
|
|
14
|
+
[FIGI](https://en.wikipedia.org/wiki/Financial_Instrument_Global_Identifier),
|
|
15
|
+
[CIK](https://en.wikipedia.org/wiki/Central_Index_Key),
|
|
16
|
+
[OCC](https://en.wikipedia.org/wiki/Option_symbol#The_OCC_Option_Symbol).
|
|
15
17
|
|
|
16
18
|
Work in progress:
|
|
17
19
|
[IBAN](https://en.wikipedia.org/wiki/International_Bank_Account_Number).
|
|
@@ -21,7 +23,7 @@ Work in progress:
|
|
|
21
23
|
Add this line to your application's Gemfile:
|
|
22
24
|
|
|
23
25
|
```ruby
|
|
24
|
-
gem 'sec_id', '~> 4.
|
|
26
|
+
gem 'sec_id', '~> 4.2'
|
|
25
27
|
```
|
|
26
28
|
|
|
27
29
|
And then execute:
|
|
@@ -115,6 +117,7 @@ isin.valid? # => true
|
|
|
115
117
|
isin.valid_format? # => true
|
|
116
118
|
isin.restore! # => 'US5949181045'
|
|
117
119
|
isin.calculate_check_digit # => 5
|
|
120
|
+
isin.to_cusip # => #<SecId::CUSIP>
|
|
118
121
|
```
|
|
119
122
|
|
|
120
123
|
### SecId::CUSIP full example
|
|
@@ -136,6 +139,8 @@ cusip.valid? # => true
|
|
|
136
139
|
cusip.valid_format? # => true
|
|
137
140
|
cusip.restore! # => '594918104'
|
|
138
141
|
cusip.calculate_check_digit # => 4
|
|
142
|
+
cusip.to_isin('US') # => #<SecId::ISIN>
|
|
143
|
+
cusip.cins? # => false
|
|
139
144
|
```
|
|
140
145
|
|
|
141
146
|
### SecId::SEDOL full example
|
|
@@ -157,6 +162,83 @@ cusip.restore! # => 'B0Z52W5'
|
|
|
157
162
|
cusip.calculate_check_digit # => 5
|
|
158
163
|
```
|
|
159
164
|
|
|
165
|
+
### SecId::FIGI full example
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
# class level
|
|
169
|
+
SecId::FIGI.valid?('BBG000DMBXR2') # => true
|
|
170
|
+
SecId::FIGI.valid_format?('BBG000DMBXR2') # => true
|
|
171
|
+
SecId::FIGI.restore!('BBG000DMBXR') # => 'BBG000DMBXR2'
|
|
172
|
+
SecId::FIGI.check_digit('BBG000DMBXR') # => 2
|
|
173
|
+
|
|
174
|
+
# instance level
|
|
175
|
+
figi = SecId::FIGI.new('BBG000DMBXR2')
|
|
176
|
+
figi.full_number # => 'BBG000DMBXR2'
|
|
177
|
+
figi.prefix # => 'BB'
|
|
178
|
+
figi.random_part # => '000DMBXR'
|
|
179
|
+
figi.check_digit # => 2
|
|
180
|
+
figi.valid? # => true
|
|
181
|
+
figi.valid_format? # => true
|
|
182
|
+
figi.restore! # => 'BBG000DMBXR2'
|
|
183
|
+
figi.calculate_check_digit # => 2
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### SecId::CIK full example
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
# class level
|
|
190
|
+
SecId::CIK.valid?('0001094517') # => true
|
|
191
|
+
SecId::CIK.valid_format?('0001094517') # => true
|
|
192
|
+
SecId::CIK.normalize!('1094517') # => '0001094517'
|
|
193
|
+
|
|
194
|
+
# instance level
|
|
195
|
+
cik = SecId::CIK.new('0001094517')
|
|
196
|
+
cik.full_number # => '0001094517'
|
|
197
|
+
cik.padding # => '000'
|
|
198
|
+
cik.identifier # => '1094517'
|
|
199
|
+
cik.valid? # => true
|
|
200
|
+
cik.valid_format? # => true
|
|
201
|
+
cik.normalize! # => '0001094517'
|
|
202
|
+
cik.to_s # => '0001094517'
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### SecId::OCC full example
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
# class level
|
|
209
|
+
SecId::OCC.valid?('BRKB 100417C00090000') # => true
|
|
210
|
+
SecId::OCC.valid_format?('BRKB 100417C00090000') # => true
|
|
211
|
+
SecId::OCC.normalize!('BRKB100417C00090000') # => 'BRKB 100417C00090000'
|
|
212
|
+
SecId::OCC.build(
|
|
213
|
+
underlying: 'BRKB',
|
|
214
|
+
date: Date.new(2010, 4, 17),
|
|
215
|
+
type: 'C',
|
|
216
|
+
strike: 90,
|
|
217
|
+
) # => #<SecId::OCC>
|
|
218
|
+
|
|
219
|
+
# instance level
|
|
220
|
+
occ = SecId::OCC.new('BRKB 100417C00090000')
|
|
221
|
+
occ.full_symbol # => 'BRKB 100417C00090000'
|
|
222
|
+
occ.underlying # => 'BRKB'
|
|
223
|
+
occ.date_str # => '100417'
|
|
224
|
+
occ.date_obj # => #<Date: 2010-04-17>
|
|
225
|
+
occ.type # => 'C'
|
|
226
|
+
occ.strike # => 90.0
|
|
227
|
+
occ.valid? # => true
|
|
228
|
+
occ.valid_format? # => true
|
|
229
|
+
occ.normalize! # => 'BRKB 100417C00090000'
|
|
230
|
+
|
|
231
|
+
occ = SecId::OCC.new('BRKB 2010-04-17C00090000')
|
|
232
|
+
occ.valid_format? # => false
|
|
233
|
+
occ.normalize! # raises SecId::InvalidFormatError
|
|
234
|
+
|
|
235
|
+
occ = SecId::OCC.new('X 250620C00050000')
|
|
236
|
+
occ.full_symbol # => 'X 250620C00050000'
|
|
237
|
+
occ.valid? # => true
|
|
238
|
+
occ.normalize! # => 'X 250620C00050000'
|
|
239
|
+
occ.full_symbol # => 'X 250620C00050000'
|
|
240
|
+
```
|
|
241
|
+
|
|
160
242
|
## Development
|
|
161
243
|
|
|
162
244
|
After checking out the repo, run `bin/setup` to install dependencies.
|
data/lib/sec_id/cik.rb
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SecId
|
|
4
|
+
# https://en.wikipedia.org/wiki/Central_Index_Key
|
|
5
|
+
class CIK
|
|
6
|
+
ID_REGEX = /\A
|
|
7
|
+
(?=\d{1,10}\z)(?<padding>0*)(?<identifier>[1-9]\d{0,9})
|
|
8
|
+
\z/x
|
|
9
|
+
|
|
10
|
+
attr_reader :full_number, :identifier, :padding
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def valid?(id)
|
|
14
|
+
new(id).valid?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def valid_format?(id)
|
|
18
|
+
new(id).valid_format?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def normalize!(id)
|
|
22
|
+
new(id).normalize!
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(cik)
|
|
27
|
+
cik_parts = parse(cik)
|
|
28
|
+
@padding = cik_parts[:padding]
|
|
29
|
+
@identifier = cik_parts[:identifier]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def valid?
|
|
33
|
+
valid_format?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def valid_format?
|
|
37
|
+
!identifier.nil?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def normalize!
|
|
41
|
+
raise InvalidFormatError, "CIK '#{full_number}' is invalid and cannot be normalized!" unless valid_format?
|
|
42
|
+
|
|
43
|
+
@padding = '0' * (10 - @identifier.length)
|
|
44
|
+
@full_number = @identifier.rjust(10, '0')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_s
|
|
48
|
+
full_number
|
|
49
|
+
end
|
|
50
|
+
alias to_str to_s
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def parse(cik_number)
|
|
55
|
+
@full_number = cik_number.to_s.strip
|
|
56
|
+
@full_number.match(ID_REGEX) || {}
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/sec_id/cusip.rb
CHANGED
|
@@ -21,27 +21,41 @@ module SecId
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def calculate_check_digit
|
|
24
|
-
|
|
24
|
+
unless valid_format?
|
|
25
|
+
raise InvalidFormatError, "CUSIP '#{full_number}' is invalid and check-digit cannot be calculated!"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
mod10(modified_luhn_sum)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_isin(country_code)
|
|
32
|
+
unless ISIN::CGS_COUNTRY_CODES.include?(country_code)
|
|
33
|
+
raise(InvalidFormatError, "'#{country_code}' is not a CGS country code!")
|
|
34
|
+
end
|
|
25
35
|
|
|
26
|
-
|
|
36
|
+
restore!
|
|
37
|
+
isin = ISIN.new(country_code + full_number)
|
|
38
|
+
isin.restore!
|
|
39
|
+
isin
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# CUSIP International Numbering System
|
|
43
|
+
def cins?
|
|
44
|
+
cusip6[0] < '0' || cusip6[0] > '9'
|
|
27
45
|
end
|
|
28
46
|
|
|
29
47
|
private
|
|
30
48
|
|
|
31
49
|
# https://en.wikipedia.org/wiki/Luhn_algorithm
|
|
32
50
|
def modified_luhn_sum
|
|
33
|
-
sum
|
|
34
|
-
|
|
35
|
-
id_digits.reverse.each_slice(2) do |even, odd|
|
|
51
|
+
reversed_id_digits.each_slice(2).reduce(0) do |sum, (even, odd)|
|
|
36
52
|
double_even = (even || 0) * 2
|
|
37
|
-
sum
|
|
53
|
+
sum + div10mod10(double_even) + div10mod10(odd || 0)
|
|
38
54
|
end
|
|
39
|
-
|
|
40
|
-
sum
|
|
41
55
|
end
|
|
42
56
|
|
|
43
|
-
def
|
|
44
|
-
|
|
57
|
+
def reversed_id_digits
|
|
58
|
+
identifier.each_char.map(&method(:char_to_digit)).reverse!
|
|
45
59
|
end
|
|
46
60
|
end
|
|
47
61
|
end
|
data/lib/sec_id/figi.rb
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
module SecId
|
|
6
|
+
class FIGI < Base
|
|
7
|
+
ID_REGEX = /\A
|
|
8
|
+
(?<identifier>
|
|
9
|
+
(?<prefix>[B-DF-HJ-NP-TV-Z0-9]{2})
|
|
10
|
+
G
|
|
11
|
+
(?<random_part>[B-DF-HJ-NP-TV-Z0-9]{8}))
|
|
12
|
+
(?<check_digit>\d)?
|
|
13
|
+
\z/x
|
|
14
|
+
|
|
15
|
+
RESTRICTED_PREFIXES = Set.new %w[BS BM GG GB GH KY VG]
|
|
16
|
+
|
|
17
|
+
attr_reader :prefix, :random_part
|
|
18
|
+
|
|
19
|
+
def initialize(figi)
|
|
20
|
+
figi_parts = parse figi
|
|
21
|
+
@identifier = figi_parts[:identifier]
|
|
22
|
+
@prefix = figi_parts[:prefix]
|
|
23
|
+
@random_part = figi_parts[:random_part]
|
|
24
|
+
@check_digit = figi_parts[:check_digit]&.to_i
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def valid_format?
|
|
28
|
+
!identifier.nil? && !RESTRICTED_PREFIXES.include?(prefix)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def calculate_check_digit
|
|
32
|
+
unless valid_format?
|
|
33
|
+
raise InvalidFormatError, "FIGI '#{full_number}' is invalid and check-digit cannot be calculated!"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
mod10(modified_luhn_sum)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# https://en.wikipedia.org/wiki/Luhn_algorithm
|
|
42
|
+
def modified_luhn_sum
|
|
43
|
+
reversed_id_digits.each_with_index.reduce(0) do |sum, (digit, index)|
|
|
44
|
+
digit *= 2 if index.odd?
|
|
45
|
+
sum + digit.divmod(10).sum
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def reversed_id_digits
|
|
50
|
+
identifier.each_char.map(&method(:char_to_digit)).reverse!
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/sec_id/isin.rb
CHANGED
|
@@ -10,6 +10,14 @@ module SecId
|
|
|
10
10
|
(?<check_digit>\d)?
|
|
11
11
|
\z/x
|
|
12
12
|
|
|
13
|
+
CGS_COUNTRY_CODES = Set.new(
|
|
14
|
+
%w[
|
|
15
|
+
US CA AG AI AN AR AS AW BB BL BM BO BQ BR BS BZ CL CO CR CW DM DO EC FM
|
|
16
|
+
GD GS GU GY HN HT JM KN KY LC MF MH MP MX NI PA PE PH PR PW PY SR SV SX
|
|
17
|
+
TT UM UY VC VE VG VI YT
|
|
18
|
+
]
|
|
19
|
+
).freeze
|
|
20
|
+
|
|
13
21
|
attr_reader :country_code, :nsin
|
|
14
22
|
|
|
15
23
|
def initialize(isin)
|
|
@@ -21,28 +29,37 @@ module SecId
|
|
|
21
29
|
end
|
|
22
30
|
|
|
23
31
|
def calculate_check_digit
|
|
24
|
-
|
|
32
|
+
unless valid_format?
|
|
33
|
+
raise InvalidFormatError, "ISIN '#{full_number}' is invalid and check-digit cannot be calculated!"
|
|
34
|
+
end
|
|
25
35
|
|
|
26
|
-
|
|
36
|
+
mod10(luhn_sum)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# CUSIP Global Services
|
|
40
|
+
def cgs?
|
|
41
|
+
CGS_COUNTRY_CODES.include?(country_code)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_cusip
|
|
45
|
+
raise InvalidFormatError, "'#{country_code}' is not a CGS country code!" unless cgs?
|
|
46
|
+
|
|
47
|
+
CUSIP.new(nsin)
|
|
27
48
|
end
|
|
28
49
|
|
|
29
50
|
private
|
|
30
51
|
|
|
31
52
|
# https://en.wikipedia.org/wiki/Luhn_algorithm
|
|
32
53
|
def luhn_sum
|
|
33
|
-
sum
|
|
34
|
-
|
|
35
|
-
id_digits.reverse.each_slice(2) do |even, odd|
|
|
54
|
+
reversed_id_digits.each_slice(2).reduce(0) do |sum, (even, odd)|
|
|
36
55
|
double_even = (even || 0) * 2
|
|
37
56
|
double_even -= 9 if double_even > 9
|
|
38
|
-
sum
|
|
57
|
+
sum + double_even + (odd || 0)
|
|
39
58
|
end
|
|
40
|
-
|
|
41
|
-
sum
|
|
42
59
|
end
|
|
43
60
|
|
|
44
|
-
def
|
|
45
|
-
|
|
61
|
+
def reversed_id_digits
|
|
62
|
+
identifier.each_char.flat_map(&method(:char_to_digits)).reverse!
|
|
46
63
|
end
|
|
47
64
|
end
|
|
48
65
|
end
|
data/lib/sec_id/occ.rb
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
|
|
5
|
+
module SecId
|
|
6
|
+
# https://en.wikipedia.org/wiki/Option_symbol#The_OCC_Option_Symbol
|
|
7
|
+
# https://web.archive.org/web/20120507220143/http://www.theocc.com/components/docs/initiatives/symbology/symbology_initiative_v1_8.pdf
|
|
8
|
+
class OCC
|
|
9
|
+
ID_REGEX = /\A
|
|
10
|
+
(?<initial>
|
|
11
|
+
(?=.{1,6})(?<underlying>\d?[A-Z]{1,5}\d?)(?<padding>[ ]*))
|
|
12
|
+
(?<date>\d{6})
|
|
13
|
+
(?<type>[CP])
|
|
14
|
+
(?<strike_mills>\d{8})
|
|
15
|
+
\z/x
|
|
16
|
+
|
|
17
|
+
attr_reader :full_symbol, :underlying, :date_str, :type
|
|
18
|
+
|
|
19
|
+
def initialize(symbol)
|
|
20
|
+
symbol_parts = parse symbol
|
|
21
|
+
@initial = symbol_parts[:initial]
|
|
22
|
+
@underlying = symbol_parts[:underlying]
|
|
23
|
+
@date_str = symbol_parts[:date]
|
|
24
|
+
@type = symbol_parts[:type]
|
|
25
|
+
@strike_mills = symbol_parts[:strike_mills]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def date
|
|
29
|
+
return @date if @date
|
|
30
|
+
|
|
31
|
+
@date = Date.strptime(date_str, '%y%m%d') if date_str
|
|
32
|
+
rescue Date::Error
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
alias date_obj date
|
|
36
|
+
|
|
37
|
+
def strike
|
|
38
|
+
@strike ||= @strike_mills.to_i / 1000.0
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def valid?
|
|
42
|
+
valid_format? && !date.nil?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def valid_format?
|
|
46
|
+
!@initial.nil?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def normalize!
|
|
50
|
+
raise InvalidFormatError, "OCC '#{full_symbol}' is invalid and cannot be normalized!" unless valid?
|
|
51
|
+
|
|
52
|
+
@strike_mills.length > 8 && @strike_mills = format('%08d', @strike_mills.to_i)
|
|
53
|
+
@initial.length < 6 && @initial = underlying.ljust(6, "\s")
|
|
54
|
+
|
|
55
|
+
@full_symbol = "#{@initial}#{date_str}#{type}#{@strike_mills}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def to_s
|
|
59
|
+
full_symbol
|
|
60
|
+
end
|
|
61
|
+
alias to_str to_s
|
|
62
|
+
|
|
63
|
+
class << self
|
|
64
|
+
def valid?(id)
|
|
65
|
+
new(id).valid?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def valid_format?(id)
|
|
69
|
+
new(id).valid_format?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def normalize!(id)
|
|
73
|
+
new(id).normalize!
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# rubocop:disable Metrics/MethodLength
|
|
77
|
+
def build(underlying:, date:, type:, strike:)
|
|
78
|
+
initial = underlying.to_s.ljust(6, "\s")
|
|
79
|
+
date = Date.parse(date.to_s) unless date.is_a?(Date)
|
|
80
|
+
|
|
81
|
+
case strike
|
|
82
|
+
when Numeric
|
|
83
|
+
strike_mills = format('%08d', (strike * 1000).to_i)
|
|
84
|
+
when /\A\d{8}\z/
|
|
85
|
+
strike_mills = strike
|
|
86
|
+
else
|
|
87
|
+
raise ArgumentError, 'Strike must be numeric or an 8-char string!'
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
symbol = "#{initial}#{date.strftime('%y%m%d')}#{type}#{strike_mills}"
|
|
91
|
+
new(symbol)
|
|
92
|
+
end
|
|
93
|
+
# rubocop:enable Metrics/MethodLength
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def parse(symbol)
|
|
99
|
+
@full_symbol = symbol.to_s.strip
|
|
100
|
+
@full_symbol.match(ID_REGEX) || {}
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
data/lib/sec_id/sedol.rb
CHANGED
|
@@ -17,9 +17,11 @@ module SecId
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def calculate_check_digit
|
|
20
|
-
|
|
20
|
+
unless valid_format?
|
|
21
|
+
raise InvalidFormatError, "SEDOL '#{full_number}' is invalid and check-digit cannot be calculated!"
|
|
22
|
+
end
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
mod10(weighted_sum)
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
private
|
data/lib/sec_id/version.rb
CHANGED
data/lib/sec_id.rb
CHANGED
data/sec_id.gemspec
CHANGED
|
@@ -11,16 +11,14 @@ Gem::Specification.new do |spec|
|
|
|
11
11
|
spec.email = ['leonid@svyatov.ru']
|
|
12
12
|
|
|
13
13
|
spec.summary = 'Validate securities identification numbers with ease!'
|
|
14
|
-
spec.description = %(#{spec.summary} Currently supported standards: ISIN, CUSIP, SEDOL.)
|
|
14
|
+
spec.description = %(#{spec.summary} Currently supported standards: ISIN, CUSIP, SEDOL, FIGI, CIK, OCC.)
|
|
15
15
|
spec.homepage = 'https://github.com/svyatov/sec_id'
|
|
16
16
|
spec.license = 'MIT'
|
|
17
17
|
|
|
18
18
|
spec.required_ruby_version = '>= 3.1.0'
|
|
19
19
|
|
|
20
20
|
spec.require_paths = ['lib']
|
|
21
|
-
spec.files =
|
|
22
|
-
f.match(%r{^(test|spec|features)/})
|
|
23
|
-
end
|
|
21
|
+
spec.files = Dir['lib/**/*.rb'] + %w[CHANGELOG.md LICENSE.txt README.md sec_id.gemspec]
|
|
24
22
|
|
|
25
23
|
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
26
24
|
end
|
metadata
CHANGED
|
@@ -1,39 +1,32 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sec_id
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Leonid Svyatov
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies: []
|
|
13
12
|
description: 'Validate securities identification numbers with ease! Currently supported
|
|
14
|
-
standards: ISIN, CUSIP, SEDOL.'
|
|
13
|
+
standards: ISIN, CUSIP, SEDOL, FIGI, CIK, OCC.'
|
|
15
14
|
email:
|
|
16
15
|
- leonid@svyatov.ru
|
|
17
16
|
executables: []
|
|
18
17
|
extensions: []
|
|
19
18
|
extra_rdoc_files: []
|
|
20
19
|
files:
|
|
21
|
-
- ".github/dependabot.yml"
|
|
22
|
-
- ".github/workflows/main.yml"
|
|
23
|
-
- ".gitignore"
|
|
24
|
-
- ".rspec"
|
|
25
|
-
- ".rubocop.yml"
|
|
26
20
|
- CHANGELOG.md
|
|
27
|
-
- Gemfile
|
|
28
21
|
- LICENSE.txt
|
|
29
22
|
- README.md
|
|
30
|
-
- Rakefile
|
|
31
|
-
- bin/console
|
|
32
|
-
- bin/setup
|
|
33
23
|
- lib/sec_id.rb
|
|
34
24
|
- lib/sec_id/base.rb
|
|
25
|
+
- lib/sec_id/cik.rb
|
|
35
26
|
- lib/sec_id/cusip.rb
|
|
27
|
+
- lib/sec_id/figi.rb
|
|
36
28
|
- lib/sec_id/isin.rb
|
|
29
|
+
- lib/sec_id/occ.rb
|
|
37
30
|
- lib/sec_id/sedol.rb
|
|
38
31
|
- lib/sec_id/version.rb
|
|
39
32
|
- sec_id.gemspec
|
|
@@ -42,7 +35,6 @@ licenses:
|
|
|
42
35
|
- MIT
|
|
43
36
|
metadata:
|
|
44
37
|
rubygems_mfa_required: 'true'
|
|
45
|
-
post_install_message:
|
|
46
38
|
rdoc_options: []
|
|
47
39
|
require_paths:
|
|
48
40
|
- lib
|
|
@@ -57,8 +49,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
57
49
|
- !ruby/object:Gem::Version
|
|
58
50
|
version: '0'
|
|
59
51
|
requirements: []
|
|
60
|
-
rubygems_version:
|
|
61
|
-
signing_key:
|
|
52
|
+
rubygems_version: 4.0.3
|
|
62
53
|
specification_version: 4
|
|
63
54
|
summary: Validate securities identification numbers with ease!
|
|
64
55
|
test_files: []
|
data/.github/dependabot.yml
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# To get started with Dependabot version updates, you'll need to specify which
|
|
2
|
-
# package ecosystems to update and where the package manifests are located.
|
|
3
|
-
# Please see the documentation for all configuration options:
|
|
4
|
-
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
5
|
-
|
|
6
|
-
version: 2
|
|
7
|
-
updates:
|
|
8
|
-
- package-ecosystem: "bundler" # See documentation for possible values
|
|
9
|
-
directory: "/" # Location of package manifests
|
|
10
|
-
schedule:
|
|
11
|
-
interval: "weekly"
|
data/.github/workflows/main.yml
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
concurrency:
|
|
4
|
-
group: ${{ github.workflow }}-${{ github.ref }}
|
|
5
|
-
cancel-in-progress: true
|
|
6
|
-
|
|
7
|
-
on:
|
|
8
|
-
push:
|
|
9
|
-
branches: ["main"]
|
|
10
|
-
pull_request:
|
|
11
|
-
branches: ["main"]
|
|
12
|
-
|
|
13
|
-
jobs:
|
|
14
|
-
build:
|
|
15
|
-
runs-on: ubuntu-latest
|
|
16
|
-
name: Ruby ${{ matrix.ruby_version }}
|
|
17
|
-
strategy:
|
|
18
|
-
matrix:
|
|
19
|
-
ruby_version: [ruby-head, '3.3', '3.2', '3.1']
|
|
20
|
-
|
|
21
|
-
env:
|
|
22
|
-
COVERAGE: true
|
|
23
|
-
CC_TEST_REPORTER_ID: ${{ vars.CC_TEST_REPORTER_ID }}
|
|
24
|
-
|
|
25
|
-
steps:
|
|
26
|
-
- uses: actions/checkout@v4
|
|
27
|
-
|
|
28
|
-
- uses: ruby/setup-ruby@v1
|
|
29
|
-
with:
|
|
30
|
-
ruby-version: ${{ matrix.ruby_version }}
|
|
31
|
-
bundler-cache: true
|
|
32
|
-
continue-on-error: ${{ matrix.ruby_version == 'ruby-head' }}
|
|
33
|
-
|
|
34
|
-
- run: bundle exec rake
|
|
35
|
-
continue-on-error: ${{ matrix.ruby_version == 'ruby-head' }}
|
|
36
|
-
|
|
37
|
-
- uses: paambaati/codeclimate-action@v8.0.0
|
|
38
|
-
# Only upload coverage for the latest Ruby
|
|
39
|
-
if: ${{ matrix.ruby_version == '3.3' }}
|
data/.gitignore
DELETED
data/.rspec
DELETED
data/.rubocop.yml
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
require:
|
|
2
|
-
- rubocop-rspec
|
|
3
|
-
|
|
4
|
-
AllCops:
|
|
5
|
-
TargetRubyVersion: 3.1
|
|
6
|
-
DisplayCopNames: true
|
|
7
|
-
DisplayStyleGuide: true
|
|
8
|
-
ExtraDetails: true
|
|
9
|
-
SuggestExtensions: false
|
|
10
|
-
NewCops: enable
|
|
11
|
-
|
|
12
|
-
Layout/LineLength:
|
|
13
|
-
Max: 120
|
|
14
|
-
|
|
15
|
-
Style/Documentation:
|
|
16
|
-
Enabled: false
|
|
17
|
-
|
|
18
|
-
Style/HashEachMethods:
|
|
19
|
-
Enabled: true
|
|
20
|
-
|
|
21
|
-
Style/HashTransformKeys:
|
|
22
|
-
Enabled: true
|
|
23
|
-
|
|
24
|
-
Style/HashTransformValues:
|
|
25
|
-
Enabled: true
|
|
26
|
-
|
|
27
|
-
Metrics/BlockLength:
|
|
28
|
-
Exclude:
|
|
29
|
-
- 'spec/**/*'
|
|
30
|
-
|
|
31
|
-
Lint/MissingSuper:
|
|
32
|
-
AllowedParentClasses:
|
|
33
|
-
- Base
|
|
34
|
-
|
|
35
|
-
RSpec/MultipleExpectations:
|
|
36
|
-
Max: 5
|
|
37
|
-
|
|
38
|
-
RSpec/ExampleLength:
|
|
39
|
-
Max: 10
|
data/Gemfile
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
source 'https://rubygems.org'
|
|
4
|
-
|
|
5
|
-
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
|
6
|
-
|
|
7
|
-
# Specify your gem's dependencies in sec_id.gemspec
|
|
8
|
-
gemspec
|
|
9
|
-
|
|
10
|
-
# Specify your gem's development dependencies below
|
|
11
|
-
gem 'rake', '>= 13'
|
|
12
|
-
|
|
13
|
-
gem 'rspec', '~> 3.9'
|
|
14
|
-
gem 'rspec_junit_formatter'
|
|
15
|
-
|
|
16
|
-
gem 'rubocop', '~> 1.64'
|
|
17
|
-
gem 'rubocop-rspec', '~> 3.0'
|
|
18
|
-
|
|
19
|
-
gem 'simplecov', '~> 0.22', require: false
|
|
20
|
-
gem 'simplecov_json_formatter', require: false
|
data/Rakefile
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'bundler/gem_tasks'
|
|
4
|
-
require 'rspec/core/rake_task'
|
|
5
|
-
require 'rubocop/rake_task'
|
|
6
|
-
|
|
7
|
-
RSpec::Core::RakeTask.new(:spec)
|
|
8
|
-
|
|
9
|
-
RuboCop::RakeTask.new do |task|
|
|
10
|
-
task.requires << 'rubocop-rspec'
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
task default: %i[rubocop spec]
|
data/bin/console
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
require 'bundler/setup'
|
|
5
|
-
require 'sec_id'
|
|
6
|
-
|
|
7
|
-
# You can add fixtures and/or initialization code here to make experimenting
|
|
8
|
-
# with your gem easier. You can also use a different console, if you like.
|
|
9
|
-
|
|
10
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
11
|
-
# require "pry"
|
|
12
|
-
# Pry.start
|
|
13
|
-
|
|
14
|
-
require 'irb'
|
|
15
|
-
IRB.start(__FILE__)
|