Skip to content

Commit

Permalink
feat(isIBAN): new validator (#1243)
Browse files Browse the repository at this point in the history
* feat: implement isIBAN(str) validator

* re-arrange country codes alphabitcally for better readability
  • Loading branch information
hamzahejja authored Feb 1, 2020
1 parent 1c9efc5 commit b331b5a
Show file tree
Hide file tree
Showing 8 changed files with 446 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ Validator | Description
**isBase32(str)** | check if a string is base32 encoded.
**isBase64(str)** | check if a string is base64 encoded.
**isBefore(str [, date])** | check if the string is a date that's before the specified date.
**isIBAN(str)** | check if a string is a IBAN (International Bank Account Number).
**isBIC(str)** | check if a string is a BIC (Bank Identification Code) or SWIFT code.
**isBoolean(str)** | check if a string is a boolean.
**isByteLength(str [, options])** | check if the string's length (in UTF-8 bytes) falls in a range.<br/><br/>`options` is an object which defaults to `{min:0, max: undefined}`.
Expand Down
3 changes: 3 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ var _isHexColor = _interopRequireDefault(require("./lib/isHexColor"));

var _isISRC = _interopRequireDefault(require("./lib/isISRC"));

var _isIBAN = _interopRequireDefault(require("./lib/isIBAN"));

var _isBIC = _interopRequireDefault(require("./lib/isBIC"));

var _isMD = _interopRequireDefault(require("./lib/isMD5"));
Expand Down Expand Up @@ -182,6 +184,7 @@ var validator = {
isIPRange: _isIPRange.default,
isFQDN: _isFQDN.default,
isBoolean: _isBoolean.default,
isIBAN: _isIBAN.default,
isBIC: _isBIC.default,
isAlpha: _isAlpha.default,
isAlphaLocales: _isAlpha.locales,
Expand Down
145 changes: 145 additions & 0 deletions lib/isIBAN.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
"use strict";

Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = isIBAN;

var _assertString = _interopRequireDefault(require("./util/assertString"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

/**
* List of country codes with
* corresponding IBAN regular expression
* Reference: https://en.wikipedia.org/wiki/International_Bank_Account_Number
*/
var ibanRegexThroughCountryCode = {
AD: /^(AD[0-9]{2})\d{8}[A-Z0-9]{12}$/,
AE: /^(AE[0-9]{2})\d{3}\d{16}$/,
AL: /^(AL[0-9]{2})\d{8}[A-Z0-9]{16}$/,
AT: /^(AT[0-9]{2})\d{16}$/,
AZ: /^(AZ[0-9]{2})[A-Z0-9]{4}\d{20}$/,
BA: /^(BA[0-9]{2})\d{16}$/,
BE: /^(BE[0-9]{2})\d{12}$/,
BG: /^(BG[0-9]{2})[A-Z]{4}\d{6}[A-Z0-9]{8}$/,
BH: /^(BH[0-9]{2})[A-Z]{4}[A-Z0-9]{14}$/,
BR: /^(BR[0-9]{2})\d{23}[A-Z]{1}[A-Z0-9]{1}$/,
BY: /^(BY[0-9]{2})[A-Z0-9]{4}\d{20}$/,
CH: /^(CH[0-9]{2})\d{5}[A-Z0-9]{12}$/,
CR: /^(CR[0-9]{2})\d{18}$/,
CY: /^(CY[0-9]{2})\d{8}[A-Z0-9]{16}$/,
CZ: /^(CZ[0-9]{2})\d{20}$/,
DE: /^(DE[0-9]{2})\d{18}$/,
DK: /^(DK[0-9]{2})\d{14}$/,
DO: /^(DO[0-9]{2})[A-Z]{4}\d{20}$/,
EE: /^(EE[0-9]{2})\d{16}$/,
ES: /^(ES[0-9]{2})\d{20}$/,
FI: /^(FI[0-9]{2})\d{14}$/,
FO: /^(FO[0-9]{2})\d{14}$/,
FR: /^(FR[0-9]{2})\d{10}[A-Z0-9]{11}\d{2}$/,
GB: /^(GB[0-9]{2})[A-Z]{4}\d{14}$/,
GE: /^(GE[0-9]{2})[A-Z0-9]{2}\d{16}$/,
GI: /^(GI[0-9]{2})[A-Z]{4}[A-Z0-9]{15}$/,
GL: /^(GL[0-9]{2})\d{14}$/,
GR: /^(GR[0-9]{2})\d{7}[A-Z0-9]{16}$/,
GT: /^(GT[0-9]{2})[A-Z0-9]{4}[A-Z0-9]{20}$/,
HR: /^(HR[0-9]{2})\d{17}$/,
HU: /^(HU[0-9]{2})\d{24}$/,
IE: /^(IE[0-9]{2})[A-Z0-9]{4}\d{14}$/,
IL: /^(IL[0-9]{2})\d{19}$/,
IQ: /^(IQ[0-9]{2})[A-Z]{4}\d{15}$/,
IS: /^(IS[0-9]{2})\d{22}$/,
IT: /^(IT[0-9]{2})[A-Z]{1}\d{10}[A-Z0-9]{12}$/,
JO: /^(JO[0-9]{2})[A-Z]{4}\d{22}$/,
KW: /^(KW[0-9]{2})[A-Z]{4}[A-Z0-9]{22}$/,
KZ: /^(KZ[0-9]{2})\d{3}[A-Z0-9]{13}$/,
LB: /^(LB[0-9]{2})\d{4}[A-Z0-9]{20}$/,
LC: /^(LC[0-9]{2})[A-Z]{4}[A-Z0-9]{24}$/,
LI: /^(LI[0-9]{2})\d{5}[A-Z0-9]{12}$/,
LT: /^(LT[0-9]{2})\d{16}$/,
LU: /^(LU[0-9]{2})\d{3}[A-Z0-9]{13}$/,
LV: /^(LV[0-9]{2})[A-Z]{4}[A-Z0-9]{13}$/,
MC: /^(MC[0-9]{2})\d{10}[A-Z0-9]{11}\d{2}$/,
MD: /^(MD[0-9]{2})[A-Z0-9]{20}$/,
ME: /^(ME[0-9]{2})\d{18}$/,
MK: /^(MK[0-9]{2})\d{3}[A-Z0-9]{10}\d{2}$/,
MR: /^(MR[0-9]{2})\d{23}$/,
MT: /^(MT[0-9]{2})[A-Z]{4}\d{5}[A-Z0-9]{18}$/,
MU: /^(MU[0-9]{2})[A-Z]{4}\d{19}[A-Z]{3}$/,
NL: /^(NL[0-9]{2})[A-Z]{4}\d{10}$/,
NO: /^(NO[0-9]{2})\d{11}$/,
PK: /^(PK[0-9]{2})[A-Z0-9]{4}\d{16}$/,
PL: /^(PL[0-9]{2})\d{24}$/,
PS: /^(PS[0-9]{2})[A-Z0-9]{4}\d{21}$/,
PT: /^(PT[0-9]{2})\d{21}$/,
QA: /^(QA[0-9]{2})[A-Z]{4}[A-Z0-9]{21}$/,
RO: /^(RO[0-9]{2})[A-Z]{4}[A-Z0-9]{16}$/,
RS: /^(RS[0-9]{2})\d{18}$/,
SA: /^(SA[0-9]{2})\d{2}[A-Z0-9]{18}$/,
SC: /^(SC[0-9]{2})[A-Z]{4}\d{20}[A-Z]{3}$/,
SE: /^(SE[0-9]{2})\d{20}$/,
SI: /^(SI[0-9]{2})\d{15}$/,
SK: /^(SK[0-9]{2})\d{20}$/,
SM: /^(SM[0-9]{2})[A-Z]{1}\d{10}[A-Z0-9]{12}$/,
TL: /^(TL[0-9]{2})\d{19}$/,
TN: /^(TN[0-9]{2})\d{20}$/,
TR: /^(TR[0-9]{2})\d{5}[A-Z0-9]{17}$/,
UA: /^(UA[0-9]{2})\d{6}[A-Z0-9]{19}$/,
VA: /^(VA[0-9]{2})\d{18}$/,
VG: /^(VG[0-9]{2})[A-Z0-9]{4}\d{16}$/,
XK: /^(XK[0-9]{2})\d{16}$/
};
/**
* Check whether string has correct universal IBAN format
* The IBAN consists of up to 34 alphanumeric characters, as follows:
* Country Code using ISO 3166-1 alpha-2, two letters
* check digits, two digits and
* Basic Bank Account Number (BBAN), up to 30 alphanumeric characters.
* NOTE: Permitted IBAN characters are: digits [0-9] and the 26 latin alphabetic [A-Z]
*
* @param {string} str - string under validation
* @return {boolean}
*/

function hasValidIbanFormat(str) {
// Strip white spaces and hyphens, keep only digits and A-Z latin alphabetic
var strippedStr = str.replace(/[^A-Z0-9]+/gi, '').toUpperCase();
var isoCountryCode = strippedStr.slice(0, 2).toUpperCase();
return isoCountryCode in ibanRegexThroughCountryCode && ibanRegexThroughCountryCode[isoCountryCode].test(strippedStr);
}
/**
* Check whether string has valid IBAN Checksum
* by performing basic mod-97 operation and
* the remainder should equal 1
* -- Start by rearranging the IBAN by moving the four initial characters to the end of the string
* -- Replace each letter in the string with two digits, A -> 10, B = 11, Z = 35
* -- Interpret the string as a decimal integer and
* -- compute the remainder on division by 97 (mod 97)
* Reference: https://en.wikipedia.org/wiki/International_Bank_Account_Number
*
* @param {string} str
* @return {boolean}
*/


function hasValidIbanChecksum(str) {
var strippedStr = str.replace(/[^A-Z0-9]+/gi, '').toUpperCase(); // Keep only digits and A-Z latin alphabetic

var rearranged = strippedStr.slice(4) + strippedStr.slice(0, 4);
var alphaCapsReplacedWithDigits = rearranged.replace(/[A-Z]/g, function (char) {
return char.charCodeAt(0) - 55;
});
var remainder = alphaCapsReplacedWithDigits.match(/\d{1,7}/g).reduce(function (acc, value) {
return Number(acc + value) % 97;
}, '');
return remainder === 1;
}

function isIBAN(str) {
(0, _assertString.default)(str);
return hasValidIbanFormat(str) && hasValidIbanChecksum(str);
}

module.exports = exports.default;
module.exports.default = exports.default;
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import isHexColor from './lib/isHexColor';

import isISRC from './lib/isISRC';

import isIBAN from './lib/isIBAN';
import isBIC from './lib/isBIC';

import isMD5 from './lib/isMD5';
Expand Down Expand Up @@ -118,6 +119,7 @@ const validator = {
isIPRange,
isFQDN,
isBoolean,
isIBAN,
isBIC,
isAlpha,
isAlphaLocales,
Expand Down
133 changes: 133 additions & 0 deletions src/lib/isIBAN.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import assertString from './util/assertString';

/**
* List of country codes with
* corresponding IBAN regular expression
* Reference: https://en.wikipedia.org/wiki/International_Bank_Account_Number
*/
const ibanRegexThroughCountryCode = {
AD: /^(AD[0-9]{2})\d{8}[A-Z0-9]{12}$/,
AE: /^(AE[0-9]{2})\d{3}\d{16}$/,
AL: /^(AL[0-9]{2})\d{8}[A-Z0-9]{16}$/,
AT: /^(AT[0-9]{2})\d{16}$/,
AZ: /^(AZ[0-9]{2})[A-Z0-9]{4}\d{20}$/,
BA: /^(BA[0-9]{2})\d{16}$/,
BE: /^(BE[0-9]{2})\d{12}$/,
BG: /^(BG[0-9]{2})[A-Z]{4}\d{6}[A-Z0-9]{8}$/,
BH: /^(BH[0-9]{2})[A-Z]{4}[A-Z0-9]{14}$/,
BR: /^(BR[0-9]{2})\d{23}[A-Z]{1}[A-Z0-9]{1}$/,
BY: /^(BY[0-9]{2})[A-Z0-9]{4}\d{20}$/,
CH: /^(CH[0-9]{2})\d{5}[A-Z0-9]{12}$/,
CR: /^(CR[0-9]{2})\d{18}$/,
CY: /^(CY[0-9]{2})\d{8}[A-Z0-9]{16}$/,
CZ: /^(CZ[0-9]{2})\d{20}$/,
DE: /^(DE[0-9]{2})\d{18}$/,
DK: /^(DK[0-9]{2})\d{14}$/,
DO: /^(DO[0-9]{2})[A-Z]{4}\d{20}$/,
EE: /^(EE[0-9]{2})\d{16}$/,
ES: /^(ES[0-9]{2})\d{20}$/,
FI: /^(FI[0-9]{2})\d{14}$/,
FO: /^(FO[0-9]{2})\d{14}$/,
FR: /^(FR[0-9]{2})\d{10}[A-Z0-9]{11}\d{2}$/,
GB: /^(GB[0-9]{2})[A-Z]{4}\d{14}$/,
GE: /^(GE[0-9]{2})[A-Z0-9]{2}\d{16}$/,
GI: /^(GI[0-9]{2})[A-Z]{4}[A-Z0-9]{15}$/,
GL: /^(GL[0-9]{2})\d{14}$/,
GR: /^(GR[0-9]{2})\d{7}[A-Z0-9]{16}$/,
GT: /^(GT[0-9]{2})[A-Z0-9]{4}[A-Z0-9]{20}$/,
HR: /^(HR[0-9]{2})\d{17}$/,
HU: /^(HU[0-9]{2})\d{24}$/,
IE: /^(IE[0-9]{2})[A-Z0-9]{4}\d{14}$/,
IL: /^(IL[0-9]{2})\d{19}$/,
IQ: /^(IQ[0-9]{2})[A-Z]{4}\d{15}$/,
IS: /^(IS[0-9]{2})\d{22}$/,
IT: /^(IT[0-9]{2})[A-Z]{1}\d{10}[A-Z0-9]{12}$/,
JO: /^(JO[0-9]{2})[A-Z]{4}\d{22}$/,
KW: /^(KW[0-9]{2})[A-Z]{4}[A-Z0-9]{22}$/,
KZ: /^(KZ[0-9]{2})\d{3}[A-Z0-9]{13}$/,
LB: /^(LB[0-9]{2})\d{4}[A-Z0-9]{20}$/,
LC: /^(LC[0-9]{2})[A-Z]{4}[A-Z0-9]{24}$/,
LI: /^(LI[0-9]{2})\d{5}[A-Z0-9]{12}$/,
LT: /^(LT[0-9]{2})\d{16}$/,
LU: /^(LU[0-9]{2})\d{3}[A-Z0-9]{13}$/,
LV: /^(LV[0-9]{2})[A-Z]{4}[A-Z0-9]{13}$/,
MC: /^(MC[0-9]{2})\d{10}[A-Z0-9]{11}\d{2}$/,
MD: /^(MD[0-9]{2})[A-Z0-9]{20}$/,
ME: /^(ME[0-9]{2})\d{18}$/,
MK: /^(MK[0-9]{2})\d{3}[A-Z0-9]{10}\d{2}$/,
MR: /^(MR[0-9]{2})\d{23}$/,
MT: /^(MT[0-9]{2})[A-Z]{4}\d{5}[A-Z0-9]{18}$/,
MU: /^(MU[0-9]{2})[A-Z]{4}\d{19}[A-Z]{3}$/,
NL: /^(NL[0-9]{2})[A-Z]{4}\d{10}$/,
NO: /^(NO[0-9]{2})\d{11}$/,
PK: /^(PK[0-9]{2})[A-Z0-9]{4}\d{16}$/,
PL: /^(PL[0-9]{2})\d{24}$/,
PS: /^(PS[0-9]{2})[A-Z0-9]{4}\d{21}$/,
PT: /^(PT[0-9]{2})\d{21}$/,
QA: /^(QA[0-9]{2})[A-Z]{4}[A-Z0-9]{21}$/,
RO: /^(RO[0-9]{2})[A-Z]{4}[A-Z0-9]{16}$/,
RS: /^(RS[0-9]{2})\d{18}$/,
SA: /^(SA[0-9]{2})\d{2}[A-Z0-9]{18}$/,
SC: /^(SC[0-9]{2})[A-Z]{4}\d{20}[A-Z]{3}$/,
SE: /^(SE[0-9]{2})\d{20}$/,
SI: /^(SI[0-9]{2})\d{15}$/,
SK: /^(SK[0-9]{2})\d{20}$/,
SM: /^(SM[0-9]{2})[A-Z]{1}\d{10}[A-Z0-9]{12}$/,
TL: /^(TL[0-9]{2})\d{19}$/,
TN: /^(TN[0-9]{2})\d{20}$/,
TR: /^(TR[0-9]{2})\d{5}[A-Z0-9]{17}$/,
UA: /^(UA[0-9]{2})\d{6}[A-Z0-9]{19}$/,
VA: /^(VA[0-9]{2})\d{18}$/,
VG: /^(VG[0-9]{2})[A-Z0-9]{4}\d{16}$/,
XK: /^(XK[0-9]{2})\d{16}$/,
};

/**
* Check whether string has correct universal IBAN format
* The IBAN consists of up to 34 alphanumeric characters, as follows:
* Country Code using ISO 3166-1 alpha-2, two letters
* check digits, two digits and
* Basic Bank Account Number (BBAN), up to 30 alphanumeric characters.
* NOTE: Permitted IBAN characters are: digits [0-9] and the 26 latin alphabetic [A-Z]
*
* @param {string} str - string under validation
* @return {boolean}
*/
function hasValidIbanFormat(str) {
// Strip white spaces and hyphens, keep only digits and A-Z latin alphabetic
const strippedStr = str.replace(/[^A-Z0-9]+/gi, '').toUpperCase();
const isoCountryCode = strippedStr.slice(0, 2).toUpperCase();

return (isoCountryCode in ibanRegexThroughCountryCode) &&
ibanRegexThroughCountryCode[isoCountryCode].test(strippedStr);
}

/**
* Check whether string has valid IBAN Checksum
* by performing basic mod-97 operation and
* the remainder should equal 1
* -- Start by rearranging the IBAN by moving the four initial characters to the end of the string
* -- Replace each letter in the string with two digits, A -> 10, B = 11, Z = 35
* -- Interpret the string as a decimal integer and
* -- compute the remainder on division by 97 (mod 97)
* Reference: https://en.wikipedia.org/wiki/International_Bank_Account_Number
*
* @param {string} str
* @return {boolean}
*/
function hasValidIbanChecksum(str) {
const strippedStr = str.replace(/[^A-Z0-9]+/gi, '').toUpperCase(); // Keep only digits and A-Z latin alphabetic
const rearranged = strippedStr.slice(4) + strippedStr.slice(0, 4);
const alphaCapsReplacedWithDigits = rearranged.replace(/[A-Z]/g, char => char.charCodeAt(0) - 55);

const remainder = alphaCapsReplacedWithDigits.match(/\d{1,7}/g)
.reduce((acc, value) => Number(acc + value) % 97, '');

return remainder === 1;
}

export default function isIBAN(str) {
assertString(str);

return hasValidIbanFormat(str) && hasValidIbanChecksum(str);
}
27 changes: 27 additions & 0 deletions test/validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -3162,6 +3162,33 @@ describe('Validators', () => {
});
});

it('should validate IBAN', () => {
test({
validator: 'isIBAN',
valid: [
'BE71 0961 2345 6769',
'FR76 3000 6000 0112 3456 7890 189',
'DE91 1000 0000 0123 4567 89',
'GR96 0810 0010 0000 0123 4567 890',
'RO09 BCYP 0000 0012 3456 7890',
'SA44 2000 0001 2345 6789 1234',
'ES79 2100 0813 6101 2345 6789',
'CH56 0483 5012 3456 7800 9',
'GB98 MIDL 0700 9312 3456 78',
'IL170108000000012612345',
'IT60X0542811101000000123456',
'JO71CBJO0000000000001234567890',
'TR320010009999901234567890',
'BR1500000000000010932840814P2',
'LB92000700000000123123456123',
],
invalid: [
'XX22YYY1234567890123',
'FR14 2004 1010 0505 0001 3',
],
});
});

it('should validate BIC codes', () => {
test({
validator: 'isBIC',
Expand Down
Loading

0 comments on commit b331b5a

Please sign in to comment.