Module: SecId::Checkable

Included in:
CEI, CUSIP, FIGI, IBAN, ISIN, LEI, SEDOL
Defined in:
lib/sec_id/concerns/checkable.rb

Overview

Provides check-digit validation and calculation for securities identifiers. Include this module in classes that have a check digit as part of their format.

Including classes must implement:

  • calculate_check_digit method that returns the calculated check digit value

This module provides:

  • Character-to-digit mapping constants

  • Luhn algorithm variants for check-digit calculation

  • valid? override that validates the check digit

  • restore! method to calculate and set the check digit

  • check_digit attribute

  • Class-level convenience methods: restore!, check_digit

Examples:

Including in an identifier class

class MyIdentifier < Base
  include Checkable

  def calculate_check_digit
    validate_format_for_calculation!
    mod10(luhn_sum_standard(reversed_digits_multi(identifier)))
  end
end

See Also:

Defined Under Namespace

Modules: ClassMethods

Constant Summary collapse

CHAR_TO_DIGITS =

Character-to-digit mapping for Luhn algorithm variants. Maps alphanumeric characters to digit arrays for multi-digit expansion. Used by ISIN for check-digit calculation.

{
  '0' => 0,      '1' => 1,      '2' => 2,      '3' => 3,      '4' => 4,
  '5' => 5,      '6' => 6,      '7' => 7,      '8' => 8,      '9' => 9,
  'A' => [1, 0], 'B' => [1, 1], 'C' => [1, 2], 'D' => [1, 3], 'E' => [1, 4],
  'F' => [1, 5], 'G' => [1, 6], 'H' => [1, 7], 'I' => [1, 8], 'J' => [1, 9],
  'K' => [2, 0], 'L' => [2, 1], 'M' => [2, 2], 'N' => [2, 3], 'O' => [2, 4],
  'P' => [2, 5], 'Q' => [2, 6], 'R' => [2, 7], 'S' => [2, 8], 'T' => [2, 9],
  'U' => [3, 0], 'V' => [3, 1], 'W' => [3, 2], 'X' => [3, 3], 'Y' => [3, 4], 'Z' => [3, 5],
  '*' => [3, 6], '@' => [3, 7], '#' => [3, 8]
}.freeze
CHAR_TO_DIGIT =

Character-to-digit mapping for single-digit conversion. Maps alphanumeric characters to values 0-38 (A=10, B=11, …, Z=35, *=36, @=37, #=38). Used by CUSIP, FIGI, SEDOL, LEI, and IBAN for check-digit calculations.

{
  '0' => 0,  '1' => 1,  '2' => 2,  '3' => 3,  '4' =>  4,
  '5' => 5,  '6' => 6,  '7' => 7,  '8' => 8,  '9' =>  9,
  'A' => 10, 'B' => 11, 'C' => 12, 'D' => 13, 'E' => 14,
  'F' => 15, 'G' => 16, 'H' => 17, 'I' => 18, 'J' => 19,
  'K' => 20, 'L' => 21, 'M' => 22, 'N' => 23, 'O' => 24,
  'P' => 25, 'Q' => 26, 'R' => 27, 'S' => 28, 'T' => 29,
  'U' => 30, 'V' => 31, 'W' => 32, 'X' => 33, 'Y' => 34, 'Z' => 35,
  '*' => 36, '@' => 37, '#' => 38
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(base) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



59
60
61
62
# File 'lib/sec_id/concerns/checkable.rb', line 59

def self.included(base)
  base.attr_reader :check_digit
  base.extend(ClassMethods)
end

Instance Method Details

#calculate_check_digitInteger

Subclasses must override this method to implement their check-digit algorithm.

Returns:

  • (Integer)

    the calculated check digit

Raises:

  • (NotImplementedError)

    if subclass doesn’t implement

  • (InvalidFormatError)

    if the identifier format is invalid



106
107
108
# File 'lib/sec_id/concerns/checkable.rb', line 106

def calculate_check_digit
  raise NotImplementedError
end

#luhn_sum_double_add_double(digits) ⇒ Integer

CUSIP/CEI style: “Double Add Double” algorithm. Processes pairs of digits, doubling the first (even-positioned from right), then summing both digit’s div10mod10 values.

Parameters:

  • digits (Array<Integer>)

    reversed array of digit values

Returns:

  • (Integer)

    the Luhn sum



122
123
124
125
126
127
# File 'lib/sec_id/concerns/checkable.rb', line 122

def luhn_sum_double_add_double(digits)
  digits.each_slice(2).reduce(0) do |sum, (even, odd)|
    double_even = (even || 0) * 2
    sum + div10mod10(double_even) + div10mod10(odd || 0)
  end
end

#luhn_sum_indexed(digits) ⇒ Integer

FIGI style: index-based doubling algorithm. Doubles odd-indexed digits (from right), then sums div10mod10 values.

Parameters:

  • digits (Array<Integer>)

    reversed array of digit values

Returns:

  • (Integer)

    the Luhn sum



134
135
136
137
138
139
# File 'lib/sec_id/concerns/checkable.rb', line 134

def luhn_sum_indexed(digits)
  digits.each_with_index.reduce(0) do |sum, (digit, index)|
    digit *= 2 if index.odd?
    sum + div10mod10(digit)
  end
end

#luhn_sum_standard(digits) ⇒ Integer

ISIN style: standard Luhn with subtract-9 for values > 9. Processes pairs of digits, doubling the first (even-positioned from right), subtracting 9 if result > 9.

Parameters:

  • digits (Array<Integer>)

    reversed array of digit values

Returns:

  • (Integer)

    the Luhn sum



147
148
149
150
151
152
153
# File 'lib/sec_id/concerns/checkable.rb', line 147

def luhn_sum_standard(digits)
  digits.each_slice(2).reduce(0) do |sum, (even, odd)|
    double_even = (even || 0) * 2
    double_even -= 9 if double_even > 9
    sum + double_even + (odd || 0)
  end
end

#restore!String

Calculates and sets the check digit, updating full_number.

Returns:

  • (String)

    the full identifier with correct check digit

Raises:



96
97
98
99
# File 'lib/sec_id/concerns/checkable.rb', line 96

def restore!
  @check_digit = calculate_check_digit
  @full_number = to_s
end

#reversed_digits_multi(id) ⇒ Array<Integer>

Converts identifier characters to reversed digit array using multi-digit mapping. Used by ISIN where letters expand to two digits.

Parameters:

  • id (String)

    the identifier string

Returns:

  • (Array<Integer>)

    reversed array of digit values



169
170
171
# File 'lib/sec_id/concerns/checkable.rb', line 169

def reversed_digits_multi(id)
  id.each_char.flat_map { |c| CHAR_TO_DIGITS.fetch(c) }.reverse!
end

#reversed_digits_single(id) ⇒ Array<Integer>

Converts identifier characters to reversed digit array using single-digit mapping. Used by CUSIP, CEI, FIGI, and SEDOL.

Parameters:

  • id (String)

    the identifier string

Returns:

  • (Array<Integer>)

    reversed array of digit values



160
161
162
# File 'lib/sec_id/concerns/checkable.rb', line 160

def reversed_digits_single(id)
  id.each_char.map { |c| CHAR_TO_DIGIT.fetch(c) }.reverse!
end

#to_sString Also known as: to_str

Returns:

  • (String)


111
112
113
# File 'lib/sec_id/concerns/checkable.rb', line 111

def to_s
  "#{identifier}#{check_digit}"
end

#valid?Boolean

Validates format and check digit.

Returns:

  • (Boolean)


86
87
88
89
90
# File 'lib/sec_id/concerns/checkable.rb', line 86

def valid?
  return false unless valid_format?

  check_digit == calculate_check_digit
end