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,1006 @@
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
+ require_relative "lut_file"
15
+ require_relative "blz_file"
16
+ require_relative "bank_data"
17
+ require_relative "check_methods"
18
+ require_relative "collation"
19
+ require_relative "iban_rules"
20
+ require_relative "search"
21
+ require_relative "update"
22
+
23
+ module KontoCheckRuby
24
+ # The Engine holds one loaded bank directory (BankData) and implements the
25
+ # functions of the C library konto_check on top of it: account number
26
+ # checks, IBAN generation/validation, lookups and searches.
27
+ #
28
+ # Method names and return conventions follow the C library closely; the
29
+ # modules KontoCheckRaw and KontoCheck (see konto_check_ruby.rb) provide the
30
+ # interface of the original Ruby gem on top of a default Engine instance.
31
+ #
32
+ # Functions that return a value plus a status return [value, status]
33
+ # (the C functions take an `int *retval` parameter for the status).
34
+ class Engine
35
+ include IbanRules
36
+ include Search
37
+
38
+ DEFAULT_INIT_LEVEL = 5
39
+ # The LUT file shipped with the gem; used whenever no file is given
40
+ # explicitly (the C library searches ./blz.lut2f, /etc/blz.lut2f ...
41
+ # instead).
42
+ BUNDLED_LUT = File.expand_path("../../data/blz.lut2f", __dir__)
43
+ EMPTY_BIC = BankData::EMPTY_BIC
44
+
45
+ # Date of the check digit methods / IBAN rules implemented (from konto_check 6.15)
46
+ PZ_METHODS_DATE = "09.12.2019"
47
+ IBAN_RULES_DATE = "09.09.2019"
48
+ C_LIBRARY_DATE = "13. April 2023"
49
+
50
+ attr_reader :data
51
+ # Fixed "current date" (Integer JJJJMMTT) for validity tests, nil = today
52
+ attr_accessor :current_date
53
+
54
+ def initialize
55
+ @data = nil
56
+ @encoding = 2 # UTF-8
57
+ @pz_aenderungen_2019_12 = true
58
+ @extra_init_done = 0
59
+ @current_date = nil
60
+ @level = -1
61
+ reset_search_cache
62
+ end
63
+
64
+ # ------------------------------------------------------------ initialisation
65
+
66
+ def initialized?
67
+ !@data.nil?
68
+ end
69
+
70
+ # Loads a LUT file (lut_init in C). Without a file name the LUT file
71
+ # bundled with the gem (BUNDLED_LUT) is loaded. level 0..9 selects the
72
+ # blocks to load (see BlzFile::LUT_SETS), set 0 = automatically choose
73
+ # the valid data set.
74
+ # If the same file (by file id) is already loaded, only missing blocks are
75
+ # loaded incrementally. Returns OK, LUT2_PARTIAL_OK, ... or an error code.
76
+ def init(lut_name = nil, level = DEFAULT_INIT_LEVEL, set = 0)
77
+ level = DEFAULT_INIT_LEVEL if level.nil?
78
+ set = 0 if set.nil?
79
+ lut_name = BUNDLED_LUT if lut_name.nil? || lut_name.to_s.empty?
80
+ return NO_LUT_FILE unless File.file?(lut_name)
81
+ required = required_blocks(level)
82
+ if @data && @data.source == lut_name && !@data.lut_id.empty?
83
+ id = file_id_of(lut_name, set)
84
+ if id == @data.lut_id && (set == 0 || set == @data.set)
85
+ return OK if level <= @level
86
+ code = @data.load_blocks(required)
87
+ @level = level if code > 0 || code == LUT2_PARTIAL_OK
88
+ @extra_init_done = 0
89
+ return code
90
+ end
91
+ end
92
+ # (unlike the C library the previously loaded data is kept if the new
93
+ # file cannot be loaded)
94
+ code, data = BankData.from_lut(lut_name, required: required, set: set, current_date: today)
95
+ if code > 0 || [LUT2_PARTIAL_OK, LUT2_NO_LONGER_VALID_PARTIAL_OK, LUT2_NOT_YET_VALID_PARTIAL_OK].include?(code)
96
+ free
97
+ @data = data
98
+ @data.level = level
99
+ @level = level
100
+ end
101
+ code
102
+ end
103
+
104
+ # Loads the bank directory directly from a Bundesbank file (TXT fixed
105
+ # width, CSV or XML; no LUT file needed).
106
+ # gueltigkeit validity "JJJJMMTT-JJJJMMTT" (optional; XML files carry
107
+ # their validity themselves)
108
+ # iban_rules :default (built-in table of konto_check 6.15) applies
109
+ # IBAN rules to files without the IBAN rule column, false
110
+ # disables that, a Hash {blz => rule*100+version} or a
111
+ # file name supplies own rules
112
+ def load_blz_file(filename, gueltigkeit: nil, iban_rules: :default)
113
+ code, records, file_format, v1, v2 = BlzFile.parse_any(filename)
114
+ return code unless code == OK
115
+ if gueltigkeit && !gueltigkeit.empty?
116
+ return LUT2_INVALID_GUELTIGKEIT unless gueltigkeit =~ /\A(\d{8})[ -](\d{8})\z/
117
+ v1 = Regexp.last_match(1).to_i
118
+ v2 = Regexp.last_match(2).to_i
119
+ return LUT2_GUELTIGKEIT_SWAPPED if v2 < v1
120
+ end
121
+ if file_format != 2 && (table = BlzFile.iban_rule_table(iban_rules))
122
+ BlzFile.apply_iban_rules!(records, table)
123
+ end
124
+ load_records(records, source: filename, valid_from: v1, valid_to: v2)
125
+ end
126
+
127
+ # Loads the current Bundesbank file from the local cache (see Update),
128
+ # downloading it first if no cached file is valid for today (or for
129
+ # current_date). If the download fails, the newest cached file is used,
130
+ # and if there is none, the bundled LUT file. Returns the load status
131
+ # (OK ...) or a negative code; the reason of a failed download is
132
+ # available in #last_update_error.
133
+ # dir: cache directory (default Update.cache_dir)
134
+ # format: :xml (default), :txt or :csv
135
+ # refresh: :auto (download only if nothing valid is cached, default),
136
+ # :always (check the download page every time), :never
137
+ def load_current(dir: Update.cache_dir, format: :xml, refresh: :auto, iban_rules: :default)
138
+ @last_update_error = nil
139
+ file = refresh == :always ? nil : Update.cached_file(dir: dir, current_date: today)
140
+ if file.nil? && refresh != :never
141
+ begin
142
+ file = Update.update(dir: dir, format: format).path
143
+ rescue Update::Error => e
144
+ @last_update_error = e.message
145
+ file = nil
146
+ end
147
+ end
148
+ file ||= Update.cached_file(dir: dir)
149
+ return init(BUNDLED_LUT, 9) if file.nil?
150
+ load_blz_file(file, iban_rules: iban_rules)
151
+ end
152
+
153
+ # Message of the last failed download in load_current, or nil
154
+ attr_reader :last_update_error
155
+
156
+ # Loads the bank directory from an Array of BlzFile::BankRecord
157
+ def load_records(records, source: nil, valid_from: 0, valid_to: 0)
158
+ free
159
+ @data = BankData.from_records(records, source: source, valid_from: valid_from, valid_to: valid_to)
160
+ @data.level = 9
161
+ @level = 9
162
+ OK
163
+ end
164
+
165
+ # Releases the loaded data (lut_cleanup in C)
166
+ def free
167
+ @data = nil
168
+ @level = -1
169
+ @extra_init_done = 0
170
+ reset_search_cache
171
+ OK
172
+ end
173
+
174
+ # [filename, set, level, retval]
175
+ def current_lutfile_name
176
+ return [nil, 0, -1, LUT2_NOT_INITIALIZED] unless @data
177
+ [@data.source, @data.set, @level, OK]
178
+ end
179
+
180
+ # Validity of the loaded data set for the current date
181
+ def lut_valid
182
+ return LUT2_NOT_INITIALIZED unless @data
183
+ @data.valid(today)
184
+ end
185
+
186
+ # Info blocks: [code, info1, info2, valid1, valid2]. Without a file name
187
+ # the info of the loaded data set is returned.
188
+ def lut_info(lut_name = nil)
189
+ if lut_name.nil? || lut_name.empty?
190
+ return [LUT2_NOT_INITIALIZED, nil, nil, LUT2_NOT_INITIALIZED, LUT2_NOT_INITIALIZED] unless @data
191
+ return [OK, @data.info_text, nil, @data.valid(today), LUT2_BLOCK_NOT_IN_FILE]
192
+ end
193
+ LutFile.info(lut_name, today)
194
+ end
195
+
196
+ # Status of the loaded blocks: [code, filename, blocks_ok, blocks_failed]
197
+ def lut_blocks(mode = 1)
198
+ return [LUT2_NOT_INITIALIZED, nil, nil, nil] unless @data
199
+ ok = []
200
+ fail = []
201
+ offset = @data.set == 2 ? LutFile::SET_OFFSET : 0
202
+ (1...LutFile::SET_OFFSET).each do |t|
203
+ st = @data.block_status[t + offset] || @data.block_status[t]
204
+ next if st.nil?
205
+ name = LutFile.block_name(t + offset, mode)
206
+ if st == OK
207
+ ok << name
208
+ else
209
+ fail << name
210
+ end
211
+ end
212
+ [fail.empty? ? OK : LUT2_BLOCKS_MISSING, @data.source, ok.join(", "), fail.join(", ")]
213
+ end
214
+
215
+ def dump_lutfile(lut_name)
216
+ LutFile.dump(lut_name)
217
+ end
218
+
219
+ # generate_lut2_p in C
220
+ def generate_lutfile(input, output, user_info = "", gueltigkeit = nil, felder = 9, filialen = false, set = 0)
221
+ BlzFile.generate_lut(input, output, user_info: user_info.to_s, gueltigkeit: gueltigkeit,
222
+ felder: felder.to_i, filialen: filialen.to_i != 0, set: set.to_i)
223
+ end
224
+
225
+ # Rebuilds a Bundesbank text file from the loaded data (rebuild_blzfile in C).
226
+ # set 0: input is a Bundesbank file, 1/2: input is a LUT file (set 1/2).
227
+ def rebuild_blzfile(input, output, set = 0)
228
+ set = set.to_i
229
+ if set == 0
230
+ code = load_blz_file(input)
231
+ else
232
+ code = init(input, 9, set > 2 ? 2 : set)
233
+ end
234
+ return code if code <= 0 && code != LUT2_PARTIAL_OK
235
+ d = @data
236
+ File.open(output, "wb") do |out|
237
+ d.cnt_hs.times do |i|
238
+ s = d.startidx[i]
239
+ d.filialen_at(i).times do |k|
240
+ j = s + k
241
+ regel = d.iban_regel ? d.iban_regel[j] : nil
242
+ pan = (d.pan && d.pan[j] != 0) ? format("%05d", d.pan[j]) : ""
243
+ nr = (d.nr && d.nr[j] != 0) ? format("%06d", d.nr[j]) : ""
244
+ line = format("%8d%1s%-58s%05d%-35s%-27s%5s%-11s%2s%6s%1s%1s%08d",
245
+ d.blz[i], k == 0 ? "1" : "2", d.name ? d.name[j] : "", d.plz ? d.plz[j] : 0,
246
+ d.ort ? d.ort[j] : "", d.name_kurz ? d.name_kurz[j] : "", pan,
247
+ d.bic ? d.bic[j] : "", BlzFile.pz_to_string(d.pz[i]), nr,
248
+ d.aenderung ? d.aenderung[j] : " ", d.loeschung ? d.loeschung[j] : " ",
249
+ d.nachfolge_blz ? d.nachfolge_blz[j] : 0)
250
+ line += format("%06d", regel) if regel
251
+ out.write(line.encode("ISO-8859-1", undef: :replace) + "\n")
252
+ end
253
+ end
254
+ end
255
+ OK
256
+ rescue SystemCallError
257
+ FILE_WRITE_ERROR
258
+ end
259
+
260
+ # ------------------------------------------------------------- BLZ lookup
261
+
262
+ # Index of a BLZ (String) in the main office arrays or a negative error
263
+ # code (lut_index in C). Leading blanks/tabs are skipped, exactly 8 digits
264
+ # followed by end of string, blank or tab are required.
265
+ def lut_index(blz)
266
+ return LUT2_NOT_INITIALIZED unless @data
267
+ s = blz.to_s.sub(/\A[ \t]+/, "")
268
+ m = s.match(/\A(\d{8})(?:[ \t]|\z)/)
269
+ return INVALID_BLZ_LENGTH unless m
270
+ @data.index(m[1].to_i) || INVALID_BLZ
271
+ end
272
+
273
+ def lut_index_i(blz_int)
274
+ return LUT2_NOT_INITIALIZED unless @data
275
+ return INVALID_BLZ_LENGTH if blz_int < 10_000_000 || blz_int > 99_999_999
276
+ @data.index(blz_int) || INVALID_BLZ
277
+ end
278
+
279
+ # Tests whether a BLZ (and branch) exists (lut_blz in C)
280
+ def lut_blz(blz, zweigstelle = 0)
281
+ return LUT2_BLZ_NOT_INITIALIZED unless @data
282
+ idx = lut_index(blz)
283
+ return idx if idx < 0
284
+ return LUT2_INDEX_OUT_OF_RANGE unless branch_ok?(idx, zweigstelle)
285
+ OK
286
+ end
287
+
288
+ # [count, retval]
289
+ def lut_filialen(blz)
290
+ return [0, LUT2_BLZ_NOT_INITIALIZED] unless @data
291
+ return [0, LUT2_FILIALEN_NOT_INITIALIZED] unless @data.filialen
292
+ idx = lut_index(blz)
293
+ return [0, idx] if idx < 0
294
+ [@data.filialen[idx], OK]
295
+ end
296
+
297
+ def lut_name(blz, zweigstelle = 0)
298
+ field_s(:name, LUT2_NAME_NOT_INITIALIZED, blz, zweigstelle)
299
+ end
300
+
301
+ def lut_name_kurz(blz, zweigstelle = 0)
302
+ field_s(:name_kurz, LUT2_NAME_KURZ_NOT_INITIALIZED, blz, zweigstelle)
303
+ end
304
+
305
+ def lut_ort(blz, zweigstelle = 0)
306
+ field_s(:ort, LUT2_ORT_NOT_INITIALIZED, blz, zweigstelle)
307
+ end
308
+
309
+ def lut_plz(blz, zweigstelle = 0)
310
+ field_i(:plz, LUT2_PLZ_NOT_INITIALIZED, blz, zweigstelle)
311
+ end
312
+
313
+ def lut_pan(blz, zweigstelle = 0)
314
+ field_i(:pan, LUT2_PAN_NOT_INITIALIZED, blz, zweigstelle)
315
+ end
316
+
317
+ def lut_nr(blz, zweigstelle = 0)
318
+ field_i(:nr, LUT2_NR_NOT_INITIALIZED, blz, zweigstelle)
319
+ end
320
+
321
+ def lut_nachfolge_blz(blz, zweigstelle = 0)
322
+ field_i(:nachfolge_blz, LUT2_NACHFOLGE_BLZ_NOT_INITIALIZED, blz, zweigstelle)
323
+ end
324
+
325
+ # [character "A"/"D"/"M"/"U", retval]
326
+ def lut_aenderung(blz, zweigstelle = 0)
327
+ v, r = field_s(:aenderung, LUT2_AENDERUNG_NOT_INITIALIZED, blz, zweigstelle)
328
+ [v.nil? || v.empty? ? nil : v, r]
329
+ end
330
+
331
+ # [0/1, retval]
332
+ def lut_loeschung(blz, zweigstelle = 0)
333
+ v, r = field_s(:loeschung, LUT2_LOESCHUNG_NOT_INITIALIZED, blz, zweigstelle)
334
+ [v.nil? || v.empty? ? nil : v.to_i, r]
335
+ end
336
+
337
+ # [check method (Integer), retval]
338
+ def lut_pz(blz, zweigstelle = 0)
339
+ return [0, LUT2_BLZ_NOT_INITIALIZED] unless @data
340
+ return [0, LUT2_PZ_NOT_INITIALIZED] unless @data.pz
341
+ idx = lut_index(blz)
342
+ return [0, idx] if idx < 0
343
+ return [0, LUT2_INDEX_OUT_OF_RANGE] unless branch_ok?(idx, zweigstelle)
344
+ [@data.pz[idx], OK]
345
+ end
346
+
347
+ # [rule*100+version, retval]. Leading "@" or "+" in the BLZ are ignored.
348
+ def lut_iban_regel(blz, zweigstelle = 0)
349
+ ret = iban_init
350
+ return [0, ret] if ret < OK
351
+ return [0, LUT2_IBAN_REGEL_NOT_INITIALIZED] unless @data.iban_regel
352
+ b = blz.to_s.sub(/\A[@+]+/, "")
353
+ idx = lut_index(b)
354
+ return [0, idx] if idx < 0
355
+ return [0, LUT2_INDEX_OUT_OF_RANGE] unless branch_ok?(idx, zweigstelle)
356
+ [@data.iban_regel[@data.startidx[idx] + zweigstelle], OK]
357
+ end
358
+
359
+ def lut_iban_regel_i(blz_int, zweigstelle = 0)
360
+ return [0, LUT2_NOT_INITIALIZED] unless @data
361
+ return [0, LUT2_IBAN_REGEL_NOT_INITIALIZED] unless @data.iban_regel
362
+ idx = lut_index_i(blz_int)
363
+ return [0, idx] if idx < 0
364
+ return [0, LUT2_INDEX_OUT_OF_RANGE] unless branch_ok?(idx, zweigstelle)
365
+ [@data.iban_regel[@data.startidx[idx] + zweigstelle], OK]
366
+ end
367
+
368
+ # BIC of a bank: [bic, retval]. If the BLZ has a successor BLZ, the BIC of
369
+ # the successor is returned (retval OK_NACHFOLGE_BLZ_USED) unless the BLZ
370
+ # starts with "!". retval is OK_INVALID_FOR_IBAN if an IBAN rule replaces
371
+ # the BIC, OK_HYPO_REQUIRES_KTO for the rules 31..35.
372
+ def lut_bic(blz, zweigstelle = 0)
373
+ ret = iban_init
374
+ return [nil, ret] if ret < OK
375
+ bic, retval = lut_bic_int(blz, zweigstelle)
376
+ regel, ret = lut_iban_regel(blz, 0)
377
+ # like the C library the rule check is done even if the lookup failed
378
+ # (the C function returns "" instead of NULL then)
379
+ if ret == OK
380
+ # The C library compares the raw value (rule * 100 + version) with
381
+ # 31..35, so OK_HYPO_REQUIRES_KTO is practically never returned; this
382
+ # is reproduced for compatibility.
383
+ if regel >= 31 && regel <= 35
384
+ retval = OK_HYPO_REQUIRES_KTO
385
+ else
386
+ b2 = blz.to_s.sub(/\A[!@+]+/, "").dup
387
+ k2 = +"0000000000"
388
+ _r, bic_neu = iban_regel_cvt(b2, k2, regel, nil)
389
+ retval = OK_INVALID_FOR_IBAN if bic_neu && (bic || "").casecmp(bic_neu) != 0
390
+ end
391
+ end
392
+ [bic, retval]
393
+ end
394
+
395
+ # BIC from the LUT data without IBAN rule check (lut_bic_int in C)
396
+ def lut_bic_int(blz, zweigstelle = 0)
397
+ return [nil, LUT2_BLZ_NOT_INITIALIZED] unless @data
398
+ return [nil, LUT2_BIC_NOT_INITIALIZED] unless @data.bic
399
+ b = blz.to_s
400
+ force_old = false
401
+ if b.start_with?("!")
402
+ b = b[1..]
403
+ force_old = true
404
+ end
405
+ idx = lut_index(b)
406
+ return [nil, idx] if idx < 0
407
+ return [nil, LUT2_INDEX_OUT_OF_RANGE] unless branch_ok?(idx, zweigstelle)
408
+ return [nil, LUT2_NACHFOLGE_BLZ_NOT_INITIALIZED] if @data.nachfolge_blz.nil? && !force_old
409
+ retval = OK
410
+ if !force_old && @data.nachfolge_blz && (nb = @data.nachfolge_blz[@data.startidx[idx]]) != 0
411
+ idx2 = lut_index_i(nb)
412
+ return [nil, idx2] if idx2 < 0
413
+ idx = idx2
414
+ retval = OK_NACHFOLGE_BLZ_USED
415
+ end
416
+ [@data.bic[@data.startidx[idx] + zweigstelle], retval]
417
+ end
418
+
419
+ # BIC of the main office (lut_bic_h in C)
420
+ def lut_bic_h(blz, zweigstelle = 0)
421
+ return [nil, LUT2_BLZ_NOT_INITIALIZED] unless @data
422
+ return [nil, LUT2_BIC_NOT_INITIALIZED] unless @data.bic_h
423
+ b = blz.to_s
424
+ force_old = false
425
+ if b.start_with?("!")
426
+ b = b[1..]
427
+ force_old = true
428
+ end
429
+ idx = lut_index(b)
430
+ return [nil, idx] if idx < 0
431
+ return [nil, LUT2_INDEX_OUT_OF_RANGE] unless branch_ok?(idx, zweigstelle)
432
+ return [nil, LUT2_NACHFOLGE_BLZ_NOT_INITIALIZED] if @data.nachfolge_blz.nil? && !force_old
433
+ retval = OK
434
+ if !force_old && @data.nachfolge_blz && (nb = @data.nachfolge_blz[@data.startidx[idx]]) != 0
435
+ idx2 = lut_index_i(nb)
436
+ return [nil, idx2] if idx2 < 0
437
+ idx = idx2
438
+ retval = OK_NACHFOLGE_BLZ_USED
439
+ end
440
+ [@data.bic_h[@data.startidx[idx] + zweigstelle], retval]
441
+ end
442
+
443
+ # Integer variants used by the IBAN rules
444
+ def lut_aenderung_i(blz_int)
445
+ return nil unless @data && @data.aenderung
446
+ idx = lut_index_i(blz_int)
447
+ return nil if idx < 0
448
+ @data.aenderung[@data.startidx[idx]]
449
+ end
450
+
451
+ def lut_nachfolge_blz_i(blz_int)
452
+ return 0 unless @data && @data.nachfolge_blz
453
+ idx = lut_index_i(blz_int)
454
+ return 0 if idx < 0
455
+ @data.nachfolge_blz[@data.startidx[idx]]
456
+ end
457
+
458
+ # All fields of a bank (lut_multiple in C). Returns a Hash with
459
+ # :retval, :cnt (branches), :idx and per field an Array over the branches
460
+ # (:name, :name_kurz, :plz, :ort, :pan, :bic, :nr, :aenderung, :loeschung,
461
+ # :nachfolge_blz, :iban_regel) plus :pz (one value). Fields of blocks that
462
+ # are not loaded are nil (then :retval is LUT2_PARTIAL_OK).
463
+ def lut_multiple(blz)
464
+ return { retval: LUT2_NOT_INITIALIZED } unless @data
465
+ idx = lut_index(blz)
466
+ return { retval: idx } if idx < 0
467
+ cnt = @data.filialen_at(idx)
468
+ s = @data.startidx[idx]
469
+ res = { retval: OK, cnt: cnt, idx: idx, blz: @data.blz[idx], pz: @data.pz ? @data.pz[idx] : nil }
470
+ res[:retval] = LUT2_PARTIAL_OK if @data.pz.nil?
471
+ %i[name name_kurz plz ort pan bic nr aenderung loeschung nachfolge_blz iban_regel].each do |f|
472
+ arr = @data.public_send(f)
473
+ if arr
474
+ res[f] = arr[s, cnt]
475
+ else
476
+ res[f] = nil
477
+ res[:retval] = LUT2_PARTIAL_OK
478
+ end
479
+ end
480
+ res
481
+ end
482
+
483
+ # -------------------------------------------------------- account checks
484
+
485
+ # Checks an account number with the check method of the given BLZ
486
+ # (kto_check_blz in C).
487
+ def kto_check_blz(blz, kto)
488
+ return MISSING_PARAMETER if blz.nil? || kto.nil?
489
+ return LUT2_NOT_INITIALIZED unless @data && @data.pz
490
+ idx = lut_index(blz)
491
+ return idx if idx < 0
492
+ return BLZ_MARKED_AS_DELETED if @data.aenderung && @data.aenderung[@data.startidx[idx]] == "D"
493
+ check_int(blz.to_s.strip, @data.pz[idx], kto, 0, nil)
494
+ end
495
+
496
+ # Same with debug information: [retval, Retvals]
497
+ def kto_check_blz_dbg(blz, kto)
498
+ rv = CheckMethods::Retvals.new("(-)", -1, -1, -1)
499
+ return [MISSING_PARAMETER, rv] if blz.nil? || kto.nil?
500
+ return [LUT2_NOT_INITIALIZED, rv] unless @data && @data.pz
501
+ idx = lut_index(blz)
502
+ return [idx, rv] if idx < 0
503
+ [check_int(blz.to_s.strip, @data.pz[idx], kto, 0, rv), rv]
504
+ end
505
+
506
+ # Checks an account number with an explicitly given check method
507
+ # ("00".."E4", optionally with sub-method letter, e.g. "51c"). The BLZ is
508
+ # only needed for the methods 52, 53, B6 and C0 (kto_check_pz in C).
509
+ def kto_check_pz(pz, kto, blz = nil, rv = nil)
510
+ return MISSING_PARAMETER if pz.nil? || kto.nil?
511
+ s = pz.to_s
512
+ return UNDEFINED_SUBMETHOD if s.length > 3
513
+ parsed = CheckMethods.parse_method(s)
514
+ unless parsed
515
+ return UNDEFINED_SUBMETHOD if s.length == 3
516
+ return NOT_IMPLEMENTED
517
+ end
518
+ methode, um = parsed
519
+ b = blz.to_s
520
+ b = nil if b.empty? || b.start_with?("0")
521
+ check_int(b, methode, kto, um, rv)
522
+ end
523
+
524
+ # [retval, Retvals]
525
+ def kto_check_pz_dbg(pz, kto, blz = nil)
526
+ rv = CheckMethods::Retvals.new("(-)", -1, -1, -1)
527
+ [kto_check_pz(pz, kto, blz, rv), rv]
528
+ end
529
+
530
+ # Universal check (kto_check in C): pz_or_blz with 2 or 3 characters is a
531
+ # check method, otherwise a BLZ.
532
+ def kto_check(pz_or_blz, kto, lut_name = nil)
533
+ return MISSING_PARAMETER if pz_or_blz.nil? || kto.nil?
534
+ s = pz_or_blz.to_s
535
+ if s.length == 2 || s.length == 3
536
+ parsed = CheckMethods.parse_method(s)
537
+ return NOT_IMPLEMENTED unless parsed
538
+ return check_int(nil, parsed[0], kto, parsed[1], nil)
539
+ end
540
+ unless @data
541
+ code = init(lut_name, 1, 0)
542
+ return code if code <= 0 && code != LUT2_PARTIAL_OK && code != LUT1_SET_LOADED
543
+ return LUT2_NOT_INITIALIZED unless @data
544
+ end
545
+ kto_check_blz(s, kto)
546
+ end
547
+
548
+ # Check with IBAN rules applied first (kto_check_regel in C)
549
+ def kto_check_regel(blz, kto)
550
+ kto_check_regel_dbg(blz, kto)[0]
551
+ end
552
+
553
+ # [retval, blz2, kto2, bic, regel*100+version, Retvals]
554
+ def kto_check_regel_dbg(blz, kto)
555
+ rv = CheckMethods::Retvals.new("-", -1, -1, -1)
556
+ return [MISSING_PARAMETER, nil, nil, nil, 0, rv] if blz.nil? || kto.nil?
557
+ k = kto.to_s
558
+ return [INVALID_KTO_LENGTH, nil, nil, nil, 0, rv] if k.length > 10
559
+ blz_n = blz.to_s[0, 8].dup
560
+ kto_n = k.rjust(10, "0")
561
+ blz_o = blz_n.dup
562
+ kto_o = kto_n.dup
563
+ regel, ret = lut_iban_regel(blz_n, 0)
564
+ regel_out = ret > 0 ? regel : 0
565
+ ret_regel, bic = iban_regel_cvt(blz_n, kto_n, regel, rv)
566
+ bic ||= lut_bic(blz_n, 0)[0]
567
+ return [ret_regel, blz_n, kto_n, bic, regel_out, rv] if ret_regel < OK
568
+ ret, = kto_check_blz_dbg(blz_n, kto_n).then { |r, rv2| rv.methode = rv2.methode; rv.pz_methode = rv2.pz_methode; rv.pz = rv2.pz; rv.pz_pos = rv2.pz_pos; [r] }
569
+ if blz_n != blz_o || kto_n != kto_o
570
+ result = ret_regel > 3 ? ret_regel : ret
571
+ else
572
+ result = ret
573
+ end
574
+ [result, blz_n, kto_n, bic, regel_out, rv]
575
+ end
576
+
577
+ # ----------------------------------------------------------- IBAN, BIC
578
+
579
+ # Generates the IBAN for a German bank account (iban_bic_gen in C).
580
+ # Returns [retval, iban ("DE89 3704 0044 0532 0130 00" with blanks) or nil,
581
+ # bic, blz2, kto2]. blz2/kto2 are the BLZ and account actually used
582
+ # (they may have been replaced by IBAN rules). Prefixes for the BLZ:
583
+ # "+" skip the account check, "@" ignore the blacklist, "!" do not
584
+ # replace the BLZ by its successor.
585
+ def iban_bic_gen(blz, kto)
586
+ return [LUT2_NO_ACCOUNT_GIVEN, nil, nil, "", ""] if blz.nil? || kto.nil? || blz.to_s.empty? || kto.to_s.empty?
587
+ ret = iban_init
588
+ return [ret, nil, nil, nil, nil] if ret < OK
589
+ b = blz.to_s
590
+ flags = 0
591
+ b.each_char do |c|
592
+ break if c =~ /\d/
593
+ flags |= 1 if c == "+"
594
+ flags |= 2 if c == "@"
595
+ flags |= 4 if c == "!"
596
+ end
597
+ b = b.sub(/\A[@+!]+/, "")
598
+ blz2 = b.dup
599
+ kto2 = kto.to_s.dup
600
+ k = kto.to_s
601
+ return [INVALID_KTO_LENGTH, nil, nil, blz2, kto2] if k.length > 10
602
+ blz_i = b =~ /\A\d{8}\z/ ? b.to_i : 100_000_000
603
+ blz_n = b.dup
604
+ kto_n = k.rjust(10, "0")
605
+ regel, ret = lut_iban_regel_i(blz_i, 0)
606
+ if ret <= 0 && ret != LUT2_IBAN_REGEL_NOT_INITIALIZED
607
+ return [ret, nil, nil, blz2, kto2]
608
+ end
609
+ bic = nil
610
+ if ret != LUT2_IBAN_REGEL_NOT_INITIALIZED
611
+ if regel == 0 && (flags & 2) == 0 && @data.own_iban && @data.own_iban.first == 2718281 && @data.own_iban.include?(blz_i)
612
+ return [BLZ_BLACKLISTED, nil, nil, blz2, kto2]
613
+ end
614
+ ret_regel, bic = iban_regel_cvt(blz_n, kto_n, regel, nil)
615
+ if ret_regel < OK
616
+ bic ||= lut_bic(blz_n, 0)[0]
617
+ bic = "" if bic.nil? || bic.start_with?(" ")
618
+ return [ret_regel, nil, bic, blz_n, kto_n]
619
+ end
620
+ else
621
+ regel = 0
622
+ ret_regel = OK
623
+ end
624
+ flags |= 1 if ret_regel == OK_IBAN_WITHOUT_KC_TEST || ret_regel == OK_KTO_REPLACED_NO_PZ
625
+ nb, ret = lut_nachfolge_blz(blz_n, 0)
626
+ if regel == 0 && (flags & 4) == 0 && ret == OK && nb > 0
627
+ ret_old = kto_check_blz(blz_n, kto_n)
628
+ blz_n = format("%8d", nb)
629
+ ret_neu = kto_check_blz(blz_n, kto_n)
630
+ return [OLD_BLZ_OK_NEW_NOT, nil, nil, blz2, kto2] if ret_old == OK && ret_neu < OK
631
+ end
632
+ bic ||= lut_bic(blz_n, 0)[0]
633
+ bic = "" if bic.nil? || bic.start_with?(" ")
634
+ blz2 = blz_n
635
+ kto2 = kto_n
636
+ if (flags & 1) == 0
637
+ ret = kto_check_blz(blz_n, kto_n)
638
+ return [ret, nil, bic, blz2, kto2] if ret <= 0
639
+ elsif ret_regel != OK_IBAN_WITHOUT_KC_TEST && ret_regel != OK_KTO_REPLACED_NO_PZ
640
+ ret_regel = LUT2_KTO_NOT_CHECKED
641
+ end
642
+ bban = format("%8s%10s", blz_n, kto_n).tr(" ", "0")
643
+ iban = "DE" + iban_checksum("DE", bban) + bban
644
+ [ret_regel, iban.scan(/.{1,4}/).join(" "), bic, blz2, kto2]
645
+ end
646
+
647
+ # IBAN without blanks or nil (iban_gen in C): [iban, retval]
648
+ def iban_gen(blz, kto)
649
+ ret, iban, = iban_bic_gen(blz, kto)
650
+ [iban&.delete(" "), ret]
651
+ end
652
+
653
+ # Checks an IBAN (checksum, length by country and for German IBANs also
654
+ # the account number and the IBAN rules). Returns [retval, retval_kc]
655
+ # where retval_kc is the result of the account check.
656
+ def iban_check(iban)
657
+ return [LUT2_NO_ACCOUNT_GIVEN, LUT2_NO_ACCOUNT_GIVEN] if iban.nil? || iban.to_s.empty?
658
+ s = iban.to_s.gsub(/[^A-Za-z0-9]/, "")
659
+ country = s[0, 2].to_s.upcase
660
+ retval = LUT2_KTO_NOT_CHECKED
661
+ expected = IBAN_LENGTHS[country]
662
+ return [INVALID_IBAN_LENGTH, retval] if expected && s.length != expected
663
+ test = iban_checksum_ok?(s) ? 1 : 0
664
+ ret_kc = 0
665
+ if country == "DE"
666
+ digits = s[4..].to_s.gsub(/\D/, "")
667
+ blz2 = digits[0, 8].to_s
668
+ kto2 = digits[8, 10].to_s
669
+ ret = ret_kc = kto_check_blz(blz2, kto2)
670
+ test |= 2 if ret > 0
671
+ retval = ret
672
+ if test & 1 == 1
673
+ j = lut_index(blz2)
674
+ if j >= 0
675
+ ret = iban_init
676
+ return [ret, retval] if ret < OK
677
+ uk = CheckMethods::UK_PZ_METHODEN.include?(@data.pz[j])
678
+ nachfolge = @data.nachfolge_blz ? @data.nachfolge_blz[@data.startidx[j]] : 0
679
+ regel = @data.iban_regel ? @data.iban_regel[@data.startidx[j]] : 0
680
+ if uk || nachfolge != 0 || regel != 0
681
+ ret, papier2, = iban_bic_gen(blz2, kto2)
682
+ return [IBAN_CHKSUM_OK_NO_IBAN_CALCULATION, retval] if ret == NO_IBAN_CALCULATION
683
+ test = 4 if ret == OK_IBAN_WITHOUT_KC_TEST || ret == OK_KTO_REPLACED_NO_PZ
684
+ if papier2
685
+ iban2 = papier2.delete(" ")
686
+ if s.casecmp(iban2) != 0
687
+ if regel > 0
688
+ return [IBAN_CHKSUM_OK_RULE_IGNORED_BLZ, retval] if s[12..] == iban2[12..]
689
+ return [IBAN_CHKSUM_OK_RULE_IGNORED, retval]
690
+ elsif nachfolge != 0
691
+ return [IBAN_CHKSUM_OK_NACHFOLGE_BLZ_DEFINED, retval]
692
+ else
693
+ return [IBAN_CHKSUM_OK_UNTERKTO_MISSING, retval]
694
+ end
695
+ end
696
+ end
697
+ end
698
+ end
699
+ end
700
+ else
701
+ test |= 2 if test != 0
702
+ retval = NO_GERMAN_BIC
703
+ end
704
+ case test
705
+ when 1
706
+ if ret_kc == INVALID_BLZ
707
+ [IBAN_CHKSUM_OK_BLZ_INVALID, retval]
708
+ elsif ret_kc == LUT2_NOT_INITIALIZED
709
+ [IBAN_CHKSUM_OK_KC_NOT_INITIALIZED, retval]
710
+ else
711
+ [IBAN_OK_KTO_NOT, retval]
712
+ end
713
+ when 2 then [KTO_OK_IBAN_NOT, retval]
714
+ when 3 then [OK, retval]
715
+ when 4 then [OK_IBAN_WITHOUT_KC_TEST, OK_NO_CHK]
716
+ else [FALSE, retval]
717
+ end
718
+ end
719
+
720
+ # BIC for a German IBAN: [bic, retval, blz, kto]
721
+ def iban2bic(iban)
722
+ s = iban.to_s.gsub(/[^A-Za-z0-9]/, "")
723
+ return ["", IBAN2BIC_ONLY_GERMAN, "", ""] unless s[0, 2].to_s.casecmp("DE").zero?
724
+ return ["", INVALID_IBAN_LENGTH, nil, nil] if s.length != 22
725
+ digits = s[4..].gsub(/\D/, "")
726
+ blz2 = digits[0, 8].to_s
727
+ kto2 = digits[8, 10].to_s
728
+ retval = OK
729
+ j = lut_index(blz2)
730
+ return ["", j, blz2, kto2] if j < 0
731
+ if j > 0
732
+ uk = CheckMethods::UK_PZ_METHODEN.include?(@data.pz[j])
733
+ regel = @data.iban_regel ? @data.iban_regel[@data.startidx[j]] : 0
734
+ if uk || regel != 0
735
+ ret, papier, bic, = iban_bic_gen(blz2, kto2)
736
+ retval = ret
737
+ return [bic, OK, blz2, kto2] if ret == NO_IBAN_CALCULATION
738
+ if papier
739
+ iban2 = papier.delete(" ")
740
+ if s.casecmp(iban2) != 0
741
+ retval = regel > 0 ? IBAN_CHKSUM_OK_RULE_IGNORED : IBAN_CHKSUM_OK_UNTERKTO_MISSING
742
+ end
743
+ end
744
+ return [bic, retval, blz2, kto2]
745
+ end
746
+ end
747
+ bic, retval = lut_bic(blz2, 0)
748
+ bic = "" if bic.nil? || bic.start_with?(" ")
749
+ [bic, retval, blz2, kto2]
750
+ end
751
+
752
+ # Tests whether a German BIC exists: [retval, count]
753
+ def bic_check(bic)
754
+ s = bic.to_s
755
+ return [BIC_ONLY_GERMAN, 0] unless s[4, 2].to_s.casecmp("DE").zero?
756
+ return [INVALID_BIC_LENGTH, 0] unless s.length == 8 || s.length == 11
757
+ code, hits = lut_suche_bic(s)
758
+ return [FALSE, 0] if code == KEY_NOT_FOUND
759
+ return [code, 0] if code < 0
760
+ [OK, hits.size]
761
+ end
762
+
763
+ # Checks a SEPA creditor identifier (Gläubiger-Identifikationsnummer)
764
+ def ci_check(ci)
765
+ return MISSING_PARAMETER if ci.nil?
766
+ s = ci.to_s.gsub(/[^A-Za-z0-9]/, "")
767
+ return FALSE if s.length < 7
768
+ numeric = to_numeric(s[7..].to_s) + to_numeric(s[0, 2]) + s[2, 2].to_s
769
+ numeric.to_i % 97 == 1 ? OK : FALSE
770
+ end
771
+
772
+ # Generates a structured remittance information (IPI): [retval, ipi, ipi_papier]
773
+ def ipi_gen(zweck)
774
+ z = zweck.to_s
775
+ return [IPI_INVALID_LENGTH, nil, nil] if z.length > 18
776
+ return [IPI_INVALID_CHARACTER, nil, nil] unless z =~ /\A[0-9A-Za-z]*\z/
777
+ body = z.upcase.rjust(18, "0")
778
+ rest = (to_numeric(body) + "00").to_i % 97
779
+ pz = 98 - rest
780
+ dst = format("%02d%s", pz, body)
781
+ [OK, dst, dst.scan(/.{1,4}/).join(" ")]
782
+ end
783
+
784
+ def ipi_check(zweck)
785
+ s = zweck.to_s.delete(" \t")
786
+ return IPI_CHECK_INVALID_LENGTH if s.length != 20
787
+ numeric = to_numeric(s[2..]) + s[0, 2]
788
+ numeric.to_i % 97 == 1 ? OK : FALSE
789
+ end
790
+
791
+ # IBAN length per country (ISO 3166 code)
792
+ IBAN_LENGTHS = {
793
+ "AL" => 28, "AD" => 24, "AZ" => 28, "BH" => 22, "BR" => 29, "BE" => 16, "BA" => 20, "BG" => 22,
794
+ "CR" => 21, "DK" => 18, "DE" => 22, "DO" => 28, "EE" => 20, "FO" => 18, "FI" => 18, "FR" => 27,
795
+ "GF" => 27, "PF" => 27, "TF" => 27, "GE" => 22, "GI" => 23, "GR" => 27, "GL" => 18, "GP" => 27,
796
+ "GT" => 28, "HK" => 16, "IE" => 22, "IS" => 26, "IL" => 23, "IT" => 27, "VG" => 24, "KZ" => 20,
797
+ "QA" => 29, "HR" => 21, "KW" => 30, "LV" => 21, "LB" => 28, "LI" => 21, "LT" => 20, "LU" => 20,
798
+ "MT" => 31, "MA" => 24, "MQ" => 27, "MR" => 27, "MU" => 30, "YT" => 27, "MK" => 19, "MD" => 24,
799
+ "MC" => 27, "ME" => 22, "NC" => 27, "NL" => 18, "NO" => 15, "AT" => 20, "PK" => 24, "PS" => 29,
800
+ "PL" => 28, "PT" => 25, "RE" => 27, "RO" => 24, "BL" => 27, "MF" => 27, "SM" => 27, "SA" => 24,
801
+ "SE" => 24, "CH" => 21, "RS" => 22, "SK" => 24, "SI" => 19, "ES" => 24, "PM" => 27, "CZ" => 24,
802
+ "TN" => 24, "TR" => 26, "HU" => 28, "AE" => 23, "GB" => 22, "WF" => 27, "CY" => 28
803
+ }.freeze
804
+
805
+ # ------------------------------------------------------------- encoding
806
+
807
+ # Output encoding of the descriptive texts (retval2txt) and of the fields
808
+ # name, name_kurz, ort: 1 = ISO-8859-1, 2 = UTF-8 (default), 3 = HTML
809
+ # entities, 4 = DOS CP850, 51..54 = short macro names for retval2txt with
810
+ # the field encoding 1..4. mode 0 returns the current encoding.
811
+ def encoding(mode = 0)
812
+ case mode
813
+ when 0 then return @encoding
814
+ when 1, "i", "I" then @encoding = 1
815
+ when 2, "u", "U" then @encoding = 2
816
+ when 3, "h", "H" then @encoding = 3
817
+ when 4, "d", "D" then @encoding = 4
818
+ when 51, 52, 53, 54, "m", "M" then @encoding = mode.is_a?(Integer) ? mode : 51
819
+ end
820
+ @encoding
821
+ end
822
+
823
+ def encoding_str(mode = 0)
824
+ case encoding(mode)
825
+ when 1 then "ISO-8859-1"
826
+ when 2 then "UTF-8"
827
+ when 3 then "HTML entities"
828
+ when 4 then "DOS CP-850"
829
+ when 51 then "Makro/ISO-8859-1"
830
+ when 52 then "Makro/UTF-8"
831
+ when 53 then "Makro/HTML"
832
+ when 54 then "Makro/DOS CP-850"
833
+ else "Unbekannte Kodierung"
834
+ end
835
+ end
836
+
837
+ # Converts a (UTF-8) string to the current output encoding
838
+ def encode_out(str, enc = @encoding)
839
+ return str if str.nil?
840
+ case enc % 10
841
+ when 1 then str.encode("ISO-8859-1", undef: :replace)
842
+ when 3 then str.gsub(/[äöüÄÖÜß]/, HTML_ENTITIES)
843
+ when 4 then str.encode("CP850", undef: :replace)
844
+ else str
845
+ end
846
+ end
847
+
848
+ HTML_ENTITIES = { "ä" => "&auml;", "ö" => "&ouml;", "ü" => "&uuml;", "Ä" => "&Auml;",
849
+ "Ö" => "&Ouml;", "Ü" => "&Uuml;", "ß" => "&szlig;" }.freeze
850
+
851
+ def retval2txt(retval)
852
+ return retval2txt_short(retval) if @encoding >= 50
853
+ encode_out(retval2utf8(retval))
854
+ end
855
+
856
+ def retval2utf8(retval)
857
+ RETVAL_TEXT.fetch(retval, RETVAL_UNKNOWN_TEXT)
858
+ end
859
+
860
+ def retval2iso(retval)
861
+ retval2utf8(retval).encode("ISO-8859-1", undef: :replace)
862
+ end
863
+
864
+ def retval2dos(retval)
865
+ retval2utf8(retval).encode("CP850", undef: :replace)
866
+ end
867
+
868
+ def retval2html(retval)
869
+ RETVAL_HTML.fetch(retval) { retval2utf8(retval).gsub(/[äöüÄÖÜß]/, HTML_ENTITIES) }
870
+ end
871
+
872
+ def retval2txt_short(retval)
873
+ RETVAL_SHORT.fetch(retval, RETVAL_UNKNOWN_SHORT)
874
+ end
875
+
876
+ # Enables/disables the check method changes of 2019-12-09 (only affects
877
+ # the version string; the methods themselves are implemented).
878
+ def pz_aenderungen_enable(set = -1)
879
+ @pz_aenderungen_2019_12 = (set == 1) if set == 0 || set == 1
880
+ @pz_aenderungen_2019_12 ? 1 : 0
881
+ end
882
+
883
+ def version(mode = 0)
884
+ case mode
885
+ when 1 then VERSION
886
+ when 2 then VERSION_DATE
887
+ when 3 then "#{VERSION_DATE}, 00:00:00"
888
+ when 4 then @pz_aenderungen_2019_12 ? PZ_METHODS_DATE : "09.09.2019 (Aenderungen vom 09.12.2019 enthalten aber noch nicht aktiviert)"
889
+ when 5 then IBAN_RULES_DATE
890
+ when 6 then C_LIBRARY_DATE
891
+ when 7 then "final"
892
+ when 8 then VERSION.split(".")[0]
893
+ when 9 then VERSION.split(".")[1]
894
+ else "konto_check_ruby Version #{VERSION} vom #{VERSION_DATE}, Copyright (C) 2026 tickettoaster GmbH (Ruby-Port von konto_check #{C_LIBRARY_VERSION} vom #{C_LIBRARY_DATE}, Copyright (C) 2002-2023 Michael Plugge)"
895
+ end
896
+ end
897
+
898
+ # ------------------------------------------------------------- internals
899
+
900
+ # Loads the blocks needed for the IBAN functions if necessary (iban_init in C)
901
+ def iban_init
902
+ return LUT2_NOT_INITIALIZED unless @data
903
+ return LUT2_NOT_ALL_IBAN_BLOCKS_LOADED if @extra_init_done < 0
904
+ return OK if @extra_init_done > 0
905
+ d = @data
906
+ if d.loeschung.nil? || d.aenderung.nil? || d.iban_regel.nil? || d.bic.nil? || d.nachfolge_blz.nil?
907
+ @extra_init_done = 1
908
+ code = d.load_blocks(BlzFile::LUT_SET_IBAN + [LUT2_OWN_IBAN])
909
+ reset_search_cache
910
+ # Deviation from the C library: a missing IBAN_REGEL block is not
911
+ # fatal here (the C library refuses all IBAN and BIC functions then);
912
+ # without the block the standard rule 0 is used for all banks.
913
+ if code < 0 && (d.loeschung.nil? || d.aenderung.nil? || d.bic.nil? || d.nachfolge_blz.nil?)
914
+ @extra_init_done = -1
915
+ return LUT2_NOT_ALL_IBAN_BLOCKS_LOADED
916
+ end
917
+ end
918
+ @extra_init_done = 1
919
+ OK
920
+ end
921
+
922
+ def today
923
+ @current_date || LutFile.today_int
924
+ end
925
+
926
+ private
927
+
928
+ def required_blocks(level)
929
+ BlzFile::LUT_SETS[level] || BlzFile::LUT_SETS[9]
930
+ end
931
+
932
+ def file_id_of(lut_name, set)
933
+ code, i1, i2, v1, v2 = LutFile.info(lut_name, today)
934
+ return nil unless code == OK
935
+ info = case set
936
+ when 1 then i1
937
+ when 2 then i2
938
+ else
939
+ if v1 == LUT2_VALID then i1
940
+ elsif v2 == LUT2_VALID then i2
941
+ elsif v1 == LUT2_NO_LONGER_VALID_BETTER then i1
942
+ elsif v2 == LUT2_NO_LONGER_VALID_BETTER then i2
943
+ else i1
944
+ end
945
+ end
946
+ LutFile.file_id(info)
947
+ end
948
+
949
+ def branch_ok?(idx, zweigstelle)
950
+ return false if zweigstelle < 0
951
+ if @data.filialen
952
+ zweigstelle < @data.filialen[idx]
953
+ else
954
+ zweigstelle == 0
955
+ end
956
+ end
957
+
958
+ def field_s(field, error, blz, zweigstelle)
959
+ return [nil, LUT2_BLZ_NOT_INITIALIZED] unless @data
960
+ arr = @data.public_send(field)
961
+ return [nil, error] unless arr
962
+ idx = lut_index(blz)
963
+ return [nil, idx] if idx < 0
964
+ return [nil, LUT2_INDEX_OUT_OF_RANGE] unless branch_ok?(idx, zweigstelle)
965
+ [arr[@data.startidx[idx] + zweigstelle], OK]
966
+ end
967
+
968
+ def field_i(field, error, blz, zweigstelle)
969
+ return [0, LUT2_BLZ_NOT_INITIALIZED] unless @data
970
+ arr = @data.public_send(field)
971
+ return [0, error] unless arr
972
+ idx = lut_index(blz)
973
+ return [0, idx] if idx < 0
974
+ return [0, LUT2_INDEX_OUT_OF_RANGE] unless branch_ok?(idx, zweigstelle)
975
+ [arr[@data.startidx[idx] + zweigstelle], OK]
976
+ end
977
+
978
+ # kto_check_int prolog + dispatch
979
+ def check_int(blz, pz_methode, kto, um, rv)
980
+ digits = CheckMethods.normalize_kto(kto)
981
+ return INVALID_KTO_LENGTH unless digits
982
+ return INVALID_KTO if digits.any? { |d| d < 0 || d > 9 }
983
+ CheckMethods.check_digits(pz_methode, digits, blz, um, rv)
984
+ end
985
+
986
+ # letters -> two digits (A=10 ... Z=35), digits unchanged, others dropped
987
+ def to_numeric(str)
988
+ str.each_char.map do |c|
989
+ if c =~ /\d/ then c
990
+ elsif c =~ /[A-Za-z]/ then (c.upcase.ord - 55).to_s
991
+ else ""
992
+ end
993
+ end.join
994
+ end
995
+
996
+ def iban_checksum(country, bban)
997
+ rest = (to_numeric(bban) + to_numeric(country) + "00").to_i % 97
998
+ format("%02d", 98 - rest)
999
+ end
1000
+
1001
+ def iban_checksum_ok?(iban)
1002
+ return false if iban.length < 5
1003
+ (to_numeric(iban[4..]) + to_numeric(iban[0, 2]) + iban[2, 2]).to_i % 97 == 1
1004
+ end
1005
+ end
1006
+ end