sec_id 4.0.0 → 4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 374eb5eb544fd34d09e5d5008473b5516c3b9ac13e720dd7ea01062478da5248
4
- data.tar.gz: 6b785b97c0bd99c56feb8a500f1376f228077f0d58503cb823af3d2abc790ae6
3
+ metadata.gz: a3391006ed4c0e135a0e94ef0d43da6aae3146a178f13c6237aec8d940931c52
4
+ data.tar.gz: a572d9b26de1c8c97476cd954caeefacc9fe44ac8ffff7f25d9b4a72ca058e54
5
5
  SHA512:
6
- metadata.gz: ca697d5bf5326587d437f6206e91d419ad11aad3166f6d2c75f423afae1fcaf3736860b313545d85761f9575bf85f56120f5d3d74e92158fa3c1ed9fc4d5be5d
7
- data.tar.gz: 1c46330ecd6e07192eaf083dc28e41e7b3edbb83a1ee226f76817df4c3182fc20f4e42415350da9bf343b5e89445e1545d79cf1ca02dea15a380fe94f07bef82
6
+ metadata.gz: 1d380490f4a25d1044734b7724bf7e752e7dbb43097a468d3210e8fd9f4efd185beac207d9b5adfc5cce0cedfd5ea944be269785f444fd7a4bbb449b63ea9962
7
+ data.tar.gz: 30434ebe94c7f2c7f3a311ab2a3857af1ae0b66b5993c3b659fd9f6852633171cd7e4ce3c73f7d1e554211cbb53259e1e7679236df243f8dfaf311a0a61cebee
@@ -35,5 +35,5 @@ jobs:
35
35
  continue-on-error: ${{ matrix.ruby_version == 'ruby-head' }}
36
36
 
37
37
  - uses: paambaati/codeclimate-action@v8.0.0
38
- # Only upload coverage for the latest Ruby
39
- if: ${{ matrix.ruby_version == '3.3' }}
38
+ # Only upload coverage for the latest Ruby and don't run for PRs from forks
39
+ if: ${{ matrix.ruby_version == '3.3' && github.event.pull_request.head.repo.fork == false }}
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.1.0] - 2024-09-23
4
+
5
+ ### Added
6
+
7
+ - FIGI support ([@wtn][], #84)
8
+ - CIK support ([@wtn][], #85)
9
+ - Convert between CUSIPs and ISINs ([@wtn][], #86, #88)
10
+ - CINS check method for CUSIPs ([@wtn][], #87)
11
+
12
+ ### Updated
13
+
14
+ - Small internal refactorings
15
+
3
16
  ## [4.0.0] - 2024-07-09
4
17
 
5
18
  ### Breaking changes
data/README.md CHANGED
@@ -11,7 +11,9 @@ Check-digit calculation is also available.
11
11
  Currently supported standards:
12
12
  [ISIN](https://en.wikipedia.org/wiki/International_Securities_Identification_Number),
13
13
  [CUSIP](https://en.wikipedia.org/wiki/CUSIP),
14
- [SEDOL](https://en.wikipedia.org/wiki/SEDOL)
14
+ [SEDOL](https://en.wikipedia.org/wiki/SEDOL),
15
+ [FIGI](https://en.wikipedia.org/wiki/Financial_Instrument_Global_Identifier),
16
+ [CIK](https://en.wikipedia.org/wiki/Central_Index_Key).
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.0'
26
+ gem 'sec_id', '~> 4.1'
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? # => true
139
144
  ```
140
145
 
141
146
  ### SecId::SEDOL full example
@@ -157,6 +162,48 @@ 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.restore!('1094517') # => '0001094517'
193
+ SecId::CIK.check_digit('0001094517') # raises NotImplementedError
194
+
195
+ # instance level
196
+ cik = SecId::CIK.new('0001094517')
197
+ cik.full_number # => '0001094517'
198
+ cik.padding # => '000'
199
+ cik.identifier # => '1094517'
200
+ cik.valid? # => true
201
+ cik.valid_format? # => true
202
+ cik.restore! # => '0001094517'
203
+ cik.calculate_check_digit # raises NotImplementedError
204
+ cik.check_digit # => nil
205
+ ```
206
+
160
207
  ## Development
161
208
 
162
209
  After checking out the repo, run `bin/setup` to install dependencies.
data/lib/sec_id/cik.rb ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SecId
4
+ # https://en.wikipedia.org/wiki/Central_Index_Key
5
+ class CIK < Base
6
+ ID_REGEX = /\A
7
+ (?=\d{1,10}\z)(?<padding>0*)(?<identifier>[1-9]\d{0,9})
8
+ \z/x
9
+
10
+ attr_reader :padding
11
+
12
+ def initialize(cik)
13
+ cik_parts = parse cik
14
+ @padding = cik_parts[:padding]
15
+ @identifier = cik_parts[:identifier]
16
+ end
17
+
18
+ def valid?
19
+ valid_format?
20
+ end
21
+
22
+ def valid_format?
23
+ !identifier.nil?
24
+ end
25
+
26
+ def restore!
27
+ raise InvalidFormatError, "CIK '#{full_number}' is invalid and cannot be restored!" unless valid_format?
28
+
29
+ @padding = '0' * (10 - @identifier.length)
30
+ @full_number = @identifier.rjust(10, '0')
31
+ end
32
+ end
33
+ 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
- return mod10(modified_luhn_sum) if valid_format?
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
- raise InvalidFormatError, "CUSIP '#{full_number}' is invalid and check-digit cannot be calculated!"
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 = 0
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 += div10mod10(double_even) + div10mod10(odd || 0)
53
+ sum + div10mod10(double_even) + div10mod10(odd || 0)
38
54
  end
39
-
40
- sum
41
55
  end
42
56
 
43
- def id_digits
44
- @id_digits ||= identifier.each_char.map(&method(:char_to_digit))
57
+ def reversed_id_digits
58
+ identifier.each_char.map(&method(:char_to_digit)).reverse!
45
59
  end
46
60
  end
47
61
  end
@@ -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
- return mod10(luhn_sum) if valid_format?
32
+ unless valid_format?
33
+ raise InvalidFormatError, "ISIN '#{full_number}' is invalid and check-digit cannot be calculated!"
34
+ end
25
35
 
26
- raise InvalidFormatError, "ISIN '#{full_number}' is invalid and check-digit cannot be calculated!"
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 = 0
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 += double_even + (odd || 0)
57
+ sum + double_even + (odd || 0)
39
58
  end
40
-
41
- sum
42
59
  end
43
60
 
44
- def id_digits
45
- @id_digits ||= identifier.each_char.flat_map(&method(:char_to_digits))
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/sedol.rb CHANGED
@@ -17,9 +17,11 @@ module SecId
17
17
  end
18
18
 
19
19
  def calculate_check_digit
20
- return mod10(weighted_sum) if valid_format?
20
+ unless valid_format?
21
+ raise InvalidFormatError, "SEDOL '#{full_number}' is invalid and check-digit cannot be calculated!"
22
+ end
21
23
 
22
- raise InvalidFormatError, "SEDOL '#{full_number}' is invalid and check-digit cannot be calculated!"
24
+ mod10(weighted_sum)
23
25
  end
24
26
 
25
27
  private
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SecId
4
- VERSION = '4.0.0'
4
+ VERSION = '4.1.0'
5
5
  end
data/lib/sec_id.rb CHANGED
@@ -6,6 +6,8 @@ require 'sec_id/base'
6
6
  require 'sec_id/isin'
7
7
  require 'sec_id/cusip'
8
8
  require 'sec_id/sedol'
9
+ require 'sec_id/figi'
10
+ require 'sec_id/cik'
9
11
 
10
12
  module SecId
11
13
  Error = Class.new(StandardError)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sec_id
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leonid Svyatov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-07-09 00:00:00.000000000 Z
11
+ date: 2024-09-23 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: 'Validate securities identification numbers with ease! Currently supported
14
14
  standards: ISIN, CUSIP, SEDOL.'
@@ -32,7 +32,9 @@ files:
32
32
  - bin/setup
33
33
  - lib/sec_id.rb
34
34
  - lib/sec_id/base.rb
35
+ - lib/sec_id/cik.rb
35
36
  - lib/sec_id/cusip.rb
37
+ - lib/sec_id/figi.rb
36
38
  - lib/sec_id/isin.rb
37
39
  - lib/sec_id/sedol.rb
38
40
  - lib/sec_id/version.rb
@@ -57,7 +59,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
57
59
  - !ruby/object:Gem::Version
58
60
  version: '0'
59
61
  requirements: []
60
- rubygems_version: 3.5.14
62
+ rubygems_version: 3.5.15
61
63
  signing_key:
62
64
  specification_version: 4
63
65
  summary: Validate securities identification numbers with ease!