Class: BankTools::SE::OCR

Inherits:
Object
  • Object
show all
Defined in:
lib/banktools-se/ocr.rb

Defined Under Namespace

Classes: BadChecksum, BadLengthDigit, BadPadding, InvalidOCR, MustBeNumeric, OverlongOCR, TooShortOCR

Constant Summary collapse

MIN_LENGTH =
2
MAX_LENGTH =
25

Class Method Summary collapse

Class Method Details

.find_all_in_string(string, length_digit: false, pad: "", min_length: 4, max_length: 18) ⇒ Object

max_length is 18, because the biggest allowed integer by default in a Postgres integer column (“bigint”) is 19 digits long, as is the next (disallowed) number. Attempting some queries with longer OCRs may cause Ruby on Rails exceptions.



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/banktools-se/ocr.rb', line 68

def self.find_all_in_string(string, length_digit: false, pad: "", min_length: 4, max_length: 18)
  # First, treat the input as one long string of digits.
  # E.g. "1234 and 5678" becomes "12345678".

  digit_string = string.gsub(/\D/, "")

  # Then find all substrings ("n-grams") of min_length, and of all other lengths, up to max_length.
  # So e.g. find all four-digit substrings ("1234", "2345", …), all five-digit substrings and so on.

  digit_string_length = digit_string.length
  candidates = []

  0.upto(digit_string.length - min_length) do |start_pos|
    min_end_pos = start_pos + min_length - 1
    max_end_pos = [ start_pos + max_length, digit_string_length ].min - 1

    min_end_pos.upto(max_end_pos) do |end_pos|
      candidates << digit_string.slice(start_pos..end_pos)
    end
  end

  # Get rid of any duplicates.

  candidates = candidates.uniq

  # Finally, limit these substrings to ones that are actually valid OCRs.

  candidates.select { |candidate|
    begin
      to_number(candidate, length_digit: length_digit, pad: pad)
      true
    rescue InvalidOCR
      false
    end
  }
end

.from_number(number, length_digit: false, pad: "") ⇒ Object

Raises:



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/banktools-se/ocr.rb', line 17

def self.from_number(number, length_digit: false, pad: "")
  number = number.to_s
  add_length_digit = length_digit
  pad = pad.to_s

  raise MustBeNumeric unless number.match(/\A\d+\z/)
  # Padding isn't something BGC specifies, but we needed it to support a legacy scheme.
  number += pad
  # Adding 2: 1 length digit, 1 check digit.
  number += ((number.length + 2) % 10).to_s if add_length_digit

  number_with_ocr = number + Utils.luhn_checksum(number).to_s

  length = number_with_ocr.length
  if length > MAX_LENGTH
    raise OverlongOCR, "OCR must be #{MIN_LENGTH} - #{MAX_LENGTH} characters (this one would be #{length} characters)"
  end

  number_with_ocr
end

.to_number(ocr, length_digit: false, pad: "") ⇒ Object

Raises:



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/banktools-se/ocr.rb', line 38

def self.to_number(ocr, length_digit: false, pad: "")
  ocr = ocr.to_s
  should_have_length_digit = length_digit
  strip_padding = pad.to_s

  raise MustBeNumeric unless ocr.match(/\A\d+\z/)
  raise BadChecksum unless Utils.valid_luhn?(ocr)
  raise TooShortOCR if ocr.length < MIN_LENGTH

  if should_have_length_digit
    length_digit = ocr[-2]
    last_digit_of_actual_length = ocr.length.to_s[-1]
    raise BadLengthDigit if length_digit != last_digit_of_actual_length
  end

  digits_to_chop  = 1  # Checksum.
  digits_to_chop += 1 if should_have_length_digit

  if strip_padding.length > 0
    expected_padding_end = -digits_to_chop - 1
    expected_padding_start = expected_padding_end - strip_padding.length + 1
    raise BadPadding if ocr[expected_padding_start..expected_padding_end] != strip_padding
  end

  digits_to_chop += strip_padding.length

  ocr[0...-digits_to_chop]
end