HiveBrain v1.2.0
Get Started
← Back to all entries
snippetjavascriptMinor

Bank account number validation in IBAN format

Submitted by: @import:stackexchange-codereview··
0
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

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