sec_id 4.3.0 → 4.4.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 +47 -26
- data/README.md +175 -8
- data/lib/sec_id/base.rb +12 -127
- data/lib/sec_id/cei.rb +51 -0
- data/lib/sec_id/cfi.rb +314 -0
- data/lib/sec_id/cik.rb +0 -6
- data/lib/sec_id/concerns/checkable.rb +201 -0
- data/lib/sec_id/cusip.rb +3 -17
- data/lib/sec_id/figi.rb +3 -18
- data/lib/sec_id/fisn.rb +52 -0
- data/lib/sec_id/iban.rb +2 -1
- data/lib/sec_id/isin.rb +88 -16
- data/lib/sec_id/lei.rb +3 -1
- data/lib/sec_id/occ.rb +0 -6
- data/lib/sec_id/sedol.rb +20 -1
- data/lib/sec_id/valoren.rb +74 -0
- data/lib/sec_id/version.rb +1 -1
- data/lib/sec_id/wkn.rb +41 -0
- data/lib/sec_id.rb +9 -3
- data/sec_id.gemspec +2 -1
- metadata +11 -4
- /data/lib/sec_id/{normalizable.rb → concerns/normalizable.rb} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d1a703602defc67f745ab0994c73f61d964ed1047a029cc01c2cf7edcda82045
|
|
4
|
+
data.tar.gz: cf7a004bb992ceb3a9991946d09c908a76d13852c7a87a0b2527a4633da869be
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c4e726ce2c1cd7b64dc6d2f8db7e284166ff0229eef52f064975d244e766e28ba239dd8f39dc1730e3b092ae0e57cf982adb683711cde6dd9b15a4bc8c88eb09
|
|
7
|
+
data.tar.gz: '0986116c63812cc37af215b4f564e3db44d4ea154d90dc49a99e82096fe18e099277087a2b399920e2bf03acbdc69d098b6442c2195d434f2af3230cffe2b257'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
|
|
7
|
+
and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
|
|
8
|
+
|
|
3
9
|
## [Unreleased]
|
|
4
10
|
|
|
11
|
+
## [4.4.0] - 2026-01-29
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Cross-identifier conversions: SEDOL, WKN, and Valoren `to_isin` methods with country code validation; ISIN `to_sedol`, `to_wkn`, `to_valoren` methods with predicate helpers (`sedol?`, `wkn?`, `valoren?`) ([#115](https://github.com/svyatov/sec_id/pull/115))
|
|
16
|
+
- ISIN `nsin_type` and `to_nsin` methods for country-aware NSIN extraction ([#114](https://github.com/svyatov/sec_id/pull/114))
|
|
17
|
+
- CEI (CUSIP Entity Identifier) support for syndicated loan market entity identification ([#113](https://github.com/svyatov/sec_id/pull/113))
|
|
18
|
+
- FISN (Financial Instrument Short Name) support per ISO 18774 ([#112](https://github.com/svyatov/sec_id/pull/112))
|
|
19
|
+
- CFI (Classification of Financial Instruments) support with category/group validation and equity-specific predicates ([#111](https://github.com/svyatov/sec_id/pull/111))
|
|
20
|
+
- Valoren support (Swiss Security Number) ([@wtn](https://github.com/wtn), [#109](https://github.com/svyatov/sec_id/pull/109))
|
|
21
|
+
- WKN support (Wertpapierkennnummer - German securities identifier) ([@wtn](https://github.com/wtn), [#108](https://github.com/svyatov/sec_id/pull/108))
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Replaced `has_check_digit` DSL with explicit `Checkable` concern that consolidates all check-digit logic (constants, Luhn algorithms, validation, restoration)
|
|
26
|
+
- Simplified `Base` class to core validation and parsing; check-digit classes now `include Checkable`
|
|
27
|
+
- Non-check-digit classes (CIK, OCC, WKN, Valoren, CFI, FISN) no longer need any special declaration
|
|
28
|
+
- Moved `Normalizable` module to `lib/sec_id/concerns/` for consistency with other concerns
|
|
29
|
+
- Optimized hot paths by replacing `&method(:char_to_digit)` with inline blocks to avoid Method object allocation
|
|
30
|
+
- Added frozen Set constants for ISIN country code lookups (`SEDOL_COUNTRY_CODES`, `VALOREN_COUNTRY_CODES`)
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- Allow Crown Dependencies (GG, IM, JE) and Overseas Territories (FK) in SEDOL/ISIN conversions ([@wtn](https://github.com/wtn), [#117](https://github.com/svyatov/sec_id/pull/117))
|
|
35
|
+
- Removed BR (Brazil) from CGS country codes — Brazil never used CINS numbers and Brazilian ISINs cannot be converted to CUSIP ([@wtn](https://github.com/wtn), [#110](https://github.com/svyatov/sec_id/pull/110))
|
|
36
|
+
|
|
5
37
|
## [4.3.0] - 2025-01-13
|
|
6
38
|
|
|
7
39
|
### Added
|
|
@@ -9,12 +41,9 @@
|
|
|
9
41
|
- LEI support (Legal Entity Identifier, ISO 17442)
|
|
10
42
|
- IBAN support (International Bank Account Number, ISO 13616) with EU/EEA country validation
|
|
11
43
|
|
|
12
|
-
###
|
|
44
|
+
### Changed
|
|
13
45
|
|
|
14
46
|
- Improved README: better formatting, navigation, and clear API distinction between check-digit and normalization identifiers
|
|
15
|
-
|
|
16
|
-
### Internal
|
|
17
|
-
|
|
18
47
|
- Refactored CIK and OCC to inherit from Base class with `has_check_digit?` hook for cleaner architecture
|
|
19
48
|
- Added `Normalizable` module for consistent `normalize!` class method across identifiers
|
|
20
49
|
- Added `validate_format_for_calculation!` helper method to Base class to reduce code duplication
|
|
@@ -31,11 +60,7 @@
|
|
|
31
60
|
|
|
32
61
|
- OCC support ([@wtn](https://github.com/wtn), [#93](https://github.com/svyatov/sec_id/pull/93))
|
|
33
62
|
|
|
34
|
-
###
|
|
35
|
-
|
|
36
|
-
- CUSIP#cins? usage example in README ([@wtn](https://github.com/wtn), [#91](https://github.com/svyatov/sec_id/pull/91))
|
|
37
|
-
|
|
38
|
-
### Updated
|
|
63
|
+
### Changed
|
|
39
64
|
|
|
40
65
|
- Separate CIK from Base for cleaner architecture ([@wtn](https://github.com/wtn), [#92](https://github.com/svyatov/sec_id/pull/92))
|
|
41
66
|
- Use rubocop-rspec plugin ([@wtn](https://github.com/wtn), [#90](https://github.com/svyatov/sec_id/pull/90))
|
|
@@ -43,6 +68,10 @@
|
|
|
43
68
|
- Add permissions to CI workflow
|
|
44
69
|
- Clean up gemspec: update description and simplify files list
|
|
45
70
|
|
|
71
|
+
### Fixed
|
|
72
|
+
|
|
73
|
+
- CUSIP#cins? usage example in README ([@wtn](https://github.com/wtn), [#91](https://github.com/svyatov/sec_id/pull/91))
|
|
74
|
+
|
|
46
75
|
## [4.1.0] - 2024-09-23
|
|
47
76
|
|
|
48
77
|
### Added
|
|
@@ -52,19 +81,16 @@
|
|
|
52
81
|
- 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))
|
|
53
82
|
- CINS check method for CUSIPs ([@wtn](https://github.com/wtn), [#87](https://github.com/svyatov/sec_id/pull/87))
|
|
54
83
|
|
|
55
|
-
###
|
|
84
|
+
### Changed
|
|
56
85
|
|
|
57
86
|
- Small internal refactorings
|
|
58
87
|
|
|
59
88
|
## [4.0.0] - 2024-07-09
|
|
60
89
|
|
|
61
|
-
###
|
|
62
|
-
|
|
63
|
-
- Minimum required Ruby version is 3.1 now
|
|
64
|
-
- Default repository branch renamed to `main`
|
|
65
|
-
|
|
66
|
-
### Updated
|
|
90
|
+
### Changed
|
|
67
91
|
|
|
92
|
+
- **BREAKING:** Minimum required Ruby version is 3.1 now
|
|
93
|
+
- **BREAKING:** Default repository branch renamed to `main`
|
|
68
94
|
- Small internal refactorings
|
|
69
95
|
- TravisCI -> GitHub Actions
|
|
70
96
|
- Dropped tests for Ruby below 3.1
|
|
@@ -72,12 +98,9 @@
|
|
|
72
98
|
|
|
73
99
|
## [3.0.0] - 2020-03-10
|
|
74
100
|
|
|
75
|
-
###
|
|
76
|
-
|
|
77
|
-
- Minimum required Ruby version is 2.5 now
|
|
78
|
-
|
|
79
|
-
### Updated
|
|
101
|
+
### Changed
|
|
80
102
|
|
|
103
|
+
- **BREAKING:** Minimum required Ruby version is 2.5 now
|
|
81
104
|
- Small internal refactorings
|
|
82
105
|
- TravisCI config updated: dropped Ruby 2.3 and 2.4, added Ruby 2.7
|
|
83
106
|
- Rubocop's Ruby target version changed to 2.5
|
|
@@ -88,11 +111,9 @@
|
|
|
88
111
|
|
|
89
112
|
- SEDOL numbers support: `SecId::SEDOL`
|
|
90
113
|
|
|
91
|
-
###
|
|
92
|
-
|
|
93
|
-
- **Breaking change**
|
|
114
|
+
### Changed
|
|
94
115
|
|
|
95
|
-
|
|
116
|
+
- **BREAKING:** API for accessing full number is unified across all classes:
|
|
96
117
|
|
|
97
118
|
```
|
|
98
119
|
SecId::ISIN#full_number # previously SecId::ISIN#isin
|
|
@@ -111,7 +132,7 @@
|
|
|
111
132
|
- CUSIP numbers support: `SecId::CUSIP`
|
|
112
133
|
- CHANGELOG.md file added
|
|
113
134
|
|
|
114
|
-
###
|
|
135
|
+
### Changed
|
|
115
136
|
|
|
116
137
|
- Char to digit conversion now uses precalculated tables instead of dynamic calculation for speed
|
|
117
138
|
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# SecId [](https://rubygems.org/gems/sec_id) [](https://app.codecov.io/gh/svyatov/sec_id) [](https://github.com/svyatov/sec_id/actions?query=workflow%3ACI)
|
|
1
|
+
# SecId [](https://rubygems.org/gems/sec_id) [](https://app.codecov.io/gh/svyatov/sec_id) [](https://github.com/svyatov/sec_id/actions?query=workflow%3ACI)
|
|
2
2
|
|
|
3
3
|
> Validate securities identification numbers with ease!
|
|
4
4
|
|
|
@@ -9,14 +9,21 @@
|
|
|
9
9
|
- [Supported Standards and Usage](#supported-standards-and-usage)
|
|
10
10
|
- [ISIN](#isin) - International Securities Identification Number
|
|
11
11
|
- [CUSIP](#cusip) - Committee on Uniform Securities Identification Procedures
|
|
12
|
+
- [CEI](#cei) - CUSIP Entity Identifier
|
|
12
13
|
- [SEDOL](#sedol) - Stock Exchange Daily Official List
|
|
13
14
|
- [FIGI](#figi) - Financial Instrument Global Identifier
|
|
14
15
|
- [LEI](#lei) - Legal Entity Identifier
|
|
15
16
|
- [IBAN](#iban) - International Bank Account Number
|
|
16
17
|
- [CIK](#cik) - Central Index Key
|
|
17
18
|
- [OCC](#occ) - Options Clearing Corporation Symbol
|
|
19
|
+
- [WKN](#wkn) - Wertpapierkennnummer
|
|
20
|
+
- [Valoren](#valoren) - Swiss Security Number
|
|
21
|
+
- [CFI](#cfi) - Classification of Financial Instruments
|
|
22
|
+
- [FISN](#fisn) - Financial Instrument Short Name
|
|
18
23
|
- [Development](#development)
|
|
19
24
|
- [Contributing](#contributing)
|
|
25
|
+
- [Changelog](#changelog)
|
|
26
|
+
- [Versioning](#versioning)
|
|
20
27
|
- [License](#license)
|
|
21
28
|
|
|
22
29
|
## Supported Ruby Versions
|
|
@@ -28,7 +35,7 @@ Ruby 3.1+ is required.
|
|
|
28
35
|
Add this line to your application's Gemfile:
|
|
29
36
|
|
|
30
37
|
```ruby
|
|
31
|
-
gem 'sec_id', '~> 4.
|
|
38
|
+
gem 'sec_id', '~> 4.4'
|
|
32
39
|
```
|
|
33
40
|
|
|
34
41
|
And then execute:
|
|
@@ -47,11 +54,11 @@ gem install sec_id
|
|
|
47
54
|
|
|
48
55
|
All identifier classes provide `valid?` and `valid_format?` methods at both class and instance levels.
|
|
49
56
|
|
|
50
|
-
**Check-digit based identifiers** (ISIN, CUSIP, SEDOL, FIGI, LEI, IBAN) also provide:
|
|
57
|
+
**Check-digit based identifiers** (ISIN, CUSIP, CEI, SEDOL, FIGI, LEI, IBAN) also provide:
|
|
51
58
|
- `restore!` - restores check-digit and returns the full number
|
|
52
59
|
- `check_digit` / `calculate_check_digit` - calculates and returns the check-digit
|
|
53
60
|
|
|
54
|
-
**Normalization based identifiers** (CIK, OCC) provide instead:
|
|
61
|
+
**Normalization based identifiers** (CIK, OCC, Valoren) provide instead:
|
|
55
62
|
- `normalize!` - pads/formats the identifier to its standard form
|
|
56
63
|
|
|
57
64
|
### ISIN
|
|
@@ -76,6 +83,26 @@ isin.valid_format? # => true
|
|
|
76
83
|
isin.restore! # => 'US5949181045'
|
|
77
84
|
isin.calculate_check_digit # => 5
|
|
78
85
|
isin.to_cusip # => #<SecId::CUSIP>
|
|
86
|
+
isin.nsin_type # => :cusip
|
|
87
|
+
isin.to_nsin # => #<SecId::CUSIP>
|
|
88
|
+
|
|
89
|
+
# NSIN extraction for different countries
|
|
90
|
+
SecId::ISIN.new('GB00B02H2F76').nsin_type # => :sedol
|
|
91
|
+
SecId::ISIN.new('GB00B02H2F76').to_nsin # => #<SecId::SEDOL>
|
|
92
|
+
SecId::ISIN.new('DE0007164600').nsin_type # => :wkn
|
|
93
|
+
SecId::ISIN.new('DE0007164600').to_nsin # => #<SecId::WKN>
|
|
94
|
+
SecId::ISIN.new('CH0012221716').nsin_type # => :valoren
|
|
95
|
+
SecId::ISIN.new('CH0012221716').to_nsin # => #<SecId::Valoren>
|
|
96
|
+
SecId::ISIN.new('FR0000120271').nsin_type # => :generic
|
|
97
|
+
SecId::ISIN.new('FR0000120271').to_nsin # => '000012027' (raw NSIN string)
|
|
98
|
+
|
|
99
|
+
# Type-specific conversion methods with validation
|
|
100
|
+
SecId::ISIN.new('GB00B02H2F76').sedol? # => true
|
|
101
|
+
SecId::ISIN.new('GB00B02H2F76').to_sedol # => #<SecId::SEDOL>
|
|
102
|
+
SecId::ISIN.new('DE0007164600').wkn? # => true
|
|
103
|
+
SecId::ISIN.new('DE0007164600').to_wkn # => #<SecId::WKN>
|
|
104
|
+
SecId::ISIN.new('CH0012221716').valoren? # => true
|
|
105
|
+
SecId::ISIN.new('CH0012221716').to_valoren # => #<SecId::Valoren>
|
|
79
106
|
```
|
|
80
107
|
|
|
81
108
|
### CUSIP
|
|
@@ -103,9 +130,33 @@ cusip.to_isin('US') # => #<SecId::ISIN>
|
|
|
103
130
|
cusip.cins? # => false
|
|
104
131
|
```
|
|
105
132
|
|
|
133
|
+
### CEI
|
|
134
|
+
|
|
135
|
+
> [CUSIP Entity Identifier](https://www.cusip.com/identifiers.html) - a 10-character alphanumeric code that identifies legal entities in the syndicated loan market.
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
# class level
|
|
139
|
+
SecId::CEI.valid?('A0BCDEFGH1') # => true
|
|
140
|
+
SecId::CEI.valid_format?('A0BCDEFGH') # => true
|
|
141
|
+
SecId::CEI.restore!('A0BCDEFGH') # => 'A0BCDEFGH1'
|
|
142
|
+
SecId::CEI.check_digit('A0BCDEFGH') # => 1
|
|
143
|
+
|
|
144
|
+
# instance level
|
|
145
|
+
cei = SecId::CEI.new('A0BCDEFGH1')
|
|
146
|
+
cei.full_number # => 'A0BCDEFGH1'
|
|
147
|
+
cei.prefix # => 'A'
|
|
148
|
+
cei.numeric # => '0'
|
|
149
|
+
cei.entity_id # => 'BCDEFGH'
|
|
150
|
+
cei.check_digit # => 1
|
|
151
|
+
cei.valid? # => true
|
|
152
|
+
cei.valid_format? # => true
|
|
153
|
+
cei.restore! # => 'A0BCDEFGH1'
|
|
154
|
+
cei.calculate_check_digit # => 1
|
|
155
|
+
```
|
|
156
|
+
|
|
106
157
|
### SEDOL
|
|
107
158
|
|
|
108
|
-
> [Stock Exchange Daily Official List](https://en.wikipedia.org/wiki/SEDOL) - a 7-character alphanumeric code used in the United Kingdom and
|
|
159
|
+
> [Stock Exchange Daily Official List](https://en.wikipedia.org/wiki/SEDOL) - a 7-character alphanumeric code used in the United Kingdom, Ireland, Crown Dependencies (Jersey, Guernsey, Isle of Man), and select British Overseas Territories.
|
|
109
160
|
|
|
110
161
|
```ruby
|
|
111
162
|
# class level
|
|
@@ -122,6 +173,8 @@ sedol.valid? # => true
|
|
|
122
173
|
sedol.valid_format? # => true
|
|
123
174
|
sedol.restore! # => 'B0Z52W5'
|
|
124
175
|
sedol.calculate_check_digit # => 5
|
|
176
|
+
sedol.to_isin # => #<SecId::ISIN> (GB ISIN by default)
|
|
177
|
+
sedol.to_isin('IE') # => #<SecId::ISIN> (IE ISIN)
|
|
125
178
|
```
|
|
126
179
|
|
|
127
180
|
### FIGI
|
|
@@ -259,18 +312,132 @@ occ.normalize! # => 'X 250620C00050000'
|
|
|
259
312
|
occ.full_symbol # => 'X 250620C00050000'
|
|
260
313
|
```
|
|
261
314
|
|
|
315
|
+
### WKN
|
|
316
|
+
|
|
317
|
+
> [Wertpapierkennnummer](https://en.wikipedia.org/wiki/Wertpapierkennnummer) - a 6-character alphanumeric code used to identify securities in Germany.
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
# class level
|
|
321
|
+
SecId::WKN.valid?('514000') # => true
|
|
322
|
+
SecId::WKN.valid?('CBK100') # => true
|
|
323
|
+
SecId::WKN.valid_format?('514000') # => true
|
|
324
|
+
|
|
325
|
+
# instance level
|
|
326
|
+
wkn = SecId::WKN.new('514000')
|
|
327
|
+
wkn.full_number # => '514000'
|
|
328
|
+
wkn.identifier # => '514000'
|
|
329
|
+
wkn.valid? # => true
|
|
330
|
+
wkn.valid_format? # => true
|
|
331
|
+
wkn.to_s # => '514000'
|
|
332
|
+
wkn.to_isin # => #<SecId::ISIN> (DE ISIN)
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
WKN excludes letters I and O to avoid confusion with digits 1 and 0.
|
|
336
|
+
|
|
337
|
+
### Valoren
|
|
338
|
+
|
|
339
|
+
> [Valoren](https://en.wikipedia.org/wiki/Valoren_number) - a numeric identifier for securities in Switzerland, Liechtenstein, and Belgium.
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
# class level
|
|
343
|
+
SecId::Valoren.valid?('3886335') # => true
|
|
344
|
+
SecId::Valoren.valid?('24476758') # => true
|
|
345
|
+
SecId::Valoren.valid?('35514757') # => true
|
|
346
|
+
SecId::Valoren.valid?('97429325') # => true
|
|
347
|
+
SecId::Valoren.valid_format?('3886335') # => true
|
|
348
|
+
SecId::Valoren.normalize!('3886335') # => '003886335'
|
|
349
|
+
|
|
350
|
+
# instance level
|
|
351
|
+
valoren = SecId::Valoren.new('3886335')
|
|
352
|
+
valoren.full_number # => '3886335'
|
|
353
|
+
valoren.padding # => ''
|
|
354
|
+
valoren.identifier # => '3886335'
|
|
355
|
+
valoren.valid? # => true
|
|
356
|
+
valoren.valid_format? # => true
|
|
357
|
+
valoren.normalize! # => '003886335'
|
|
358
|
+
valoren.to_s # => '003886335'
|
|
359
|
+
valoren.to_isin # => #<SecId::ISIN> (CH ISIN by default)
|
|
360
|
+
valoren.to_isin('LI') # => #<SecId::ISIN> (LI ISIN)
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### CFI
|
|
364
|
+
|
|
365
|
+
> [Classification of Financial Instruments](https://en.wikipedia.org/wiki/ISO_10962) - a 6-character alphabetic code that classifies financial instruments per ISO 10962.
|
|
366
|
+
|
|
367
|
+
```ruby
|
|
368
|
+
# class level
|
|
369
|
+
SecId::CFI.valid?('ESXXXX') # => true
|
|
370
|
+
SecId::CFI.valid?('ESVUFR') # => true
|
|
371
|
+
SecId::CFI.valid_format?('ESXXXX') # => true
|
|
372
|
+
|
|
373
|
+
# instance level
|
|
374
|
+
cfi = SecId::CFI.new('ESVUFR')
|
|
375
|
+
cfi.full_number # => 'ESVUFR'
|
|
376
|
+
cfi.identifier # => 'ESVUFR'
|
|
377
|
+
cfi.category_code # => 'E'
|
|
378
|
+
cfi.group_code # => 'S'
|
|
379
|
+
cfi.category # => :equity
|
|
380
|
+
cfi.group # => :common_shares
|
|
381
|
+
cfi.valid? # => true
|
|
382
|
+
cfi.valid_format? # => true
|
|
383
|
+
|
|
384
|
+
# Equity-specific predicates
|
|
385
|
+
cfi.equity? # => true
|
|
386
|
+
cfi.voting? # => true
|
|
387
|
+
cfi.restrictions? # => false
|
|
388
|
+
cfi.fully_paid? # => true
|
|
389
|
+
cfi.registered? # => true
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
CFI validates the category code (position 1) against 14 valid values and the group code (position 2) against valid values for that category. Attribute positions 3-6 accept any letter A-Z, with X meaning "not applicable".
|
|
393
|
+
|
|
394
|
+
### FISN
|
|
395
|
+
|
|
396
|
+
> [Financial Instrument Short Name](https://en.wikipedia.org/wiki/ISO_18774) - a human-readable short name for financial instruments per ISO 18774.
|
|
397
|
+
|
|
398
|
+
```ruby
|
|
399
|
+
# class level
|
|
400
|
+
SecId::FISN.valid?('APPLE INC/SH') # => true
|
|
401
|
+
SecId::FISN.valid?('apple inc/sh') # => true (normalized to uppercase)
|
|
402
|
+
SecId::FISN.valid_format?('APPLE INC/SH') # => true
|
|
403
|
+
|
|
404
|
+
# instance level
|
|
405
|
+
fisn = SecId::FISN.new('APPLE INC/SH')
|
|
406
|
+
fisn.full_number # => 'APPLE INC/SH'
|
|
407
|
+
fisn.identifier # => 'APPLE INC/SH'
|
|
408
|
+
fisn.issuer # => 'APPLE INC'
|
|
409
|
+
fisn.description # => 'SH'
|
|
410
|
+
fisn.valid? # => true
|
|
411
|
+
fisn.valid_format? # => true
|
|
412
|
+
fisn.to_s # => 'APPLE INC/SH'
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
FISN format: `Issuer Name/Abbreviated Instrument Description` with issuer (1-15 chars) and description (1-19 chars) separated by a forward slash. Character set: uppercase A-Z, digits 0-9, and space.
|
|
416
|
+
|
|
262
417
|
## Development
|
|
263
418
|
|
|
264
419
|
After checking out the repo, run `bin/setup` to install dependencies.
|
|
265
|
-
Then, run `rake
|
|
420
|
+
Then, run `bundle exec rake` to run the tests. You can also run `bin/console`
|
|
266
421
|
for an interactive prompt that will allow you to experiment.
|
|
267
422
|
|
|
268
423
|
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
269
424
|
|
|
270
425
|
## Contributing
|
|
271
426
|
|
|
272
|
-
|
|
273
|
-
|
|
427
|
+
1. Fork it
|
|
428
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
429
|
+
3. Make your changes and run tests (`bundle exec rake`)
|
|
430
|
+
4. Commit using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format (`git commit -m 'feat: add some feature'`)
|
|
431
|
+
5. Push to the branch (`git push origin my-new-feature`)
|
|
432
|
+
6. Create a new Pull Request
|
|
433
|
+
|
|
434
|
+
## Changelog
|
|
435
|
+
|
|
436
|
+
See [CHANGELOG.md](CHANGELOG.md) for a detailed history of changes, following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format.
|
|
437
|
+
|
|
438
|
+
## Versioning
|
|
439
|
+
|
|
440
|
+
This project follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html)
|
|
274
441
|
|
|
275
442
|
## License
|
|
276
443
|
|
data/lib/sec_id/base.rb
CHANGED
|
@@ -1,49 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SecId
|
|
4
|
-
# Character-to-digit mapping for Luhn algorithm variants.
|
|
5
|
-
# Maps alphanumeric characters to digit arrays for multi-digit expansion.
|
|
6
|
-
# Used by ISIN for check-digit calculation.
|
|
7
|
-
CHAR_TO_DIGITS = {
|
|
8
|
-
'0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4,
|
|
9
|
-
'5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9,
|
|
10
|
-
'A' => [1, 0], 'B' => [1, 1], 'C' => [1, 2], 'D' => [1, 3], 'E' => [1, 4],
|
|
11
|
-
'F' => [1, 5], 'G' => [1, 6], 'H' => [1, 7], 'I' => [1, 8], 'J' => [1, 9],
|
|
12
|
-
'K' => [2, 0], 'L' => [2, 1], 'M' => [2, 2], 'N' => [2, 3], 'O' => [2, 4],
|
|
13
|
-
'P' => [2, 5], 'Q' => [2, 6], 'R' => [2, 7], 'S' => [2, 8], 'T' => [2, 9],
|
|
14
|
-
'U' => [3, 0], 'V' => [3, 1], 'W' => [3, 2], 'X' => [3, 3], 'Y' => [3, 4], 'Z' => [3, 5],
|
|
15
|
-
'*' => [3, 6], '@' => [3, 7], '#' => [3, 8]
|
|
16
|
-
}.freeze
|
|
17
|
-
|
|
18
|
-
# Character-to-digit mapping for single-digit conversion.
|
|
19
|
-
# Maps alphanumeric characters to values 0-38 (A=10, B=11, ..., Z=35, *=36, @=37, #=38).
|
|
20
|
-
# Used by CUSIP, FIGI, SEDOL, LEI, and IBAN for check-digit calculations.
|
|
21
|
-
CHAR_TO_DIGIT = {
|
|
22
|
-
'0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4,
|
|
23
|
-
'5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9,
|
|
24
|
-
'A' => 10, 'B' => 11, 'C' => 12, 'D' => 13, 'E' => 14,
|
|
25
|
-
'F' => 15, 'G' => 16, 'H' => 17, 'I' => 18, 'J' => 19,
|
|
26
|
-
'K' => 20, 'L' => 21, 'M' => 22, 'N' => 23, 'O' => 24,
|
|
27
|
-
'P' => 25, 'Q' => 26, 'R' => 27, 'S' => 28, 'T' => 29,
|
|
28
|
-
'U' => 30, 'V' => 31, 'W' => 32, 'X' => 33, 'Y' => 34, 'Z' => 35,
|
|
29
|
-
'*' => 36, '@' => 37, '#' => 38
|
|
30
|
-
}.freeze
|
|
31
|
-
|
|
32
4
|
# Base class for securities identifiers that provides a common interface
|
|
33
|
-
# for validation
|
|
5
|
+
# for validation and parsing.
|
|
34
6
|
#
|
|
35
7
|
# Subclasses must implement:
|
|
36
8
|
# - ID_REGEX constant with named capture groups for parsing
|
|
37
9
|
# - initialize method that calls parse and extracts components
|
|
38
|
-
# - calculate_check_digit method (only if has_check_digit? returns true)
|
|
39
10
|
#
|
|
40
|
-
# Subclasses
|
|
41
|
-
#
|
|
42
|
-
# - valid_format? for additional format validation beyond regex matching
|
|
43
|
-
# - to_s for custom string representation
|
|
11
|
+
# Subclasses with check digits should also include the Checkable concern,
|
|
12
|
+
# which provides check-digit validation, calculation, and restoration.
|
|
44
13
|
#
|
|
45
14
|
# @example Implementing a check-digit identifier
|
|
46
15
|
# class MyIdentifier < Base
|
|
16
|
+
# include Checkable
|
|
17
|
+
#
|
|
47
18
|
# ID_REGEX = /\A(?<identifier>[A-Z]{6})(?<check_digit>\d)?\z/x
|
|
48
19
|
#
|
|
49
20
|
# def initialize(id)
|
|
@@ -60,8 +31,11 @@ module SecId
|
|
|
60
31
|
#
|
|
61
32
|
# @example Implementing a non-check-digit identifier
|
|
62
33
|
# class SimpleId < Base
|
|
63
|
-
#
|
|
64
|
-
#
|
|
34
|
+
# ID_REGEX = /\A(?<identifier>[A-Z]{6})\z/x
|
|
35
|
+
#
|
|
36
|
+
# def initialize(id)
|
|
37
|
+
# parts = parse(id)
|
|
38
|
+
# @identifier = parts[:identifier]
|
|
65
39
|
# end
|
|
66
40
|
# end
|
|
67
41
|
class Base
|
|
@@ -71,9 +45,6 @@ module SecId
|
|
|
71
45
|
# @return [String, nil] the main identifier portion (without check digit)
|
|
72
46
|
attr_reader :identifier
|
|
73
47
|
|
|
74
|
-
# @return [Integer, nil] the check digit value
|
|
75
|
-
attr_reader :check_digit
|
|
76
|
-
|
|
77
48
|
class << self
|
|
78
49
|
# @param id [String] the identifier to validate
|
|
79
50
|
# @return [Boolean]
|
|
@@ -86,22 +57,6 @@ module SecId
|
|
|
86
57
|
def valid_format?(id)
|
|
87
58
|
new(id).valid_format?
|
|
88
59
|
end
|
|
89
|
-
|
|
90
|
-
# Restores (calculates) the check digit and returns the full identifier.
|
|
91
|
-
#
|
|
92
|
-
# @param id_without_check_digit [String] identifier without or with incorrect check digit
|
|
93
|
-
# @return [String] the full identifier with correct check digit
|
|
94
|
-
# @raise [InvalidFormatError] if the identifier format is invalid
|
|
95
|
-
def restore!(id_without_check_digit)
|
|
96
|
-
new(id_without_check_digit).restore!
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# @param id [String] the identifier to calculate check digit for
|
|
100
|
-
# @return [Integer] the calculated check digit
|
|
101
|
-
# @raise [InvalidFormatError] if the identifier format is invalid
|
|
102
|
-
def check_digit(id)
|
|
103
|
-
new(id).calculate_check_digit
|
|
104
|
-
end
|
|
105
60
|
end
|
|
106
61
|
|
|
107
62
|
# Subclasses must override this method.
|
|
@@ -112,19 +67,9 @@ module SecId
|
|
|
112
67
|
raise NotImplementedError
|
|
113
68
|
end
|
|
114
69
|
|
|
115
|
-
# Override in subclasses to return false for identifiers without check digits.
|
|
116
|
-
#
|
|
117
|
-
# @return [Boolean]
|
|
118
|
-
def has_check_digit?
|
|
119
|
-
true
|
|
120
|
-
end
|
|
121
|
-
|
|
122
70
|
# @return [Boolean]
|
|
123
71
|
def valid?
|
|
124
|
-
|
|
125
|
-
return false unless valid_format?
|
|
126
|
-
|
|
127
|
-
check_digit == calculate_check_digit
|
|
72
|
+
valid_format?
|
|
128
73
|
end
|
|
129
74
|
|
|
130
75
|
# Override in subclasses for additional format validation.
|
|
@@ -134,38 +79,14 @@ module SecId
|
|
|
134
79
|
!identifier.nil?
|
|
135
80
|
end
|
|
136
81
|
|
|
137
|
-
# @return [String] the full identifier with correct check digit
|
|
138
|
-
# @raise [InvalidFormatError] if the identifier format is invalid
|
|
139
|
-
def restore!
|
|
140
|
-
@check_digit = calculate_check_digit
|
|
141
|
-
@full_number = to_s
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
# Subclasses must override this method.
|
|
145
|
-
#
|
|
146
|
-
# @return [Integer] the calculated check digit
|
|
147
|
-
# @raise [NotImplementedError] always raised in base class
|
|
148
|
-
# @raise [InvalidFormatError] if the identifier format is invalid (in subclasses)
|
|
149
|
-
def calculate_check_digit
|
|
150
|
-
raise NotImplementedError
|
|
151
|
-
end
|
|
152
|
-
|
|
153
82
|
# @return [String]
|
|
154
83
|
def to_s
|
|
155
|
-
|
|
84
|
+
identifier.to_s
|
|
156
85
|
end
|
|
157
86
|
alias to_str to_s
|
|
158
87
|
|
|
159
88
|
private
|
|
160
89
|
|
|
161
|
-
# @raise [InvalidFormatError] if valid_format? returns false
|
|
162
|
-
# @return [void]
|
|
163
|
-
def validate_format_for_calculation!
|
|
164
|
-
return if valid_format?
|
|
165
|
-
|
|
166
|
-
raise InvalidFormatError, "#{self.class.name} '#{full_number}' is invalid and check-digit cannot be calculated!"
|
|
167
|
-
end
|
|
168
|
-
|
|
169
90
|
# @param sec_id_number [String, #to_s] the identifier to parse
|
|
170
91
|
# @param upcase [Boolean] whether to upcase the input
|
|
171
92
|
# @return [MatchData, Hash] the regex match data or empty hash if no match
|
|
@@ -174,41 +95,5 @@ module SecId
|
|
|
174
95
|
@full_number.upcase! if upcase
|
|
175
96
|
@full_number.match(self.class::ID_REGEX) || {}
|
|
176
97
|
end
|
|
177
|
-
|
|
178
|
-
# @return [Array<Integer>] array of digit values
|
|
179
|
-
# @raise [NotImplementedError] always raised in base class
|
|
180
|
-
def id_digits
|
|
181
|
-
raise NotImplementedError
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# @param char [String] single character to convert
|
|
185
|
-
# @return [Integer, Array<Integer>] single digit or array of digits
|
|
186
|
-
def char_to_digits(char)
|
|
187
|
-
SecId::CHAR_TO_DIGITS.fetch(char)
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
# @param char [String] single character to convert
|
|
191
|
-
# @return [Integer] numeric value (0-38)
|
|
192
|
-
def char_to_digit(char)
|
|
193
|
-
SecId::CHAR_TO_DIGIT.fetch(char)
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
# @param sum [Integer] the sum to calculate check digit from
|
|
197
|
-
# @return [Integer] check digit (0-9)
|
|
198
|
-
def mod10(sum)
|
|
199
|
-
(10 - (sum % 10)) % 10
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
# @param number [Integer] number to split
|
|
203
|
-
# @return [Integer] sum of tens and units digits
|
|
204
|
-
def div10mod10(number)
|
|
205
|
-
(number / 10) + (number % 10)
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
# @param numeric_string [String] numeric string representation
|
|
209
|
-
# @return [Integer] check digit value (1-98)
|
|
210
|
-
def mod97(numeric_string)
|
|
211
|
-
98 - (numeric_string.to_i % 97)
|
|
212
|
-
end
|
|
213
98
|
end
|
|
214
99
|
end
|
data/lib/sec_id/cei.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SecId
|
|
4
|
+
# CUSIP Entity Identifier (CEI) - a 10-character alphanumeric code that identifies
|
|
5
|
+
# legal entities in the syndicated loan market.
|
|
6
|
+
#
|
|
7
|
+
# Format: 1 alpha + 1 digit + 7 alphanumeric + 1 check digit
|
|
8
|
+
#
|
|
9
|
+
# @see https://www.cusip.com/identifiers.html
|
|
10
|
+
#
|
|
11
|
+
# @example Validate a CEI
|
|
12
|
+
# SecId::CEI.valid?('A0BCDEFGH1') #=> true
|
|
13
|
+
class CEI < Base
|
|
14
|
+
include Checkable
|
|
15
|
+
|
|
16
|
+
# Regular expression for parsing CEI components.
|
|
17
|
+
ID_REGEX = /\A
|
|
18
|
+
(?<identifier>
|
|
19
|
+
(?<prefix>[A-Z])
|
|
20
|
+
(?<numeric>[0-9])
|
|
21
|
+
(?<entity_id>[A-Z0-9]{7}))
|
|
22
|
+
(?<check_digit>\d)?
|
|
23
|
+
\z/x
|
|
24
|
+
|
|
25
|
+
# @return [String, nil] the first character (alphabetic)
|
|
26
|
+
attr_reader :prefix
|
|
27
|
+
|
|
28
|
+
# @return [String, nil] the second character (numeric)
|
|
29
|
+
attr_reader :numeric
|
|
30
|
+
|
|
31
|
+
# @return [String, nil] the 7-character entity identifier
|
|
32
|
+
attr_reader :entity_id
|
|
33
|
+
|
|
34
|
+
# @param cei [String] the CEI string to parse
|
|
35
|
+
def initialize(cei)
|
|
36
|
+
cei_parts = parse cei
|
|
37
|
+
@identifier = cei_parts[:identifier]
|
|
38
|
+
@prefix = cei_parts[:prefix]
|
|
39
|
+
@numeric = cei_parts[:numeric]
|
|
40
|
+
@entity_id = cei_parts[:entity_id]
|
|
41
|
+
@check_digit = cei_parts[:check_digit]&.to_i
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [Integer] the calculated check digit (0-9)
|
|
45
|
+
# @raise [InvalidFormatError] if the CEI format is invalid
|
|
46
|
+
def calculate_check_digit
|
|
47
|
+
validate_format_for_calculation!
|
|
48
|
+
mod10(luhn_sum_double_add_double(reversed_digits_single(identifier)))
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|