snippetjavascriptMinor
Bank account number validation in IBAN format
Viewed 0 times
formatnumbervalidationaccountibanbank
Problem
I wrote a JavaScript function for validating bank account numbers in IBAN format for my own joy. Could you have a look if you don't see any obvious mistakes or places for improvement?
```
function isIbanValid(value) {
var lengthMap = getLengthMap();
//cleanup
value = value.toString().toUpperCase().replace(/\s/g, '').replace(/[-]/g, '');
//check if alphanumeric
if (!/^[a-zA-Z0-9]+$/.test(value)) return false;
//extract countrycode
var countryCode = value.substring(0, 2);
//check if letter
if (!/([a-z]+[\s\-]?)*/i.test(countryCode)) return false;
//check string length
if (value.length != lengthMap[countryCode]) return false;
value = value.concat(value.substring(0, 4)).substring(4);
value = value.replace(countryCode, countryCodeToStringValue(countryCode));
return modulo(value, 97) == 1;
function countryCodeToStringValue(countryCode) {
return "".concat(((countryCode.charCodeAt(0)) - 55).toString() + (countryCode.charCodeAt(1) - 55).toString());
}
function modulo(divident, divisor) {
var quantization = 12;
while (quantization < divident.length) {
var part = divident.substring(0, quantization);
divident = (part % divisor) + divident.substring(quantization);
}
return divident % divisor;
}
function getLengthMap() {
var lengthMap = {};
lengthMap["AD"] = 24;
lengthMap["AT"] = 20;
lengthMap["AZ"] = 28;
lengthMap["BH"] = 22;
lengthMap["BE"] = 16;
lengthMap["BA"] = 20;
lengthMap["BR"] = 29;
lengthMap["BG"] = 22;
lengthMap["CR"] = 21;
lengthMap["HR"] = 21;
lengthMap["CY"] = 28;
lengthMap["CZ"] = 24;
lengthMap["DK"] = 18;
lengthMap["DO"] = 28;
lengthMap["EE"] = 20;
lengthMap["FO"] = 18;
lengthMap["FI"] = 18;
lengthMap["FR"] = 27;
lengthMap["DE"] = 22;
lengthMap["GR"] = 2
```
function isIbanValid(value) {
var lengthMap = getLengthMap();
//cleanup
value = value.toString().toUpperCase().replace(/\s/g, '').replace(/[-]/g, '');
//check if alphanumeric
if (!/^[a-zA-Z0-9]+$/.test(value)) return false;
//extract countrycode
var countryCode = value.substring(0, 2);
//check if letter
if (!/([a-z]+[\s\-]?)*/i.test(countryCode)) return false;
//check string length
if (value.length != lengthMap[countryCode]) return false;
value = value.concat(value.substring(0, 4)).substring(4);
value = value.replace(countryCode, countryCodeToStringValue(countryCode));
return modulo(value, 97) == 1;
function countryCodeToStringValue(countryCode) {
return "".concat(((countryCode.charCodeAt(0)) - 55).toString() + (countryCode.charCodeAt(1) - 55).toString());
}
function modulo(divident, divisor) {
var quantization = 12;
while (quantization < divident.length) {
var part = divident.substring(0, quantization);
divident = (part % divisor) + divident.substring(quantization);
}
return divident % divisor;
}
function getLengthMap() {
var lengthMap = {};
lengthMap["AD"] = 24;
lengthMap["AT"] = 20;
lengthMap["AZ"] = 28;
lengthMap["BH"] = 22;
lengthMap["BE"] = 16;
lengthMap["BA"] = 20;
lengthMap["BR"] = 29;
lengthMap["BG"] = 22;
lengthMap["CR"] = 21;
lengthMap["HR"] = 21;
lengthMap["CY"] = 28;
lengthMap["CZ"] = 24;
lengthMap["DK"] = 18;
lengthMap["DO"] = 28;
lengthMap["EE"] = 20;
lengthMap["FO"] = 18;
lengthMap["FI"] = 18;
lengthMap["FR"] = 27;
lengthMap["DE"] = 22;
lengthMap["GR"] = 2
Solution
-
I'm pretty sure this will fail for some IBANs. You convert the country code to digits, but I don't see you doing the same for any other letters in the IBAN - and there might well be others. The example Wikipedia gives is
-
Not sure why
-
The regexes have some redundancy.This one,
-
And while you're using regexes, you might as well use that to pick the string apart, rather than
-
Your
In all, I think you can do a lot more with fewer regexes, which would help streamline the code.
To sanitize the input:
To "parse" the IBAN:
if
Alternatively, you can check and rearrange in one go:
if
Finally, to convert letters to numbers:
Here's my take
I'm pretty sure this will fail for some IBANs. You convert the country code to digits, but I don't see you doing the same for any other letters in the IBAN - and there might well be others. The example Wikipedia gives is
GB82WEST12345698765432. The "WEST" should be converted to numbers as well (same procedure as the country code; 'A' = 10, B = 11, etc.).-
Not sure why
getLengthMap is a function - it's basically a constant value, and might as well be declared as a simple variable.-
The regexes have some redundancy.This one,
/^[a-zA-Z0-9]+$/, checks for lowercase letters, though the line before has already called toUpperCase(). And the next regex accepts trailing whitespace and dashes, though those have been removed.-
And while you're using regexes, you might as well use that to pick the string apart, rather than
substring plus magic numbers.-
Your
modulo function seems fine, though there's little reason to have it accept a divisor argument. Since it's an internal helper function specifically for IBAN, it'll always be mod 97. So in this case, I might actually prefer that that was hard-coded (and the function was named mod97 or something equally explanatory).In all, I think you can do a lot more with fewer regexes, which would help streamline the code.
To sanitize the input:
var iban = value.toUpperCase().replace(/[^A-Z0-9]/g, '');To "parse" the IBAN:
var match = iban.match(/^([A-Z]{2})(\d{2})(.+)$/);if
match is null the code's automatically invalid. Otherwise, match[1] will be the 2-letter country code, match[2] will be the check digits, and match[3] will be the rest of the IBAN. Provided there's a match it should then rearranged as match[3] + match[1] + match[2]Alternatively, you can check and rearrange in one go:
var code = iban.replace(/^([A-Z]{2})(\d{2})(.+)$/, "$3$1$2");if
code === iban, nothing's been rearranged, which means there wasn't a match, and the code's invalid. You'd still have to extract the country code, though, so not much is won by doing this.Finally, to convert letters to numbers:
var digits = iban.replace(/[A-Z]/g, function (letter) {
return String(letter.charCodeAt(0) - 55);
});Here's my take
var validIBAN = (function () { // use an IIFE
// A "constant" lookup table of IBAN lengths per country
// (the funky formatting is just to make it fit better in the answer here on CR)
var CODE_LENGTHS = {
AD: 24, AE: 23, AT: 20, AZ: 28, BA: 20, BE: 16, BG: 22, BH: 22, BR: 29,
CH: 21, CR: 21, CY: 28, CZ: 24, DE: 22, DK: 18, DO: 28, EE: 20, ES: 24,
FI: 18, FO: 18, FR: 27, GB: 22, GI: 23, GL: 18, GR: 27, GT: 28, HR: 21,
HU: 28, IE: 22, IL: 23, IS: 26, IT: 27, JO: 30, KW: 30, KZ: 20, LB: 28,
LI: 21, LT: 20, LU: 20, LV: 21, MC: 27, MD: 24, ME: 22, MK: 19, MR: 27,
MT: 31, MU: 30, NL: 18, NO: 15, PK: 24, PL: 28, PS: 29, PT: 25, QA: 29,
RO: 24, RS: 22, SA: 24, SE: 24, SI: 19, SK: 24, SM: 27, TN: 24, TR: 26
};
// piece-wise mod97 using 9 digit "chunks", as per Wikipedia's example:
// http://en.wikipedia.org/wiki/International_Bank_Account_Number#Modulo_operation_on_IBAN
function mod97(string) {
var checksum = string.slice(0, 2),
fragment;
for(var offset = 2 ; offset < string.length ; offset += 7) {
fragment = String(checksum) + string.substring(offset, offset + 7);
checksum = parseInt(fragment, 10) % 97;
}
return checksum;
}
// return a function that does the actual work
return function (input) {
var iban = String(input).toUpperCase().replace(/[^A-Z0-9]/g, ''), // keep only alphanumeric characters
code = iban.match(/^([A-Z]{2})(\d{2})([A-Z\d]+)$/), // match and capture (1) the country code, (2) the check digits, and (3) the rest
digits;
// check syntax and length
if(!code || iban.length !== CODE_LENGTHS[code[1]]) {
return false;
}
// rearrange country code and check digits, and convert chars to ints
digits = (code[3] + code[1] + code[2]).replace(/[A-Z]/g, function (letter) {
return letter.charCodeAt(0) - 55;
});
// final check
return mod97(digits) === 1;
};
}());Code Snippets
var iban = value.toUpperCase().replace(/[^A-Z0-9]/g, '');var match = iban.match(/^([A-Z]{2})(\d{2})(.+)$/);var code = iban.replace(/^([A-Z]{2})(\d{2})(.+)$/, "$3$1$2");var digits = iban.replace(/[A-Z]/g, function (letter) {
return String(letter.charCodeAt(0) - 55);
});var validIBAN = (function () { // use an IIFE
// A "constant" lookup table of IBAN lengths per country
// (the funky formatting is just to make it fit better in the answer here on CR)
var CODE_LENGTHS = {
AD: 24, AE: 23, AT: 20, AZ: 28, BA: 20, BE: 16, BG: 22, BH: 22, BR: 29,
CH: 21, CR: 21, CY: 28, CZ: 24, DE: 22, DK: 18, DO: 28, EE: 20, ES: 24,
FI: 18, FO: 18, FR: 27, GB: 22, GI: 23, GL: 18, GR: 27, GT: 28, HR: 21,
HU: 28, IE: 22, IL: 23, IS: 26, IT: 27, JO: 30, KW: 30, KZ: 20, LB: 28,
LI: 21, LT: 20, LU: 20, LV: 21, MC: 27, MD: 24, ME: 22, MK: 19, MR: 27,
MT: 31, MU: 30, NL: 18, NO: 15, PK: 24, PL: 28, PS: 29, PT: 25, QA: 29,
RO: 24, RS: 22, SA: 24, SE: 24, SI: 19, SK: 24, SM: 27, TN: 24, TR: 26
};
// piece-wise mod97 using 9 digit "chunks", as per Wikipedia's example:
// http://en.wikipedia.org/wiki/International_Bank_Account_Number#Modulo_operation_on_IBAN
function mod97(string) {
var checksum = string.slice(0, 2),
fragment;
for(var offset = 2 ; offset < string.length ; offset += 7) {
fragment = String(checksum) + string.substring(offset, offset + 7);
checksum = parseInt(fragment, 10) % 97;
}
return checksum;
}
// return a function that does the actual work
return function (input) {
var iban = String(input).toUpperCase().replace(/[^A-Z0-9]/g, ''), // keep only alphanumeric characters
code = iban.match(/^([A-Z]{2})(\d{2})([A-Z\d]+)$/), // match and capture (1) the country code, (2) the check digits, and (3) the rest
digits;
// check syntax and length
if(!code || iban.length !== CODE_LENGTHS[code[1]]) {
return false;
}
// rearrange country code and check digits, and convert chars to ints
digits = (code[3] + code[1] + code[2]).replace(/[A-Z]/g, function (letter) {
return letter.charCodeAt(0) - 55;
});
// final check
return mod97(digits) === 1;
};
}());Context
StackExchange Code Review Q#58174, answer score: 8
Revisions (0)
No revisions yet.