iso-iban 0.0.1

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.
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