sec_id 1.0.0 → 1.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: 0f9406f82fc8d468dff0ada3da32d7c9a3cf69d3f18e57eee32c146bf4d7057e
4
- data.tar.gz: 8b85da6d43ebd5c46daff708c0070693e16dd8f66e11a0682e4c54c0d46aeead
3
+ metadata.gz: 4f48ba902d7677a13e7d53d251473ba5a8b5c0379c06b09ceacda30df79a31d1
4
+ data.tar.gz: 1e54b0d518ca576ad853ff0d170026bbf9e0267d419c8a20209e5334a6d15276
5
5
  SHA512:
6
- metadata.gz: 867400988954ad03423b476920077c8d498ed2744cb1b863c4ab2959478609b6ce3cd8b424f4189d5d6220a9794fb3df9e3abe3c2b948273c41754e34a69f9de
7
- data.tar.gz: 0a357e424416fc0969a25fc492c05215a54dd5c8518978fb55830dbac47e656fa5b50e482e34e09b7d0cb21057bf906dda963350a826e934f77e107a727d28ca
6
+ metadata.gz: 10c3e1c32582736216141a539161a6d8aead43bdde27c4ad25ac95fd02ce4009959585248c24a740b0b8752554d36d31dfe2e3d28b1ec1ca92b9b8be2c69b1e7
7
+ data.tar.gz: 41a279f6df4045093a9c870c3ddd03845fe508d0d729bc145f59d96351b3580d51285fbae1aa0bef2c176b4c526d4452a5423796b348028ecbe63a9346430f12
data/.travis.yml CHANGED
@@ -5,7 +5,7 @@ language: ruby
5
5
 
6
6
  env:
7
7
  global:
8
- - CC_TEST_REPORTER_ID=9995f7b2aa6866b0427aef31d946ff1feded8f6e109eaf6ccb58e91c73d0bfeb
8
+ - CC_TEST_REPORTER_ID=48c33b2bc3ccdc6dafe9ed76482eca03cde47b922a7859074fc2be791247e5fd
9
9
 
10
10
  rvm:
11
11
  - 2.3
data/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ ## [1.1.0] - 2019-02-03
4
+
5
+ ### Added
6
+
7
+ - CUSIP numbers support: `SecId::CUSIP`
8
+ - CHANGELOG.md file added
9
+
10
+ ### Updated
11
+
12
+ - Char to digit conversion now uses precalculated tables instead of dynamic calculation for speed
13
+
14
+ ## [1.0.0] - 2017-10-25
15
+
16
+ ### Added
17
+
18
+ - ISIN numbers support: `SecId::ISIN`
data/README.md CHANGED
@@ -1,15 +1,18 @@
1
1
  # SecId
2
2
  [![Gem Version](https://badge.fury.io/rb/sec_id.svg)](https://badge.fury.io/rb/sec_id)
3
3
  [![Build Status](https://travis-ci.org/svyatov/sec_id.svg?branch=master)](https://travis-ci.org/svyatov/sec_id)
4
+ [![Maintainability](https://api.codeclimate.com/v1/badges/a4759963a5ddc4d55b24/maintainability)](https://codeclimate.com/github/svyatov/sec_id/maintainability)
5
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/a4759963a5ddc4d55b24/test_coverage)](https://codeclimate.com/github/svyatov/sec_id/test_coverage)
4
6
 
5
7
  Validate security identification numbers with ease!
6
8
 
7
9
  Check-digit calculation is also available.
8
10
 
9
11
  Currently supported standards:
10
- [ISIN](https://en.wikipedia.org/wiki/International_Securities_Identification_Number)
12
+ [ISIN](https://en.wikipedia.org/wiki/International_Securities_Identification_Number),
13
+ [CUSIP](https://en.wikipedia.org/wiki/CUSIP)
11
14
 
12
- WIP: [CUSIP](https://en.wikipedia.org/wiki/CUSIP),
15
+ WIP:
13
16
  [SEDOL](https://en.wikipedia.org/wiki/SEDOL),
14
17
  [IBAN](https://en.wikipedia.org/wiki/International_Bank_Account_Number).
15
18
 
@@ -18,7 +21,7 @@ WIP: [CUSIP](https://en.wikipedia.org/wiki/CUSIP),
18
21
  Add this line to your application's Gemfile:
19
22
 
20
23
  ```ruby
21
- gem 'sec_id', '~> 1.0'
24
+ gem 'sec_id', '~> 1.1'
22
25
  ```
23
26
 
24
27
  And then execute:
@@ -114,6 +117,27 @@ isin.restore! # => 'US5949181045'
114
117
  isin.calculate_check_digit # => 5
115
118
  ```
116
119
 
120
+ ### SecId::CUSIP full example
121
+
122
+ ```ruby
123
+ # class level
124
+ SecId::CUSIP.valid?('594918104') # => true
125
+ SecId::CUSIP.valid_format?('59491810') # => true
126
+ SecId::CUSIP.restore!('59491810') # => '594918104'
127
+ SecId::CUSIP.check_digit('59491810') # => 5
128
+
129
+ # instance level
130
+ cusip = SecId::CUSIP.new('594918104')
131
+ cusip.cusip # => '594918104'
132
+ cusip.cusip6 # => '594918'
133
+ cusip.issue # => '10'
134
+ cusip.check_digit # => 4
135
+ cusip.valid? # => true
136
+ cusip.valid_format? # => true
137
+ cusip.restore! # => '594918104'
138
+ cusip.calculate_check_digit # => 4
139
+ ```
140
+
117
141
  ## Development
118
142
 
119
143
  After checking out the repo, run `bin/setup` to install dependencies.
data/lib/sec_id.rb CHANGED
@@ -4,6 +4,7 @@ require 'set'
4
4
  require 'sec_id/version'
5
5
  require 'sec_id/base'
6
6
  require 'sec_id/isin'
7
+ require 'sec_id/cusip'
7
8
 
8
9
  module SecId
9
10
  Error = Class.new(StandardError)
data/lib/sec_id/base.rb CHANGED
@@ -2,7 +2,27 @@
2
2
 
3
3
  module SecId
4
4
  class Base
5
- LETTERS = Set.new('A'..'Z').freeze
5
+ CHAR_TO_DIGITS = {
6
+ '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4,
7
+ '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9,
8
+ 'A' => [1, 0], 'B' => [1, 1], 'C' => [1, 2], 'D' => [1, 3], 'E' => [1, 4],
9
+ 'F' => [1, 5], 'G' => [1, 6], 'H' => [1, 7], 'I' => [1, 8], 'J' => [1, 9],
10
+ 'K' => [2, 0], 'L' => [2, 1], 'M' => [2, 2], 'N' => [2, 3], 'O' => [2, 4],
11
+ 'P' => [2, 5], 'Q' => [2, 6], 'R' => [2, 7], 'S' => [2, 8], 'T' => [2, 9],
12
+ 'U' => [3, 0], 'V' => [3, 1], 'W' => [3, 2], 'X' => [3, 3], 'Y' => [3, 4], 'Z' => [3, 5],
13
+ '*' => [3, 6], '@' => [3, 7], '#' => [3, 8]
14
+ }.freeze
15
+
16
+ CHAR_TO_DIGIT = {
17
+ '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4,
18
+ '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9,
19
+ 'A' => 10, 'B' => 11, 'C' => 12, 'D' => 13, 'E' => 14,
20
+ 'F' => 15, 'G' => 16, 'H' => 17, 'I' => 18, 'J' => 19,
21
+ 'K' => 20, 'L' => 21, 'M' => 22, 'N' => 23, 'O' => 24,
22
+ 'P' => 25, 'Q' => 26, 'R' => 27, 'S' => 28, 'T' => 29,
23
+ 'U' => 30, 'V' => 31, 'W' => 32, 'X' => 33, 'Y' => 34, 'Z' => 35,
24
+ '*' => 36, '@' => 37, '#' => 38
25
+ }.freeze
6
26
 
7
27
  attr_reader :identifier, :check_digit
8
28
 
@@ -50,14 +70,15 @@ module SecId
50
70
  private
51
71
 
52
72
  def digitized_identifier
53
- @digitized_identifier ||= identifier.each_char.flat_map { |char| char_to_digits char }
73
+ raise NotImplementedError
54
74
  end
55
75
 
56
76
  def char_to_digits(char)
57
- return char.to_i unless LETTERS.include? char
77
+ CHAR_TO_DIGITS.fetch(char)
78
+ end
58
79
 
59
- number = char.to_i(36)
60
- [number / 10, number % 10]
80
+ def char_to_digit(char)
81
+ CHAR_TO_DIGIT.fetch(char)
61
82
  end
62
83
 
63
84
  def mod_10(sum)
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SecId
4
+ # https://en.wikipedia.org/wiki/CUSIP
5
+ class CUSIP < Base
6
+ ID_REGEX = /\A
7
+ (?<identifier>
8
+ (?<cusip6>[A-Z0-9]{5}[A-Z0-9*@#])
9
+ (?<issue>[A-Z0-9*@#]{2}))
10
+ (?<check_digit>\d)?
11
+ \z/x.freeze
12
+
13
+ VALID_COUNTRY_CODES_FOR_CONVERSION_TO_ISIN = %w[US CA].freeze
14
+
15
+ attr_reader :cusip, :cusip6, :issue
16
+
17
+ def initialize(cusip)
18
+ @cusip = cusip.to_s.strip.upcase
19
+ cusip_parts = @cusip.match(ID_REGEX) || {}
20
+
21
+ @identifier = cusip_parts[:identifier]
22
+ @cusip6 = cusip_parts[:cusip6]
23
+ @issue = cusip_parts[:issue]
24
+ @check_digit = cusip_parts[:check_digit].to_i if cusip_parts[:check_digit]
25
+ end
26
+
27
+ def valid?
28
+ return false unless valid_format?
29
+
30
+ check_digit == calculate_check_digit
31
+ end
32
+
33
+ def valid_format?
34
+ identifier ? true : false
35
+ end
36
+
37
+ def restore!
38
+ @check_digit = calculate_check_digit
39
+ @cusip = to_s
40
+ end
41
+
42
+ def calculate_check_digit
43
+ return mod_10(modified_luhn_sum) if valid_format?
44
+
45
+ raise InvalidFormatError, "CUSIP '#{cusip}' is invalid and check-digit cannot be calculated!"
46
+ end
47
+
48
+ private
49
+
50
+ # https://en.wikipedia.org/wiki/Luhn_algorithm
51
+ def modified_luhn_sum
52
+ sum = 0
53
+
54
+ digitized_identifier.reverse.each_slice(2) do |even, odd|
55
+ double_even = (even || 0) * 2
56
+ double_even -= 9 if double_even > 9
57
+ sum += div_10_mod_10(double_even) + div_10_mod_10(odd || 0)
58
+ end
59
+
60
+ sum
61
+ end
62
+
63
+ def digitized_identifier
64
+ @digitized_identifier ||= identifier.each_char.map(&method(:char_to_digit))
65
+ end
66
+
67
+ def div_10_mod_10(number)
68
+ (number / 10) + (number % 10)
69
+ end
70
+ end
71
+ end
data/lib/sec_id/isin.rb CHANGED
@@ -38,9 +38,9 @@ module SecId
38
38
  end
39
39
 
40
40
  def calculate_check_digit
41
- raise InvalidFormatError, "ISIN '#{isin}' is invalid and check-digit cannot be calculated!" unless valid_format?
41
+ return mod_10(luhn_sum) if valid_format?
42
42
 
43
- mod_10 luhn_sum
43
+ raise InvalidFormatError, "ISIN '#{isin}' is invalid and check-digit cannot be calculated!"
44
44
  end
45
45
 
46
46
  private
@@ -57,5 +57,9 @@ module SecId
57
57
 
58
58
  sum
59
59
  end
60
+
61
+ def digitized_identifier
62
+ @digitized_identifier ||= identifier.each_char.flat_map(&method(:char_to_digits))
63
+ end
60
64
  end
61
65
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SecId
4
- VERSION = '1.0.0'
4
+ VERSION = '1.1.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sec_id
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leonid Svyatov
@@ -106,6 +106,7 @@ files:
106
106
  - ".rspec"
107
107
  - ".rubocop.yml"
108
108
  - ".travis.yml"
109
+ - CHANGELOG.md
109
110
  - Gemfile
110
111
  - LICENSE.txt
111
112
  - README.md
@@ -114,6 +115,7 @@ files:
114
115
  - bin/setup
115
116
  - lib/sec_id.rb
116
117
  - lib/sec_id/base.rb
118
+ - lib/sec_id/cusip.rb
117
119
  - lib/sec_id/isin.rb
118
120
  - lib/sec_id/version.rb
119
121
  - sec_id.gemspec