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.
data/lib/sec_id/isin.rb CHANGED
@@ -14,6 +14,8 @@ module SecId
14
14
  # @example Restore check digit
15
15
  # SecId::ISIN.restore!('US594918104') #=> 'US5949181045'
16
16
  class ISIN < Base
17
+ include Checkable
18
+
17
19
  # Regular expression for parsing ISIN components.
18
20
  ID_REGEX = /\A
19
21
  (?<identifier>
@@ -25,12 +27,35 @@ module SecId
25
27
  # Country codes that use CUSIP Global Services (CGS) for NSIN assignment.
26
28
  CGS_COUNTRY_CODES = Set.new(
27
29
  %w[
28
- US CA AG AI AN AR AS AW BB BL BM BO BQ BR BS BZ CL CO CR CW DM DO EC FM
29
- GD GS GU GY HN HT JM KN KY LC MF MH MP MX NI PA PE PH PR PW PY SR SV SX
30
- TT UM UY VC VE VG VI YT
30
+ US CA AG AI AN AR AS AW BB BL BM BO BQ BS BZ CL CO CR CW DM DO EC FM GD
31
+ GS GU GY HN HT JM KN KY LC MF MH MP MX NI PA PE PH PR PW PY SR SV SX TT
32
+ UM UY VC VE VG VI YT
31
33
  ]
32
34
  ).freeze
33
35
 
36
+ # Maps country codes to their NSIN identifier types.
37
+ # Countries not in this map return :generic (CGS countries handled via {#cgs?}).
38
+ NSIN_COUNTRY_TYPES = {
39
+ # SEDOL countries (UK, Ireland, Crown Dependencies, and Overseas Territories)
40
+ 'GB' => :sedol,
41
+ 'IE' => :sedol,
42
+ 'GG' => :sedol,
43
+ 'IM' => :sedol,
44
+ 'JE' => :sedol,
45
+ 'FK' => :sedol,
46
+ # WKN country (Germany)
47
+ 'DE' => :wkn,
48
+ # Valoren countries (Switzerland and Liechtenstein)
49
+ 'CH' => :valoren,
50
+ 'LI' => :valoren
51
+ }.freeze
52
+
53
+ # Country codes that use SEDOL as their national identifier.
54
+ SEDOL_COUNTRY_CODES = Set.new(%w[GB IE IM JE GG FK]).freeze
55
+
56
+ # Country codes that use Valoren as their national identifier.
57
+ VALOREN_COUNTRY_CODES = Set.new(%w[CH LI]).freeze
58
+
34
59
  # @return [String, nil] the ISO 3166-1 alpha-2 country code
35
60
  attr_reader :country_code
36
61
 
@@ -50,7 +75,7 @@ module SecId
50
75
  # @raise [InvalidFormatError] if the ISIN format is invalid
51
76
  def calculate_check_digit
52
77
  validate_format_for_calculation!
53
- mod10(luhn_sum)
78
+ mod10(luhn_sum_standard(reversed_digits_multi(identifier)))
54
79
  end
55
80
 
56
81
  # @return [CUSIP] a new CUSIP instance
@@ -66,21 +91,68 @@ module SecId
66
91
  CGS_COUNTRY_CODES.include?(country_code)
67
92
  end
68
93
 
69
- private
94
+ # @return [Boolean] true if the country code uses SEDOL
95
+ def sedol?
96
+ SEDOL_COUNTRY_CODES.include?(country_code)
97
+ end
70
98
 
71
- # @return [Integer] the Luhn sum
72
- # @see https://en.wikipedia.org/wiki/Luhn_algorithm
73
- def luhn_sum
74
- reversed_id_digits.each_slice(2).reduce(0) do |sum, (even, odd)|
75
- double_even = (even || 0) * 2
76
- double_even -= 9 if double_even > 9
77
- sum + double_even + (odd || 0)
78
- end
99
+ # @return [Boolean] true if the country code uses WKN
100
+ def wkn?
101
+ country_code == 'DE'
102
+ end
103
+
104
+ # @return [Boolean] true if the country code uses Valoren
105
+ def valoren?
106
+ VALOREN_COUNTRY_CODES.include?(country_code)
107
+ end
108
+
109
+ # @return [SEDOL] a new SEDOL instance
110
+ # @raise [InvalidFormatError] if the country code is not valid for SEDOL
111
+ def to_sedol
112
+ raise InvalidFormatError, "'#{country_code}' is not a SEDOL country code!" unless sedol?
113
+
114
+ SEDOL.new(nsin[2..])
79
115
  end
80
116
 
81
- # @return [Array<Integer>] the reversed digit array
82
- def reversed_id_digits
83
- identifier.each_char.flat_map(&method(:char_to_digits)).reverse!
117
+ # @return [WKN] a new WKN instance
118
+ # @raise [InvalidFormatError] if the country code is not DE
119
+ def to_wkn
120
+ raise InvalidFormatError, "'#{country_code}' is not a WKN country code!" unless wkn?
121
+
122
+ WKN.new(nsin[3..])
123
+ end
124
+
125
+ # @return [Valoren] a new Valoren instance
126
+ # @raise [InvalidFormatError] if the country code is not CH or LI
127
+ def to_valoren
128
+ raise InvalidFormatError, "'#{country_code}' is not a Valoren country code!" unless valoren?
129
+
130
+ Valoren.new(nsin)
131
+ end
132
+
133
+ # Returns the type of NSIN embedded in this ISIN.
134
+ #
135
+ # @return [Symbol] :cusip, :sedol, :wkn, :valoren, or :generic
136
+ def nsin_type
137
+ return :cusip if cgs?
138
+
139
+ NSIN_COUNTRY_TYPES.fetch(country_code, :generic)
140
+ end
141
+
142
+ # Extracts the national identifier from this ISIN.
143
+ #
144
+ # @return [CUSIP, SEDOL, WKN, Valoren, String] the extracted identifier
145
+ # @raise [InvalidFormatError] if ISIN format is invalid
146
+ def to_nsin
147
+ raise InvalidFormatError, 'Invalid ISIN format' unless valid_format?
148
+
149
+ case nsin_type
150
+ when :cusip then to_cusip
151
+ when :sedol then SEDOL.new(nsin[2..])
152
+ when :wkn then WKN.new(nsin[3..])
153
+ when :valoren then Valoren.new(nsin)
154
+ else nsin # :generic - return raw string
155
+ end
84
156
  end
85
157
  end
86
158
  end
data/lib/sec_id/lei.rb CHANGED
@@ -15,6 +15,8 @@ module SecId
15
15
  # @example Calculate check digit
16
16
  # SecId::LEI.check_digit('529900T8BM49AURSDO') #=> 55
17
17
  class LEI < Base
18
+ include Checkable
19
+
18
20
  # Regular expression for parsing LEI components.
19
21
  ID_REGEX = /\A
20
22
  (?<identifier>
@@ -61,7 +63,7 @@ module SecId
61
63
 
62
64
  # @return [String] the numeric string representation
63
65
  def numeric_identifier
64
- identifier.each_char.map { |char| char_to_digit(char) }.join
66
+ identifier.each_char.map { |char| CHAR_TO_DIGIT.fetch(char) }.join
65
67
  end
66
68
  end
67
69
  end
data/lib/sec_id/occ.rb CHANGED
@@ -97,12 +97,6 @@ module SecId
97
97
  @date_str = symbol_parts[:date]
98
98
  @type = symbol_parts[:type]
99
99
  @strike_mills = symbol_parts[:strike_mills]
100
- @check_digit = nil
101
- end
102
-
103
- # @return [Boolean] always false
104
- def has_check_digit?
105
- false
106
100
  end
107
101
 
108
102
  # Normalizes the OCC symbol to standard format with 6-char padded underlying and 8-digit strike.
data/lib/sec_id/sedol.rb CHANGED
@@ -15,6 +15,8 @@ module SecId
15
15
  # @example Calculate check digit
16
16
  # SecId::SEDOL.check_digit('B19GKT') #=> 4
17
17
  class SEDOL < Base
18
+ include Checkable
19
+
18
20
  # Regular expression for parsing SEDOL components.
19
21
  # Excludes vowels (A, E, I, O, U) from valid characters.
20
22
  ID_REGEX = /\A
@@ -32,6 +34,23 @@ module SecId
32
34
  @check_digit = sedol_parts[:check_digit]&.to_i
33
35
  end
34
36
 
37
+ # Valid country codes for SEDOL to ISIN conversion.
38
+ ISIN_COUNTRY_CODES = Set.new(%w[GB IE GG IM JE FK]).freeze
39
+
40
+ # @param country_code [String] the ISO 3166-1 alpha-2 country code (default: 'GB')
41
+ # @return [ISIN] a new ISIN instance with calculated check digit
42
+ # @raise [InvalidFormatError] if the country code is not valid for SEDOL
43
+ def to_isin(country_code = 'GB')
44
+ unless ISIN_COUNTRY_CODES.include?(country_code)
45
+ raise InvalidFormatError, "'#{country_code}' is not a valid SEDOL country code!"
46
+ end
47
+
48
+ restore!
49
+ isin = ISIN.new("#{country_code}00#{full_number}")
50
+ isin.restore!
51
+ isin
52
+ end
53
+
35
54
  # @return [Integer] the calculated check digit (0-9)
36
55
  # @raise [InvalidFormatError] if the SEDOL format is invalid
37
56
  def calculate_check_digit
@@ -58,7 +77,7 @@ module SecId
58
77
 
59
78
  # @return [Array<Integer>] array of digit values
60
79
  def id_digits
61
- @id_digits ||= identifier.each_char.map(&method(:char_to_digit))
80
+ @id_digits ||= identifier.each_char.map { |c| CHAR_TO_DIGIT.fetch(c) }
62
81
  end
63
82
  end
64
83
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SecId
4
+ # Valoren (Swiss Security Number) - a numeric identifier for securities
5
+ # in Switzerland, Liechtenstein, and Belgium.
6
+ #
7
+ # Format: 5-9 numeric digits
8
+ #
9
+ # @note Valoren identifiers have no check digit. The {#has_check_digit?} method
10
+ # returns false and validation is based solely on format.
11
+ #
12
+ # @see https://en.wikipedia.org/wiki/Valoren_number
13
+ #
14
+ # @example Validate a Valoren
15
+ # SecId::Valoren.valid?('3886335') #=> true
16
+ # SecId::Valoren.valid?('003886335') #=> true
17
+ #
18
+ # @example Normalize a Valoren to 9 digits
19
+ # SecId::Valoren.normalize!('3886335') #=> '003886335'
20
+ class Valoren < Base
21
+ include Normalizable
22
+
23
+ # Regular expression for parsing Valoren components.
24
+ ID_REGEX = /\A
25
+ (?=\d{5,9}\z)(?<padding>0*)(?<identifier>[1-9]\d{4,8})
26
+ \z/x
27
+
28
+ # Valid country codes for Valoren to ISIN conversion.
29
+ ISIN_COUNTRY_CODES = Set.new(%w[CH LI]).freeze
30
+
31
+ # @return [String, nil] the leading zeros in the Valoren
32
+ attr_reader :padding
33
+
34
+ # @param valoren [String, Integer] the Valoren to parse
35
+ def initialize(valoren)
36
+ valoren_parts = parse(valoren)
37
+ @padding = valoren_parts[:padding]
38
+ @identifier = valoren_parts[:identifier]
39
+ end
40
+
41
+ # @param country_code [String] the ISO 3166-1 alpha-2 country code (default: 'CH')
42
+ # @return [ISIN] a new ISIN instance with calculated check digit
43
+ # @raise [InvalidFormatError] if the country code is not CH or LI
44
+ def to_isin(country_code = 'CH')
45
+ unless ISIN_COUNTRY_CODES.include?(country_code)
46
+ raise InvalidFormatError, "'#{country_code}' is not a valid Valoren country code!"
47
+ end
48
+
49
+ normalize!
50
+ isin = ISIN.new(country_code + full_number)
51
+ isin.restore!
52
+ isin
53
+ end
54
+
55
+ # Normalizes the Valoren to a 9-digit zero-padded format.
56
+ # Updates both @full_number and @padding to reflect the normalized state.
57
+ #
58
+ # @return [String] the normalized 9-digit Valoren
59
+ # @raise [InvalidFormatError] if the Valoren format is invalid
60
+ def normalize!
61
+ raise InvalidFormatError, "Valoren '#{full_number}' is invalid and cannot be normalized!" unless valid_format?
62
+
63
+ @full_number = @identifier.rjust(9, '0')
64
+ @padding = @full_number[0, 9 - @identifier.length]
65
+ @full_number
66
+ end
67
+
68
+ # @return [String]
69
+ def to_s
70
+ full_number
71
+ end
72
+ alias to_str to_s
73
+ end
74
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SecId
4
- VERSION = '4.3.0'
4
+ VERSION = '4.4.0'
5
5
  end
data/lib/sec_id/wkn.rb ADDED
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SecId
4
+ # Wertpapierkennnummer (WKN) - a 6-character alphanumeric code
5
+ # used to identify securities in Germany.
6
+ #
7
+ # Format: 6 alphanumeric characters (excluding I and O)
8
+ # Note: WKN excludes letters I and O to avoid confusion with 1 and 0.
9
+ #
10
+ # @see https://en.wikipedia.org/wiki/Wertpapierkennnummer
11
+ #
12
+ # @example Validate a WKN
13
+ # SecId::WKN.valid?('514000') #=> true
14
+ # SecId::WKN.valid?('CBK100') #=> true
15
+ class WKN < Base
16
+ # Regular expression for parsing WKN components.
17
+ # Excludes letters I and O to avoid confusion with 1 and 0.
18
+ ID_REGEX = /\A
19
+ (?<identifier>[0-9A-HJ-NP-Z]{6})
20
+ \z/x
21
+
22
+ # @param wkn [String] the WKN string to parse
23
+ def initialize(wkn)
24
+ wkn_parts = parse(wkn)
25
+ @identifier = wkn_parts[:identifier]
26
+ end
27
+
28
+ # @param country_code [String] the ISO 3166-1 alpha-2 country code (default: 'DE')
29
+ # @return [ISIN] a new ISIN instance with calculated check digit
30
+ # @raise [InvalidFormatError] if the country code is not DE
31
+ # @raise [InvalidFormatError] if the WKN format is invalid
32
+ def to_isin(country_code = 'DE')
33
+ raise InvalidFormatError, "'#{country_code}' is not a valid WKN country code!" unless country_code == 'DE'
34
+ raise InvalidFormatError, "WKN '#{full_number}' is invalid!" unless valid_format?
35
+
36
+ isin = ISIN.new("#{country_code}000#{identifier}")
37
+ isin.restore!
38
+ isin
39
+ end
40
+ end
41
+ end
data/lib/sec_id.rb CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  require 'set'
4
4
  require 'sec_id/version'
5
- require 'sec_id/normalizable'
5
+ require 'sec_id/concerns/normalizable'
6
+ require 'sec_id/concerns/checkable'
6
7
  require 'sec_id/base'
7
8
  require 'sec_id/isin'
8
9
  require 'sec_id/cusip'
@@ -12,8 +13,13 @@ require 'sec_id/lei'
12
13
  require 'sec_id/iban'
13
14
  require 'sec_id/cik'
14
15
  require 'sec_id/occ'
16
+ require 'sec_id/wkn'
17
+ require 'sec_id/valoren'
18
+ require 'sec_id/cei'
19
+ require 'sec_id/cfi'
20
+ require 'sec_id/fisn'
15
21
 
16
22
  module SecId
17
- Error = Class.new(StandardError)
18
- InvalidFormatError = Class.new(Error)
23
+ class Error < StandardError; end
24
+ class InvalidFormatError < Error; end
19
25
  end
data/sec_id.gemspec CHANGED
@@ -12,7 +12,8 @@ Gem::Specification.new do |spec|
12
12
 
13
13
  spec.summary = 'Validate securities identification numbers with ease!'
14
14
  spec.description = 'Validate, calculate check digits, and parse components of securities identifiers. ' \
15
- 'Supports ISIN, CUSIP, SEDOL, FIGI, LEI, IBAN, CIK, and OCC standards.'
15
+ 'Supports ISIN, CUSIP, CEI, SEDOL, FIGI, LEI, IBAN, CIK, OCC, WKN, Valoren, CFI, ' \
16
+ 'and FISN standards.'
16
17
  spec.homepage = 'https://github.com/svyatov/sec_id'
17
18
  spec.license = 'MIT'
18
19
 
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: 4.3.0
4
+ version: 4.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leonid Svyatov
@@ -10,7 +10,8 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: Validate, calculate check digits, and parse components of securities
13
- identifiers. Supports ISIN, CUSIP, SEDOL, FIGI, LEI, IBAN, CIK, and OCC standards.
13
+ identifiers. Supports ISIN, CUSIP, CEI, SEDOL, FIGI, LEI, IBAN, CIK, OCC, WKN, Valoren,
14
+ CFI, and FISN standards.
14
15
  email:
15
16
  - leonid@svyatov.ru
16
17
  executables: []
@@ -22,17 +23,23 @@ files:
22
23
  - README.md
23
24
  - lib/sec_id.rb
24
25
  - lib/sec_id/base.rb
26
+ - lib/sec_id/cei.rb
27
+ - lib/sec_id/cfi.rb
25
28
  - lib/sec_id/cik.rb
29
+ - lib/sec_id/concerns/checkable.rb
30
+ - lib/sec_id/concerns/normalizable.rb
26
31
  - lib/sec_id/cusip.rb
27
32
  - lib/sec_id/figi.rb
33
+ - lib/sec_id/fisn.rb
28
34
  - lib/sec_id/iban.rb
29
35
  - lib/sec_id/iban/country_rules.rb
30
36
  - lib/sec_id/isin.rb
31
37
  - lib/sec_id/lei.rb
32
- - lib/sec_id/normalizable.rb
33
38
  - lib/sec_id/occ.rb
34
39
  - lib/sec_id/sedol.rb
40
+ - lib/sec_id/valoren.rb
35
41
  - lib/sec_id/version.rb
42
+ - lib/sec_id/wkn.rb
36
43
  - sec_id.gemspec
37
44
  homepage: https://github.com/svyatov/sec_id
38
45
  licenses:
@@ -53,7 +60,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
53
60
  - !ruby/object:Gem::Version
54
61
  version: '0'
55
62
  requirements: []
56
- rubygems_version: 4.0.3
63
+ rubygems_version: 4.0.4
57
64
  specification_version: 4
58
65
  summary: Validate securities identification numbers with ease!
59
66
  test_files: []