iso-iban 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/iso-iban.gemspec ADDED
@@ -0,0 +1,42 @@
1
+ # encoding: utf-8
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "iso-iban"
5
+ s.version = "0.0.1"
6
+ s.authors = "Stefan Rusterholz"
7
+ s.email = "stefan.rusterholz@gmail.com"
8
+ s.homepage = "https://github.com/apeiros/iso-iban"
9
+ s.license = 'BSD 2-Clause'
10
+
11
+ s.description = <<-DESCRIPTION.gsub(/^ /, '').chomp
12
+ ISO::IBAN implements IBAN (International Bank Account Number) specification as per ISO 13616-1.
13
+ It provides methods to generate valid IBAN numbers from components, or to validate a given IBAN.
14
+ DESCRIPTION
15
+ s.summary = <<-SUMMARY.gsub(/^ /, '').chomp
16
+ Utilities for International Bank Account Numbers (IBAN) as per ISO 13616-1.
17
+ SUMMARY
18
+
19
+ s.files =
20
+ Dir['bin/**/*'] +
21
+ Dir['data/**/*'] +
22
+ Dir['documentation/**/*'] +
23
+ Dir['lib/**/*'] +
24
+ Dir['rake/**/*'] +
25
+ Dir['test/**/*'] +
26
+ Dir['*.gemspec'] +
27
+ %w[
28
+ .yardopts
29
+ LICENSE.txt
30
+ Rakefile
31
+ README.markdown
32
+ ]
33
+
34
+ if File.directory?('bin') then
35
+ s.executables = Dir.chdir('bin') { Dir.glob('**/*').select { |f| File.executable?(f) } }
36
+ end
37
+
38
+ s.required_ruby_version = ">= 1.9.2"
39
+ s.required_rubygems_version = Gem::Requirement.new("> 1.3.1")
40
+ s.rubygems_version = "1.3.1"
41
+ s.specification_version = 3
42
+ end
data/lib/iso/iban.rb ADDED
@@ -0,0 +1,324 @@
1
+ # encoding: utf-8
2
+
3
+ require 'iso/iban/specification'
4
+ require 'iso/iban/version'
5
+
6
+ module ISO
7
+
8
+ # IBAN - ISO 13616-1
9
+ #
10
+ # General IBAN Information
11
+ # ========================
12
+ #
13
+ # * What is an IBAN?
14
+ # IBAN stands for International Bank Account Number. It is the ISO 13616
15
+ # international standard for numbering bank accounts. In 2006, the
16
+ # International Organization for Standardization (ISO) designated SWIFT as
17
+ # the Registration Authority for ISO 13616.
18
+ #
19
+ # * Use
20
+ # The IBAN facilitates the communication and processing of cross-border
21
+ # transactions. It allows exchanging account identification details in a
22
+ # machine-readable form.
23
+ #
24
+ #
25
+ # The ISO 13616 IBAN Standard
26
+ # ===========================
27
+ #
28
+ # * Structure
29
+ # The IBAN structure is defined in ISO 13616-1 and consists of a two-letter
30
+ # ISO 3166-1 country code, followed by two check digits and up to thirty
31
+ # alphanumeric characters for a BBAN (Basic Bank Account Number) which has a
32
+ # fixed length per country and, included within it, a bank identifier with a
33
+ # fixed position and a fixed length per country. The check digits are
34
+ # calculated based on the scheme defined in ISO/IEC 7064 (MOD97-10).
35
+ #
36
+ # * Terms and definitions
37
+ # Bank identifier: The identifier that uniquely identifies the financial
38
+ # institution and, when appropriate, the branch of that financial institution
39
+ # servicing an account
40
+ #
41
+ # `In this registry, the branch identifier format is shown specifically, when
42
+ # present.`
43
+ #
44
+ # *BBAN*: basic bank account number: The identifier that uniquely identifies
45
+ # an individual account, at a specific financial institution, in a particular
46
+ # country. The BBAN includes a bank identifier of the financial institution
47
+ # servicing that account.
48
+ # *IBAN*: international bank account number: The expanded version of the
49
+ # basic bank account number (BBAN), intended for use internationally. The
50
+ # IBAN uniquely identifies an individual account, at a specific financial
51
+ # institution, in a particular country.
52
+ #
53
+ # * Submitters
54
+ # Nationally-agreed, ISO13616-compliant IBAN formats are submitted to the
55
+ # registration authority exclusively by the National Standards Body or the
56
+ # National Central Bank of the country.
57
+ class IBAN
58
+ include Comparable
59
+
60
+ # Character code translation used to convert an IBAN into its numeric
61
+ # (digits-only) form
62
+ CharacterCodes = Hash[('0'..'9').zip('0'..'9')+('a'..'z').zip(10..35)+('A'..'Z').zip(10..35)]
63
+
64
+ # All specifications, see ISO::IBAN::Specification
65
+ @specifications = nil
66
+
67
+ # Load the IBAN specifications file, which determines how the IBAN
68
+ # for any given country looks like.
69
+ #
70
+ # It will use the following sources in this order (first one which exists wins)
71
+ #
72
+ # * Path passed as spec_file parameter
73
+ # * Path provided by the env variable IBAN_SPECIFICATIONS
74
+ # * The file ../data/iso-iban/specs.yaml relative to the lib dir
75
+ # * The Gem datadir path
76
+ #
77
+ # @param [String] spec_file
78
+ # Override the default specifications file path.
79
+ #
80
+ # @return [self]
81
+ def self.load_specifications(spec_file=nil)
82
+ if spec_file then
83
+ # do nothing
84
+ elsif ENV['IBAN_SPECIFICATIONS'] then
85
+ spec_file = ENV['IBAN_SPECIFICATIONS']
86
+ else
87
+ spec_file = File.expand_path('../../../../data/iso-iban/specs.yaml', __FILE__)
88
+ spec_file = Gem.datadir('iso-iban')+'/specs.yaml' if defined?(Gem) && !File.file?(spec_file)
89
+ end
90
+
91
+ @specifications = ISO::IBAN::Specification.load_yaml(spec_file)
92
+
93
+ self
94
+ end
95
+
96
+ # @return [Hash<String => ISO::IBAN::Specification>]
97
+ # A hash with the country (ISO3166 2-letter) as key and the specification for that country as value
98
+ def self.specifications
99
+ @specifications || raise("No specifications have been loaded yet.")
100
+ end
101
+
102
+ # @param [String] a2_country_code
103
+ # The country (ISO3166 2-letter), e.g. 'CH' or 'DE'.
104
+ #
105
+ # @return [ISO::IBAN::Specification]
106
+ # The specification for the given country
107
+ def self.specification(a2_country_code, *default, &default_block)
108
+ specifications.fetch(a2_country_code, *default, &default_block)
109
+ end
110
+
111
+ # @param [String] iban
112
+ # An IBAN number, either in compact or human format.
113
+ #
114
+ # @return [true, false]
115
+ # Whether the IBAN is valid.
116
+ # See {#validate} for details.
117
+ def self.valid?(iban)
118
+ new(iban).valid?
119
+ end
120
+
121
+ # @param [String] iban
122
+ # An IBAN number, either in compact or human format.
123
+ #
124
+ # @return [Array<Symbol>]
125
+ # An array with a code of all validation errors, empty if valid.
126
+ # See {#validate} for details.
127
+ def self.validate(iban)
128
+ new(iban).validate
129
+ end
130
+
131
+ # @param [String] iban
132
+ # The IBAN in either compact or human readable form.
133
+ #
134
+ # @return [String]
135
+ # The IBAN in compact form.
136
+ def self.strip(iban)
137
+ iban.tr(' -', '')
138
+ end
139
+
140
+ # Generate an IBAN from country code and components, automatically filling in the checksum.
141
+ #
142
+ # @example Generate an IBAN for UBS Switzerland with account number '12345'
143
+ # ISO::IBAN.generate('CH', '216', '12345') # => #<ISO::IBAN CH92 0021 6000 0000 1234 5>
144
+ #
145
+ # @param [String] country
146
+ # The ISO-3166 2-letter country code.
147
+ #
148
+ def self.generate(country, *components)
149
+ spec = specification(country)
150
+ justified = spec.component_lengths.zip(components).map { |length, component| component.rjust(length, "0") }
151
+ iban = new(country+'??'+justified.join(''))
152
+ iban.update_checksum!
153
+
154
+ iban
155
+ end
156
+
157
+ # Converts a String into its digits-only form, i.e. all characters a-z are replaced with their corresponding
158
+ # digit sequences, according to the IBAN specification.
159
+ #
160
+ # @param [String] string
161
+ # The string to convert into its numeric form.
162
+ #
163
+ # @return [String] The string in its numeric, digits-only form.
164
+ def self.numerify(string)
165
+ string.downcase.gsub(/\D/) { |char|
166
+ CharacterCodes.fetch(char) {
167
+ raise ArgumentError, "The string contains an invalid character #{char.inspect}"
168
+ }
169
+ }.to_i
170
+ end
171
+
172
+ # @return [String] The standard form of the IBAN for machine communication, without spaces.
173
+ attr_reader :compact
174
+
175
+ # @return [String] The ISO-3166 2-letter country code.
176
+ attr_reader :country
177
+
178
+ # @return [ISO::IBAN::Specification] The specification for this IBAN (determined by its country).
179
+ attr_reader :specification
180
+
181
+ # @param [String] iban
182
+ # The IBAN number, either in formatted, human readable or in compact form.
183
+ def initialize(iban)
184
+ raise ArgumentError, "String expected for iban, but got #{iban.class}" unless iban.is_a?(String)
185
+
186
+ @compact = self.class.strip(iban)
187
+ @country = iban[0,2]
188
+ @specification = self.class.specification(@country, nil)
189
+ end
190
+
191
+ # @example Formatted IBAN
192
+ #
193
+ # ISO::IBAN.new('CH')
194
+ #
195
+ # @return [String] The IBAN in its formatted form, which is more human readable than the compact form.
196
+ def formatted
197
+ @_formatted ||= @compact.gsub(/.{4}(?=.)/, '\0 ')
198
+ end
199
+
200
+ # @return [String]
201
+ # IBAN in its numeric form, i.e. all characters a-z are replaced with their corresponding
202
+ # digit sequences.
203
+ def numeric
204
+ self.class.numerify(@compact[4..-1]+@compact[0,4])
205
+ end
206
+
207
+ # @return [true, false]
208
+ # Whether the IBAN is valid.
209
+ # See {#validate} for details.
210
+ def valid?
211
+ validate.empty?
212
+ end
213
+
214
+ # Validation error codes:
215
+ #
216
+ # * :invalid_country
217
+ # * :invalid_checksum
218
+ # * :invalid_length
219
+ # * :invalid_format
220
+ #
221
+ # Invalid country means the country is unknown (char 1 & 2 in the IBAN).
222
+ # Invalid checksum means the two check digits (char 3 & 4 in the IBAN).
223
+ # Invalid length means the IBAN does not comply with the length specified for the country of that IBAN.
224
+ # Invalid format means the IBAN does not comply with the format specified for the country of that IBAN.
225
+ #
226
+ # @return [Array<Symbol>] An array with a code of all validation errors, empty if valid.
227
+ def validate
228
+ errors = []
229
+ errors << :invalid_country unless valid_country?
230
+ errors << :invalid_checksum unless valid_checksum?
231
+ errors << :invalid_length unless valid_length?
232
+ errors << :invalid_format unless valid_format?
233
+
234
+ errors
235
+ end
236
+
237
+ # @return [String] The checksum digits in the IBAN.
238
+ def checksum_digits
239
+ @compact[2,2]
240
+ end
241
+
242
+ # @return [String] The BBAN of the IBAN.
243
+ def bban
244
+ @compact[4..-1]
245
+ end
246
+
247
+ # @return [String, nil] The bank code part of the IBAN, nil if not applicable.
248
+ def bank_code
249
+ if @specification && @specification.bank_position_from && @specification.bank_position_to
250
+ @compact[@specification.bank_position_from..@specification.bank_position_to]
251
+ else
252
+ nil
253
+ end
254
+ end
255
+
256
+ # @return [String, nil] The branch code part of the IBAN, nil if not applicable.
257
+ def branch_code
258
+ if @specification && @specification.branch_position_from && @specification.branch_position_to
259
+ @compact[@specification.branch_position_from..@specification.branch_position_to]
260
+ else
261
+ nil
262
+ end
263
+ end
264
+
265
+ # @return [String] The account code part of the IBAN.
266
+ def account_code
267
+ @compact[((@specification.branch_position_to || @specification.bank_position_to || 3)+1)..-1]
268
+ end
269
+
270
+ # @return [true, false] Whether the country of the IBAN is valid.
271
+ def valid_country?
272
+ @specification ? true : false
273
+ end
274
+
275
+ # @return [true, false] Whether the format of the IBAN is valid.
276
+ def valid_format?
277
+ specification && specification.iban_regex =~ @compact ? true : false
278
+ end
279
+
280
+ # @return [true, false] Whether the length of the IBAN is valid.
281
+ def valid_length?
282
+ specification && @compact.size == specification.iban_length ? true : false
283
+ end
284
+
285
+ # @return [true, false] Whether the checksum of the IBAN is valid.
286
+ def valid_checksum?
287
+ numeric % 97 == 1
288
+ end
289
+
290
+ # See Object#<=>
291
+ #
292
+ # @return [-1, 0, 1, nil]
293
+ def <=>(other)
294
+ other.respond_to?(:compact) ? @compact <=> other.compact : nil
295
+ end
296
+
297
+ # Requires that the checksum digits were left as '??', replaces them with
298
+ # the proper checksum.
299
+ #
300
+ # @return [self]
301
+ def update_checksum!
302
+ raise "Checksum digit placeholders missing" unless @compact[2,2] == '??'
303
+
304
+ @compact[2,2] = calculated_check_digits
305
+
306
+ self
307
+ end
308
+
309
+ # @return [String] The check-digits as calculated from the IBAN.
310
+ def calculated_check_digits
311
+ "%02d" % (98-(self.class.numerify(bban+@country)*100)%97)
312
+ end
313
+
314
+ # See Object#inspect
315
+ def inspect
316
+ sprintf "#<%p %s>", self.class, formatted
317
+ end
318
+
319
+ # @return [String] The compact form of the IBAN as a String.
320
+ def to_s
321
+ @compact.dup
322
+ end
323
+ end
324
+ end
@@ -0,0 +1,5 @@
1
+ # This file automatically loads all specifications. It's recommended that you require this file instead of iso/iban.
2
+
3
+ require 'iso/iban'
4
+
5
+ ISO::IBAN.load_specifications
@@ -0,0 +1,128 @@
1
+ # encoding: utf-8
2
+
3
+ module ISO
4
+ class IBAN
5
+
6
+ # Specification of the IBAN format for one country. Every country has its
7
+ # own specification of the IBAN format.
8
+ # SWIFT is the official authority where those formats are registered.
9
+ class Specification
10
+
11
+ # A mapping from SWIFT structure specification to PCRE regex.
12
+ StructureCodes = {
13
+ 'n' => '\d',
14
+ 'a' => '[A-Z]',
15
+ 'c' => '[A-Za-z0-9]',
16
+ 'e' => ' ',
17
+ }
18
+
19
+ # Load the specifications YAML.
20
+ #
21
+ # @return [Hash<String => ISO::IBAN::Specification>]
22
+ def self.load_yaml(path)
23
+ Hash[YAML.load_file(path).map { |country, spec| [country, new(*spec)] }]
24
+ end
25
+
26
+ # Parse the SWIFT provided file (which sadly is a mess).
27
+ #
28
+ # @return [Array<ISO::IBAN::Specification>] an array with all specifications.
29
+ def self.parse_file(path)
30
+ File.read(path, encoding: Encoding::Windows_1252).encode(Encoding::UTF_8).split("\r\n").tap(&:shift).flat_map { |line|
31
+ country_name, country_codes, iban_structure, iban_length, bban_structure, bban_length, bank_position = line.split(/\t/).values_at(0,1,11,12,4,5,6)
32
+ codes = country_codes.size == 2 ? [country_codes] : country_codes.scan(/\b[A-Z]{2}\b/)
33
+ bank_position_from, bank_position_to, branch_position_from, branch_position_to = bank_position.match(/(?:[Pp]ositions?|) (\d+)-(\d+)(?:.*Branch identifier positions?: (\d+)-(\d+))?/).captures.map { |pos| pos && pos.to_i+3 }
34
+
35
+ codes.map { |a2_country_code|
36
+ new(
37
+ country_name.strip,
38
+ a2_country_code,
39
+ iban_structure.strip,
40
+ iban_length.to_i,
41
+ bban_structure.strip,
42
+ bban_length.to_i,
43
+ bank_position_from,
44
+ bank_position_to,
45
+ branch_position_from,
46
+ branch_position_to
47
+ )
48
+ }
49
+ }
50
+ end
51
+
52
+ # *n: Digits (numeric characters 0 to 9 only)
53
+ # *a: Upper case letters (alphabetic characters A-Z only)
54
+ # *c: upper and lower case alphanumeric characters (A-Z, a-z and 0-9)
55
+ # *e: blank space
56
+ # *nn!: fixed length
57
+ # *nn: maximum length
58
+ #
59
+ # Example: "AL2!n8!n16!c"
60
+ def self.structure_regex(structure, anchored=true)
61
+ source = structure.scan(/([A-Z]+)|(\d+)(!?)([nac])/).map { |exact, length, fixed, code|
62
+ if exact
63
+ Regexp.escape(exact)
64
+ else
65
+ StructureCodes[code]+(fixed ? "{#{length}}" : "{,#{length}}")
66
+ end
67
+ }.join('')
68
+
69
+ anchored ? /\A#{source}\z/ : /#{source}/
70
+ end
71
+
72
+ attr_reader :country_name,
73
+ :a2_country_code,
74
+ :iban_structure,
75
+ :iban_length,
76
+ :bban_structure,
77
+ :bban_length,
78
+ :bank_position_from,
79
+ :bank_position_to,
80
+ :branch_position_from,
81
+ :branch_position_to
82
+
83
+ def initialize(country_name, a2_country_code, iban_structure, iban_length, bban_structure, bban_length, bank_position_from, bank_position_to, branch_position_from, branch_position_to)
84
+ @country_name = country_name
85
+ @a2_country_code = a2_country_code
86
+ @iban_structure = iban_structure
87
+ @iban_length = iban_length
88
+ @bban_structure = bban_structure
89
+ @bban_length = bban_length
90
+ @bank_position_from = bank_position_from
91
+ @bank_position_to = bank_position_to
92
+ @branch_position_from = branch_position_from
93
+ @branch_position_to = branch_position_to
94
+ end
95
+
96
+ # @return [Regexp] A regex to verify the structure of the IBAN.
97
+ def iban_regex
98
+ @iban_regex ||= self.class.structure_regex(@iban_structure)
99
+ end
100
+
101
+ # @return [Regexp] A regex to identify the structure of the IBAN, without anchors.
102
+ def unanchored_iban_regex
103
+ self.class.structure_regex(@iban_structure, false)
104
+ end
105
+
106
+ # @return [Array<Integer>] An array with the lengths of all components.
107
+ def component_lengths
108
+ @bban_structure.scan(/\d+/).map(&:to_i)
109
+ end
110
+
111
+ # @return [Array] An array with the Specification properties. Used for serialization.
112
+ def to_a
113
+ [
114
+ @country_name,
115
+ @a2_country_code,
116
+ @iban_structure,
117
+ @iban_length,
118
+ @bban_structure,
119
+ @bban_length,
120
+ @bank_position_from,
121
+ @bank_position_to,
122
+ @branch_position_from,
123
+ @branch_position_to,
124
+ ]
125
+ end
126
+ end
127
+ end
128
+ end