ibandit 1.13.0 → 1.14.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 78a7fd2e1ca3e1062a4200e0d07f961c67783b1f93f69ed348143ee066d2d8f3
4
- data.tar.gz: 0736d59cf783918276ba8fb1376325e58bd37cdbc306c0b05ae583df89aaaf18
3
+ metadata.gz: a67c31f07d7b33b4f60998d4e4fd97473c72c36f23817bb07e5d1d632fa7aba9
4
+ data.tar.gz: 680db335cd4e6697c45f42550145ed5cce83262ab83eb6abb0d929579cfb00fb
5
5
  SHA512:
6
- metadata.gz: f5d478dd806b967e1903abdf20eff7fcca90128ca7e1949deb760ea55aedad4dfc862bf45ff89b11566e5155b0ec9e60fea61bf966e431ac6718ec54d3965aef
7
- data.tar.gz: dcdd42b6b06eea11236d27f7c0bdd9da9cf45f9a2502e5f8bb9e0903351965127645b8c50874871c5ed737bf60389ec0542b2778e9e2cb20c11c746d30a4f593
6
+ metadata.gz: 5498e32bae4a3779c2a5965d3b71c375ee823582803bbf2e78a7667476cbd7af6be0b7bbdd101a14bd678bd262653e14f481a52cbe8a65a728a10e0916441557
7
+ data.tar.gz: 8d4314276d23c4cc338d9b31d634a48fef8ad4c4c3aface176d99361550a9dbf15a83429ed90fc22aacec48dd5f3ac20dcf12af675535d97d52fe3453ba3f4b5
data/.rubocop.yml CHANGED
@@ -4,7 +4,7 @@ inherit_gem:
4
4
  require: rubocop-rails
5
5
 
6
6
  AllCops:
7
- TargetRubyVersion: 2.5
7
+ TargetRubyVersion: 3.2
8
8
 
9
9
  # Limit lines to 90 characters.
10
10
  Layout/LineLength:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 1.14.0 - March 28, 2023
2
+
3
+ - Update IBAN registry #233
4
+ - Add GL and FO to the IBAN constructor for local details
5
+
1
6
  ## 1.13.0 - March 6, 2023
2
7
 
3
8
  - Update BLZ data - BLZ_20230306
@@ -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 "ibanstructure", as: :countries, class: Country
27
+ elements "ibanstructure_v2", as: :countries, class: Country
26
28
  end
27
29
 
28
- # rubocop:disable Metrics/AbcSize
29
- def get_iban_structures(iban_structures_file, iban_registry_file)
30
- bban_formats = get_bban_formats(iban_registry_file)
31
-
32
- report = Report.parse(iban_structures_file)
33
- report.countries.each_with_object({}) do |country, hash|
34
- hash[country.country_code] = {
35
- bank_code_position: country.bank_code_position.to_i,
36
- bank_code_length: country.bank_code_length.to_i,
37
- branch_code_position: country.branch_code_position.to_i,
38
- branch_code_length: country.branch_code_length.to_i,
39
- account_number_position: country.account_number_position.to_i,
40
- account_number_length: country.account_number_length.to_i,
41
- total_length: country.total_length.to_i,
42
- national_id_length: country.national_id_length.to_i,
43
- }.merge(bban_formats[country.country_code])
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
- end
46
- # rubocop:enable Metrics/AbcSize
47
-
48
- FILE_ELEMENTS = [
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
- # IBAN Registry has BBAN format (which seems to be accurate), and Bank
94
- # identifier length, which contains something roughly like the format for the
95
- # bank code and usually the branch code where applicable. This is a best attempt
96
- # to convert those from weird SWIFT-talk into regexes, and then work out the
97
- # account number format regex by taking the bank and branch code regexes off
98
- # the front of the BBAN format.
99
- #
100
- # This works about 70% of the time, the rest are overridden in
101
- # structure_additions.yml
102
- def convert_swift_convention(bban, bank, branch)
103
- bban_regex = iban_registry_to_regex(bban)
104
- bank_regex = iban_registry_to_regex(bank)
105
- branch_regex = branch.nil? ? nil : iban_registry_to_regex(branch)
106
-
107
- non_account_number_regex = [bank_regex, branch_regex].join
108
- account_number_start = (bban_regex.index(non_account_number_regex) || 0) +
109
- non_account_number_regex.length
110
- account_number_regex = bban_regex[account_number_start..-1]
111
-
112
- {
113
- bban_format: bban_regex,
114
- bank_code_format: bank_regex,
115
- branch_code_format: branch_regex,
116
- account_number_format: account_number_regex,
117
- }.compact
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
- def iban_registry_to_regex(swift_string)
121
- swift_string.gsub(/(\d+)!([nac])/, '\2{\1}').
122
- gsub("n", '\d').
123
- gsub("a", "[A-Z]").
124
- gsub("c", "[A-Z0-9]")
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
- iban_registry_file = CSV.read(
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
- iban_structures = get_iban_structures(
152
- iban_structures_file,
153
- iban_registry_file,
154
- )
252
+ iban_registry_file = IbanRegistryTextFile.call
253
+ iban_structures = IbanStructureFile.call(iban_registry_file)
155
254
 
156
- structure_additions = YAML.safe_load(
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