ibandit 1.13.0 → 1.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +5 -0
- data/bin/build_structure_file.rb +221 -114
- data/data/raw/IBANSTRUCTURE.xml +1134 -292
- data/data/raw/IBAN_Registry.txt +51 -48
- data/data/raw/pseudo_ibans.yml +52 -0
- data/data/raw/structure_additions.yml +29 -1
- data/data/structures.yml +524 -25
- data/lib/ibandit/constants.rb +1 -1
- data/lib/ibandit/iban.rb +2 -2
- data/lib/ibandit/iban_assembler.rb +1 -1
- data/lib/ibandit/local_details_cleaner.rb +7 -7
- data/lib/ibandit/sweden/local_details_converter.rb +1 -1
- data/lib/ibandit/version.rb +1 -1
- data/spec/ibandit/iban_assembler_spec.rb +48 -0
- data/spec/ibandit/iban_spec.rb +61 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a67c31f07d7b33b4f60998d4e4fd97473c72c36f23817bb07e5d1d632fa7aba9
|
4
|
+
data.tar.gz: 680db335cd4e6697c45f42550145ed5cce83262ab83eb6abb0d929579cfb00fb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5498e32bae4a3779c2a5965d3b71c375ee823582803bbf2e78a7667476cbd7af6be0b7bbdd101a14bd678bd262653e14f481a52cbe8a65a728a10e0916441557
|
7
|
+
data.tar.gz: 8d4314276d23c4cc338d9b31d634a48fef8ad4c4c3aface176d99361550a9dbf15a83429ed90fc22aacec48dd5f3ac20dcf12af675535d97d52fe3453ba3f4b5
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
data/bin/build_structure_file.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
+
# rubocop:disable Layout/LineLength
|
5
|
+
|
4
6
|
# Script for parsing the IBAN registry (IBAN_Registry.txt) and IBAN structures
|
5
7
|
# (IBANSTRUCTURE.xml) files from SWIFT.
|
6
8
|
require "csv"
|
@@ -22,143 +24,240 @@ end
|
|
22
24
|
|
23
25
|
class Report
|
24
26
|
include SAXMachine
|
25
|
-
elements "
|
27
|
+
elements "ibanstructure_v2", as: :countries, class: Country
|
26
28
|
end
|
27
29
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
30
|
+
class IbanRegistryTextFile
|
31
|
+
attr_accessor :lines, :registry
|
32
|
+
|
33
|
+
FILE_ELEMENTS = [
|
34
|
+
# 0 Data element
|
35
|
+
# 1 Name of country
|
36
|
+
# 2 IBAN prefix country code (ISO 3166)
|
37
|
+
COUNTRY_CODE = 2,
|
38
|
+
# 3 Country code includes other countries/territories
|
39
|
+
# 4 SEPA country
|
40
|
+
# 5 SEPA country also includes
|
41
|
+
# 6 Domestic account number example
|
42
|
+
DOMESTIC_ACCOUNT_NUMBER_EXAMPLE = 6,
|
43
|
+
# 7 BBAN
|
44
|
+
# 8 BBAN structure
|
45
|
+
BBAN_STRUCTURE = 8,
|
46
|
+
# 9 BBAN length
|
47
|
+
# 10 Bank identifier position within the BBAN
|
48
|
+
BANK_IDENTIFIER_POSITION = 10,
|
49
|
+
# 11 Bank identifier pattern
|
50
|
+
BANK_IDENTIFIER_PATTERN = 11,
|
51
|
+
# 12 Branch identifier position within the BBAN
|
52
|
+
BRANCH_IDENTIFIER_POSITION = 12,
|
53
|
+
# 13 Branch identifier pattern
|
54
|
+
BRANCH_IDENTIFIER_PATTERN = 13,
|
55
|
+
# 14 Bank identifier example
|
56
|
+
# 15 Branch identifier example
|
57
|
+
# 16 BBAN example
|
58
|
+
BBAN_EXAMPLE = 16,
|
59
|
+
# 17 IBAN
|
60
|
+
# 18 IBAN structure
|
61
|
+
# 19 IBAN length
|
62
|
+
# 20 Effective date
|
63
|
+
# 21 IBAN electronic format example
|
64
|
+
IBAN_EXAMPLE = 21,
|
65
|
+
].freeze
|
66
|
+
|
67
|
+
def self.call(path = "../data/raw/IBAN_Registry.txt")
|
68
|
+
lines = CSV.read(
|
69
|
+
File.expand_path(path, __dir__),
|
70
|
+
col_sep: "\t",
|
71
|
+
headers: true,
|
72
|
+
encoding: Encoding::ISO_8859_1,
|
73
|
+
).to_a.transpose.tap(&:shift)
|
74
|
+
|
75
|
+
new(lines).tap(&:parse)
|
44
76
|
end
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
# 0 Data element
|
50
|
-
# 1 Name of country
|
51
|
-
# 2 IBAN prefix country code (ISO 3166)
|
52
|
-
COUNTRY_CODE = 2,
|
53
|
-
# 3 Country code includes other countries/territories
|
54
|
-
# 4 SEPA country
|
55
|
-
# 5 SEPA country also includes
|
56
|
-
# 6 Domestic account number example
|
57
|
-
# 7 BBAN
|
58
|
-
# 8 BBAN structure
|
59
|
-
BBAN_STRUCTURE = 8,
|
60
|
-
# 9 BBAN length
|
61
|
-
# 10 Bank identifier position within the BBAN
|
62
|
-
# 11 Bank identifier pattern
|
63
|
-
BANK_IDENTIFIER_PATTERN = 11,
|
64
|
-
# 12 Branch identifier position within the BBAN
|
65
|
-
# 13 Branch identifier pattern
|
66
|
-
BRANCH_IDENTIFIER_PATTERN = 13,
|
67
|
-
# 14 Bank identifier example
|
68
|
-
# 15 Branch identifier example
|
69
|
-
# 16 BBAN example
|
70
|
-
# 17 IBAN
|
71
|
-
# 18 IBAN structure
|
72
|
-
# 19 IBAN length
|
73
|
-
# 20 Effective date
|
74
|
-
# 21 IBAN electronic format example
|
75
|
-
].freeze
|
76
|
-
|
77
|
-
def get_bban_formats(iban_registry_file)
|
78
|
-
iban_registry_file.each_with_object({}) do |line, hash|
|
79
|
-
bban_structure = line[BBAN_STRUCTURE].strip
|
80
|
-
|
81
|
-
bank_code_structure = line[BANK_IDENTIFIER_PATTERN].strip
|
82
|
-
branch_code_structure = line[BRANCH_IDENTIFIER_PATTERN]&.strip
|
83
|
-
|
84
|
-
bank_code_structure = "" if bank_code_structure == "N/A"
|
85
|
-
|
86
|
-
country_code = line[COUNTRY_CODE].strip
|
87
|
-
hash[country_code] = convert_swift_convention(bban_structure,
|
88
|
-
bank_code_structure,
|
89
|
-
branch_code_structure)
|
77
|
+
|
78
|
+
def initialize(lines)
|
79
|
+
@lines = lines
|
80
|
+
@registry = {}
|
90
81
|
end
|
91
|
-
end
|
92
82
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
83
|
+
def parse
|
84
|
+
lines.each do |line|
|
85
|
+
country_code = clean_string(line[COUNTRY_CODE])
|
86
|
+
|
87
|
+
bban_details = convert_swift_convention(
|
88
|
+
country_code: country_code,
|
89
|
+
bban_structure: clean_string(line[BBAN_STRUCTURE]),
|
90
|
+
bank_code_structure: clean_string(line[BANK_IDENTIFIER_PATTERN]),
|
91
|
+
branch_code_structure: clean_string(line[BRANCH_IDENTIFIER_PATTERN]),
|
92
|
+
bank_identifier_position: clean_string(line[BANK_IDENTIFIER_POSITION]),
|
93
|
+
branch_identifier_position: clean_string(line[BRANCH_IDENTIFIER_POSITION]),
|
94
|
+
) || {}
|
95
|
+
|
96
|
+
registry[country_code] = {
|
97
|
+
iban_example: clean_string(line[IBAN_EXAMPLE]),
|
98
|
+
bban_example: clean_string(line[BBAN_EXAMPLE]),
|
99
|
+
domestic_account_number_example: clean_string(line[DOMESTIC_ACCOUNT_NUMBER_EXAMPLE]),
|
100
|
+
**bban_details,
|
101
|
+
}.compact
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def clean_string(string)
|
108
|
+
return nil if string.nil?
|
109
|
+
|
110
|
+
string.strip!
|
111
|
+
return nil if string == "N/A"
|
112
|
+
|
113
|
+
string
|
114
|
+
end
|
115
|
+
|
116
|
+
# IBAN Registry has BBAN format (which seems to be accurate), and Bank
|
117
|
+
# identifier length, which contains something roughly like the format for the
|
118
|
+
# bank code and usually the branch code where applicable. This is a best attempt
|
119
|
+
# to convert those from weird SWIFT-talk into regexes, and then work out the
|
120
|
+
# account number format regex by taking the bank and branch code regexes off
|
121
|
+
# the front of the BBAN format.
|
122
|
+
#
|
123
|
+
# This works about 90% of the time, the rest are overridden in
|
124
|
+
# structure_additions.yml
|
125
|
+
def convert_swift_convention( # rubocop:todo Metrics/AbcSize
|
126
|
+
country_code:,
|
127
|
+
bban_structure:,
|
128
|
+
branch_code_structure:,
|
129
|
+
bank_code_structure: nil,
|
130
|
+
bank_identifier_position: nil,
|
131
|
+
branch_identifier_position: nil
|
132
|
+
)
|
133
|
+
bban_regex = iban_registry_to_regex(bban_structure)
|
134
|
+
bank_regex = iban_registry_to_regex(bank_code_structure)
|
135
|
+
branch_regex = branch_code_structure.nil? ? nil : iban_registry_to_regex(branch_code_structure)
|
136
|
+
|
137
|
+
bban_ranges = create_bban_ranges(bban_structure)
|
138
|
+
ranges_to_remove = [
|
139
|
+
convert_string_range(bank_identifier_position),
|
140
|
+
convert_string_range(branch_identifier_position),
|
141
|
+
].compact.uniq
|
142
|
+
max_bank_details_index = ranges_to_remove.map(&:last).max
|
143
|
+
|
144
|
+
_, non_bank_identifier_ranges = bban_ranges.partition do |_, range|
|
145
|
+
max_bank_details_index >= range.last
|
146
|
+
end
|
147
|
+
|
148
|
+
account_number_regex = iban_registry_to_regex(non_bank_identifier_ranges.map(&:first).join)
|
149
|
+
|
150
|
+
{
|
151
|
+
bban_format: bban_regex.source,
|
152
|
+
bank_code_format: bank_regex.source,
|
153
|
+
branch_code_format: branch_regex&.source,
|
154
|
+
account_number_format: account_number_regex.source,
|
155
|
+
}
|
156
|
+
rescue StandardError => e
|
157
|
+
puts "-----------------"
|
158
|
+
puts "Issue with: #{country_code}"
|
159
|
+
puts "\t #{e.message}"
|
160
|
+
puts "\t #{e.backtrace}"
|
161
|
+
puts "\t -----------------"
|
162
|
+
puts "\t country_code: #{country_code}"
|
163
|
+
puts "\t bban_structure: #{bban_structure}"
|
164
|
+
puts "\t branch_code_structure: #{branch_code_structure}"
|
165
|
+
puts "\t bank_code_structure: #{bank_code_structure}"
|
166
|
+
puts "\t bank_identifier_position: #{bank_identifier_position}"
|
167
|
+
puts "\t branch_identifier_position: #{branch_identifier_position}"
|
168
|
+
end
|
169
|
+
|
170
|
+
# Given "4!n4!n12!c" this returns an array that contains the ranges that cover the
|
171
|
+
# structure. Eg; [["4!n", 0..3]]
|
172
|
+
def create_bban_ranges(bban_structure)
|
173
|
+
arr = bban_structure.scan(/((\d+)![anc])/)
|
174
|
+
|
175
|
+
start = 0
|
176
|
+
|
177
|
+
arr.each_with_object([]) do |(structure, length), acc|
|
178
|
+
end_number = start + length.to_i - 1
|
179
|
+
acc.push([structure, start..end_number])
|
180
|
+
start = end_number + 1
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def convert_string_range(str)
|
185
|
+
start_val, end_val = str.split("-").map(&:to_i)
|
186
|
+
(start_val - 1)..(end_val - 1)
|
187
|
+
rescue StandardError
|
188
|
+
nil
|
189
|
+
end
|
190
|
+
|
191
|
+
def iban_registry_to_regex(swift_string)
|
192
|
+
regex = swift_string.
|
193
|
+
gsub(/(\d+)!n/, '\\d{\1}').
|
194
|
+
gsub(/(\d+)!a/, '[A-Z]{\1}').
|
195
|
+
gsub(/(\d+)!c/, '[A-Z0-9]{\1}')
|
196
|
+
Regexp.new(regex)
|
197
|
+
end
|
118
198
|
end
|
119
199
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
200
|
+
class IbanStructureFile
|
201
|
+
attr_accessor :report, :iban_registry_file
|
202
|
+
|
203
|
+
def self.call(iban_registry_file, path: "../data/raw/IBANSTRUCTURE.xml")
|
204
|
+
iban_structures_file = File.read(File.expand_path(path, __dir__))
|
205
|
+
new(iban_registry_file:, iban_structures_file:).parse
|
206
|
+
end
|
207
|
+
|
208
|
+
def initialize(iban_registry_file:, iban_structures_file:)
|
209
|
+
@iban_registry_file = iban_registry_file
|
210
|
+
@report = Report.parse(iban_structures_file)
|
211
|
+
end
|
212
|
+
|
213
|
+
def parse # rubocop:todo Metrics/AbcSize
|
214
|
+
report.countries.each_with_object({}) do |country, hash|
|
215
|
+
country_bban = iban_registry_file.registry[country.country_code] || {}
|
216
|
+
|
217
|
+
hash[country.country_code] = {
|
218
|
+
bank_code_position: country.bank_code_position.to_i,
|
219
|
+
bank_code_length: country.bank_code_length.to_i,
|
220
|
+
branch_code_position: country.branch_code_position.to_i,
|
221
|
+
branch_code_length: country.branch_code_length.to_i,
|
222
|
+
account_number_position: country.account_number_position.to_i,
|
223
|
+
account_number_length: country.account_number_length.to_i,
|
224
|
+
total_length: country.total_length.to_i,
|
225
|
+
national_id_length: country.national_id_length.to_i,
|
226
|
+
**country_bban,
|
227
|
+
}
|
228
|
+
end
|
229
|
+
end
|
125
230
|
end
|
126
231
|
|
127
232
|
def merge_structures(structures, additions)
|
128
233
|
additions.each_pair do |key, value|
|
129
|
-
structures[key].merge!(value) if structures.include?(key)
|
234
|
+
structures[key].merge!(value).compact! if structures.include?(key)
|
130
235
|
end
|
131
236
|
|
132
237
|
structures
|
133
238
|
end
|
134
239
|
|
240
|
+
def load_yaml_file(path)
|
241
|
+
YAML.safe_load(
|
242
|
+
File.read(File.expand_path(path, __dir__)),
|
243
|
+
permitted_classes: [Range, Symbol, Regexp],
|
244
|
+
)
|
245
|
+
end
|
246
|
+
|
135
247
|
# Only parse the files if this file is run as an executable (not required in,
|
136
248
|
# as it is in the specs)
|
137
249
|
if __FILE__ == $PROGRAM_NAME
|
138
|
-
|
139
|
-
File.expand_path("../data/raw/IBAN_Registry.txt", __dir__),
|
140
|
-
col_sep: "\t",
|
141
|
-
headers: true,
|
142
|
-
encoding: Encoding::ISO_8859_1,
|
143
|
-
).to_a.transpose
|
144
|
-
|
145
|
-
iban_registry_file.shift
|
146
|
-
|
147
|
-
iban_structures_file = File.read(
|
148
|
-
File.expand_path("../data/raw/IBANSTRUCTURE.xml", __dir__),
|
149
|
-
)
|
250
|
+
old_file = load_yaml_file("../data/structures.yml")
|
150
251
|
|
151
|
-
|
152
|
-
|
153
|
-
iban_registry_file,
|
154
|
-
)
|
252
|
+
iban_registry_file = IbanRegistryTextFile.call
|
253
|
+
iban_structures = IbanStructureFile.call(iban_registry_file)
|
155
254
|
|
156
|
-
structure_additions =
|
157
|
-
File.read(File.expand_path("../data/raw/structure_additions.yml", __dir__)),
|
158
|
-
permitted_classes: [Range, Symbol],
|
159
|
-
)
|
255
|
+
structure_additions = load_yaml_file("../data/raw/structure_additions.yml")
|
160
256
|
|
161
257
|
complete_structures = merge_structures(iban_structures, structure_additions)
|
258
|
+
pseudo_ibans = load_yaml_file("../data/raw/pseudo_ibans.yml")
|
259
|
+
|
260
|
+
complete_structures.merge!(pseudo_ibans)
|
162
261
|
|
163
262
|
output_file_path = File.expand_path(
|
164
263
|
"../data/structures.yml",
|
@@ -166,4 +265,12 @@ if __FILE__ == $PROGRAM_NAME
|
|
166
265
|
)
|
167
266
|
|
168
267
|
File.open(output_file_path, "w") { |f| f.write(complete_structures.to_yaml) }
|
268
|
+
|
269
|
+
new_countries = old_file.keys.to_set ^ complete_structures.keys.to_set
|
270
|
+
puts "New countries:"
|
271
|
+
new_countries.each do |country|
|
272
|
+
puts "#{country} #{complete_structures[country][:iban_example]} #{complete_structures[country][:domestic_account_number_example]}"
|
273
|
+
end
|
169
274
|
end
|
275
|
+
|
276
|
+
# rubocop:enable Layout/LineLength
|