konto_check_ruby 1.0.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.
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is part of konto_check_ruby, a Ruby port of the C library
4
+ # konto_check, Copyright (C) 2002-2023 Michael Plugge <konto_check@yahoo.com>.
5
+ # Ruby port Copyright (C) 2026 tickettoaster GmbH <https://tickettoaster.de>.
6
+ #
7
+ # konto_check_ruby is free software; you can redistribute it and/or modify it
8
+ # under the terms of the GNU Lesser General Public License as published by
9
+ # the Free Software Foundation; either version 2.1 of the License, or (at
10
+ # your option) any later version. It is distributed WITHOUT ANY WARRANTY; see
11
+ # the file LICENSE for details.
12
+
13
+ require_relative "retvals"
14
+
15
+ module KontoCheckRuby
16
+ # Port of kto_check_int() from konto_check.c: the check-digit methods
17
+ # 00..E4 of the Deutsche Bundesbank.
18
+ #
19
+ # Every C `case N:` label of the big switch in kto_check_int() becomes a
20
+ # method `mN(kto, blz, um, rv)` in this module (see check_methods/part*.rb).
21
+ #
22
+ # kto : Array of 10 Integers (digits 0..9), the account number right
23
+ # aligned and left padded with zeros. Methods may modify it.
24
+ # blz : String with the bank code (8 digits) or nil
25
+ # um : Integer, requested sub-method (0 = none, 1 = 'a', 2 = 'b', ...)
26
+ # rv : Retvals struct (methode, pz_methode, pz, pz_pos) or nil
27
+ #
28
+ # Return value is one of the integer return codes (OK, FALSE, INVALID_KTO, ...).
29
+ module CheckMethods
30
+ extend self
31
+
32
+ Retvals = Struct.new(:methode, :pz_methode, :pz, :pz_pos)
33
+
34
+ # Weights for methods 27, 29 and 69 (m10h_digits in C)
35
+ M10H_DIGITS = [
36
+ [0, 1, 5, 9, 3, 7, 4, 8, 2, 6],
37
+ [0, 1, 7, 6, 9, 8, 3, 2, 5, 4],
38
+ [0, 1, 8, 4, 6, 2, 9, 5, 7, 3],
39
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
40
+ ].freeze
41
+
42
+ # Tables for method 87
43
+ TAB1 = [0, 4, 3, 2, 6].freeze
44
+ TAB2 = [7, 1, 5, 9, 8].freeze
45
+
46
+ # Weights for methods 24, 52/53/B6/C0 and 93
47
+ W52 = [2, 4, 8, 5, 10, 9, 7, 3, 6, 1, 2, 4, 0, 0, 0, 0].freeze
48
+ W24 = [1, 2, 3, 1, 2, 3, 1, 2, 3].freeze
49
+ W93 = [2, 3, 4, 5, 6, 7, 2, 3, 4].freeze
50
+
51
+ # Methods that may contain omitted sub-account numbers (uk_pz_methoden in C)
52
+ UK_PZ_METHODEN = [13, 26, 50, 63, 76, 127].freeze
53
+
54
+ # Converts the account number string into an array of 10 digits, exactly
55
+ # as the prolog of kto_check_int() does: leading zeros, blanks and tabs
56
+ # are skipped, the number ends at the first blank/tab, it must have 1..10
57
+ # characters and is right aligned (zero padded) in a 10 digit field.
58
+ # Returns nil if the length is invalid.
59
+ def normalize_kto(kto)
60
+ s = kto.to_s
61
+ i = 0
62
+ i += 1 while i < s.length && (s[i] == "0" || s[i] == " " || s[i] == "\t")
63
+ j = i
64
+ j += 1 while j < s.length && s[j] != " " && s[j] != "\t"
65
+ len = j - i
66
+ return nil if len < 1 || len > 10
67
+ digits = Array.new(10, 0)
68
+ k = 10 - len
69
+ s.byteslice(i, len).each_byte do |b|
70
+ digits[k] = b - 48
71
+ k += 1
72
+ end
73
+ digits
74
+ end
75
+
76
+ # Converts a method string like "00", "51", "b6", "13a", "C0" into the
77
+ # numeric method used by the C library (bx2/b1/by4 tables) and the
78
+ # sub-method. Returns [pz_methode, untermethode] or nil for invalid input.
79
+ def parse_method(pz)
80
+ s = pz.to_s
81
+ return nil if s.length < 2 || s.length > 3
82
+ c1 = alnum_value(s[0])
83
+ c2 = s[1] =~ /\A\d\z/ ? s[1].to_i : nil
84
+ return nil if c1.nil? || c2.nil?
85
+ methode = c1 * 10 + c2
86
+ um = 0
87
+ if s.length == 3
88
+ um = letter_value(s[2])
89
+ return nil if um.nil?
90
+ methode += um * 1000
91
+ end
92
+ [methode, um]
93
+ end
94
+
95
+ # Numeric method -> two character method string ("00".."99", "A0".."E4")
96
+ def method_string(pz_methode)
97
+ base = pz_methode % 1000
98
+ um = pz_methode / 1000
99
+ s = base < 100 ? format("%02d", base) : ((base / 10) - 10 + 65).chr + (base % 10).to_s
100
+ s += (96 + um).chr if um > 0
101
+ s
102
+ end
103
+
104
+ # Runs check method pz_methode (an integer as in the C switch, e.g. 13,
105
+ # 1013, 2013) for the account number kto (String). Returns the result code.
106
+ def check(pz_methode, kto, blz = nil, um = 0, rv = nil)
107
+ digits = normalize_kto(kto)
108
+ return INVALID_KTO_LENGTH unless digits
109
+ check_digits(pz_methode, digits, blz, um, rv)
110
+ end
111
+
112
+ # Same as check(), but with an already normalized digit array.
113
+ def check_digits(pz_methode, digits, blz = nil, um = 0, rv = nil)
114
+ meth = :"m#{pz_methode}"
115
+ if respond_to?(meth)
116
+ send(meth, digits, blz, um, rv)
117
+ else
118
+ # default branch of the C switch
119
+ if rv
120
+ rv.methode = "(-)"
121
+ rv.pz_methode = -1
122
+ end
123
+ return UNDEFINED_SUBMETHOD if um && um != 0
124
+ NOT_IMPLEMENTED
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ def alnum_value(c)
131
+ case c
132
+ when /\A\d\z/ then c.to_i
133
+ when /\A[a-z]\z/ then c.ord - 97 + 10
134
+ when /\A[A-Z]\z/ then c.ord - 65 + 10
135
+ end
136
+ end
137
+
138
+ def letter_value(c)
139
+ case c
140
+ when /\A[a-z]\z/ then c.ord - 96
141
+ when /\A[A-Z]\z/ then c.ord - 64
142
+ end
143
+ end
144
+ end
145
+ end
146
+
147
+ Dir[File.join(__dir__, "check_methods", "*.rb")].sort.each { |f| require f }
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is part of konto_check_ruby, a Ruby port of the C library
4
+ # konto_check, Copyright (C) 2002-2023 Michael Plugge <konto_check@yahoo.com>.
5
+ # Ruby port Copyright (C) 2026 tickettoaster GmbH <https://tickettoaster.de>.
6
+ #
7
+ # konto_check_ruby is free software; you can redistribute it and/or modify it
8
+ # under the terms of the GNU Lesser General Public License as published by
9
+ # the Free Software Foundation; either version 2.1 of the License, or (at
10
+ # your option) any later version. It is distributed WITHOUT ANY WARRANTY; see
11
+ # the file LICENSE for details.
12
+
13
+ module KontoCheckRuby
14
+ # Case and accent insensitive collation used for the search functions and
15
+ # the sort index blocks (get_sortc / stri_cmp in the C source): letters are
16
+ # folded to lower case and accented letters are compared like their base
17
+ # letter (ä -> a, ß -> s, é -> e ...).
18
+ module Collation
19
+ FOLD = {
20
+ "ä" => "a", "à" => "a", "á" => "a", "â" => "a", "ã" => "a", "å" => "a",
21
+ "ö" => "o", "ò" => "o", "ó" => "o", "ô" => "o", "õ" => "o", "ø" => "o",
22
+ "ü" => "u", "ù" => "u", "ú" => "u", "û" => "u",
23
+ "è" => "e", "é" => "e", "ê" => "e", "ë" => "e",
24
+ "ì" => "i", "í" => "i", "î" => "i", "ï" => "i",
25
+ "ç" => "c", "ñ" => "n", "ý" => "y", "ÿ" => "y", "ß" => "s"
26
+ }.freeze
27
+
28
+ FOLD_RE = Regexp.union(FOLD.keys).freeze
29
+
30
+ # Sort key of a string: lower case, accents removed. Non-letters are kept.
31
+ def self.sort_key(str)
32
+ s = str.to_s
33
+ s = s.encode("UTF-8", invalid: :replace, undef: :replace) unless s.encoding == Encoding::UTF_8 && s.valid_encoding?
34
+ s.downcase.gsub(FOLD_RE, FOLD)
35
+ end
36
+
37
+ # Words (alphanumeric sequences) of a string, for the full text search
38
+ def self.words(str)
39
+ s = str.to_s
40
+ s = s.encode("UTF-8", invalid: :replace, undef: :replace) unless s.encoding == Encoding::UTF_8 && s.valid_encoding?
41
+ s.scan(/[[:alnum:]]+/)
42
+ end
43
+
44
+ # Prefix comparison like strni_cmp(): 0 if a is a prefix of b (after folding)
45
+ def self.prefix_cmp(a, b)
46
+ ka = sort_key(a)
47
+ kb = sort_key(b)
48
+ return 0 if kb.start_with?(ka)
49
+ ka <=> kb
50
+ end
51
+ end
52
+ end