Comparing version
{ | ||
"name": "iban-ts", | ||
"version": "0.6.0", | ||
"version": "0.7.0", | ||
"description": "A TypeScript library for validating, formatting, and converting IBAN (International Bank Account Number) and BBAN (Basic Bank Account Number), offering comprehensive support for international banking data standards.", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
113
src/index.ts
@@ -7,17 +7,32 @@ import { Specification } from './Specification'; | ||
/** | ||
* Converts an IBAN to electronic format by removing non-alphanumeric characters | ||
* Removes non-alphanumeric characters from the string and converts it to uppercase. | ||
* | ||
* @param iban - The IBAN string to format. | ||
* @returns The formatted IBAN string. | ||
*/ | ||
const electronicFormat = (iban: string): string => iban.replace(NON_ALPHANUM, '').toUpperCase(); | ||
// Improve type safety with a dedicated type for the countries map | ||
type CountryMap = Record<string, Specification>; | ||
const countries: CountryMap = {}; | ||
/** | ||
* Type guard for checking if a value is a string. | ||
* | ||
* @param value - The value to check. | ||
* @returns True if the value is a string, false otherwise. | ||
*/ | ||
const isString = (value: any): value is string => typeof value === 'string' || value instanceof String; | ||
/** | ||
* Map of country codes to their respective IBAN specifications. | ||
*/ | ||
const countries: Record<string, Specification> = {}; | ||
/** | ||
* Adds a new IBAN specification for a country. | ||
* | ||
* @param IBAN - The IBAN specification to add. | ||
*/ | ||
const addSpecification = (spec: Specification): void => { | ||
countries[spec.countryCode] = spec; | ||
const addSpecification = (IBAN: Specification): void => { | ||
countries[IBAN.countryCode] = IBAN; | ||
}; | ||
// Add all the country specifications | ||
addSpecification(new Specification('AD', 24, 'F04F04A12', 'AD1200012030200359100100')); | ||
@@ -57,5 +72,5 @@ addSpecification(new Specification('AE', 23, 'F03F16', 'AE070331234567890123456')); | ||
addSpecification(new Specification('IL', 23, 'F03F03F13', 'IL620108000000099999999')); | ||
addSpecification(new Specification('IQ', 23, 'U04F03A12', 'IQ98NBIQ850123456789012')); | ||
addSpecification(new Specification('IS', 26, 'F04F02F06F10', 'IS140159260076545510730339')); | ||
addSpecification(new Specification('IT', 27, 'U01F05F05A12', 'IT60X0542811101000000123456')); | ||
addSpecification(new Specification('IQ', 23, 'U04F03A12', 'IQ98NBIQ850123456789012')); | ||
addSpecification(new Specification('JO', 30, 'A04F22', 'JO15AAAA1234567890123456789012')); | ||
@@ -102,3 +117,3 @@ addSpecification(new Specification('KW', 30, 'U04A22', 'KW81CBKU0000000000001234560101')); | ||
// Non-official IBAN countries (using IBAN-like formats) | ||
// Non-official IBAN countries | ||
addSpecification(new Specification('AO', 25, 'F21', 'AO69123456789012345678901')); | ||
@@ -118,3 +133,3 @@ addSpecification(new Specification('BF', 27, 'F23', 'BF2312345678901234567890123')); | ||
// French territories (same structure as France) | ||
// French territories | ||
addSpecification(new Specification('GF', 27, 'F05F05A11F02', 'GF121234512345123456789AB13')); | ||
@@ -134,18 +149,14 @@ addSpecification(new Specification('GP', 27, 'F05F05A11F02', 'GP791234512345123456789AB13')); | ||
/** | ||
* Type guard for checking if a value is a string. | ||
*/ | ||
const isString = (value: unknown): value is string => typeof value === 'string' || value instanceof String; | ||
/** | ||
* Validates an IBAN number. | ||
* | ||
* @param iban - The IBAN to validate. | ||
* @returns True if the IBAN is valid, false otherwise. | ||
*/ | ||
const isValid = (iban: unknown): boolean => { | ||
const isValid = (iban: string): boolean => { | ||
if (!isString(iban)) { | ||
return false; | ||
} | ||
const formatted = electronicFormat(iban); | ||
const countryCode = formatted.slice(0, 2); | ||
const countryStructure = countries[countryCode]; | ||
return !!countryStructure && countryStructure.isValid(formatted); | ||
iban = electronicFormat(iban); | ||
const countryStructure = countries[iban.slice(0, 2)]; | ||
return !!countryStructure && countryStructure.isValid(iban); | ||
}; | ||
@@ -155,29 +166,24 @@ | ||
* Converts an IBAN to a BBAN. | ||
* | ||
* @param iban - The IBAN to convert. | ||
* @param separator - The separator to use between BBAN blocks, defaults to ' '. | ||
* @returns The BBAN or undefined if conversion fails. | ||
* @throws {Error} If the country code is invalid. | ||
*/ | ||
const toBBAN = (iban: string, separator = ' '): string => { | ||
const formatted = electronicFormat(iban); | ||
const countryCode = formatted.slice(0, 2); | ||
const countryStructure = countries[countryCode]; | ||
const toBBAN = (iban: string, separator: string = ' '): string | undefined => { | ||
iban = electronicFormat(iban); | ||
const countryStructure = countries[iban.slice(0, 2)]; | ||
if (!countryStructure) { | ||
throw new Error(`No country with code ${countryCode}`); | ||
throw new Error('No country with code ' + iban.slice(0, 2)); | ||
} | ||
const bban = countryStructure.toBBAN(formatted, separator); | ||
if (!bban) { | ||
throw new Error('Failed to convert IBAN to BBAN'); | ||
} | ||
return bban; | ||
return countryStructure.toBBAN(iban, separator); | ||
}; | ||
/** | ||
* Convert the passed BBAN to an IBAN for this country specification. | ||
* Please note that <i>"generation of the IBAN shall be the exclusive responsibility of the bank/branch servicing the account"</i>. | ||
* This method implements the preferred algorithm described in http://en.wikipedia.org/wiki/International_Bank_Account_Number#Generating_IBAN_check_digits | ||
* | ||
* @param countryCode the country of the BBAN | ||
* @param bban the BBAN to convert to IBAN | ||
* @returns {string} the IBAN | ||
* Converts the passed BBAN to an IBAN for this country specification. | ||
* @param countryCode - The country code of the BBAN. | ||
* @param bban - The BBAN to convert to IBAN. | ||
* @returns The corresponding IBAN. | ||
* @throws {Error} If the BBAN is invalid or the country code is invalid. | ||
*/ | ||
@@ -194,6 +200,7 @@ const fromBBAN = (countryCode: string, bban: string): string => { | ||
/** | ||
* Check the validity of the passed BBAN. | ||
* Checks the validity of the passed BBAN. | ||
* | ||
* @param countryCode the country of the BBAN | ||
* @param bban the BBAN to check the validity of | ||
* @param countryCode - The country code of the BBAN. | ||
* @param bban - The BBAN to check the validity of. | ||
* @returns True if the BBAN is valid, false otherwise. | ||
*/ | ||
@@ -205,19 +212,15 @@ const isValidBBAN = (countryCode: string, bban: string): boolean => { | ||
const countryStructure = countries[countryCode]; | ||
return countryStructure?.isValidBBAN(electronicFormat(bban)); | ||
return !!countryStructure && countryStructure.isValidBBAN(electronicFormat(bban)); | ||
}; | ||
/** | ||
* Prints an IBAN in a formatted string, separating groups of characters. | ||
* | ||
* @param {string} iban - The IBAN to format | ||
* @param {string} separator - The separator to use between groups (defaults to space) | ||
* @param {number} groupSize - The size of each group of characters (defaults to 4) | ||
* @returns {string} The formatted IBAN string | ||
* Formats the IBAN in print format by inserting spaces every four characters. | ||
* | ||
* @param iban - The IBAN to format. | ||
* @param separator - The separator to use between groups, defaults to ' '. | ||
* @returns The formatted IBAN string. | ||
*/ | ||
const printFormat = (iban: string, separator: string = ' ', groupSize: number = 4): string => { | ||
const regex = new RegExp(`(.{${groupSize}})(?!$)`, 'g'); | ||
return electronicFormat(iban).replace(regex, `$1${separator}`); | ||
}; | ||
const printFormat = (iban: string, separator: string = ' '): string => | ||
electronicFormat(iban).replace(EVERY_FOUR_CHARS, '$1' + separator); | ||
export { countries, electronicFormat, fromBBAN, isValid, isValidBBAN, printFormat, toBBAN }; |
@@ -1,19 +0,3 @@ | ||
const ASCII_A = 'A'.charCodeAt(0); | ||
type FormatMap = { | ||
readonly [key: string]: string; | ||
}; | ||
const FORMATS: FormatMap = { | ||
A: '0-9A-Za-z', | ||
B: '0-9A-Z', | ||
C: 'A-Za-z', | ||
F: '0-9', | ||
L: 'a-z', | ||
U: 'A-Z', | ||
W: '0-9a-z', | ||
} as const; | ||
/** | ||
* Represents a specification for validating and manipulating IBANs. | ||
* Represents a specification for validating and manipulating IBANs (International Bank Account Numbers). | ||
*/ | ||
@@ -23,2 +7,9 @@ export class Specification { | ||
/** | ||
* Creates a new instance of Specification. | ||
* @param countryCode - The country code associated with the IBAN. | ||
* @param length - The total length of the IBAN. | ||
* @param structure - The structure of the underlying BBAN (Basic Bank Account Number). | ||
* @param example - An example of a valid IBAN for this specification. | ||
*/ | ||
constructor( | ||
@@ -32,4 +23,3 @@ public readonly countryCode: string, | ||
/** | ||
* Validates an IBAN number. | ||
* | ||
* Checks if the given IBAN is valid according to this specification. | ||
* @param iban - The IBAN to validate. | ||
@@ -39,7 +29,6 @@ * @returns True if the IBAN is valid, false otherwise. | ||
isValid(iban: string): boolean { | ||
if (iban.length < 4) return false; | ||
return ( | ||
this.length === iban.length && | ||
this.countryCode === iban.slice(0, 2) && | ||
this.regex.test(iban.slice(4)) && | ||
this._regex().test(iban.slice(4)) && | ||
this.iso7064Mod97_10(this.iso13616Prepare(iban)) === 1 | ||
@@ -50,33 +39,2 @@ ); | ||
/** | ||
* Gets the cached regex for BBAN validation. | ||
*/ | ||
private get regex(): RegExp { | ||
if (!this._cachedRegex) { | ||
this._cachedRegex = this.parseStructure(this.structure); | ||
} | ||
return this._cachedRegex; | ||
} | ||
/** | ||
* Parses the BBAN structure and returns a matching regular expression. | ||
*/ | ||
private parseStructure(structure: string): RegExp { | ||
const blocks = structure.match(/.{3}/g) || []; | ||
const pattern = blocks | ||
.map((block) => { | ||
const [format, ...repeat] = block; | ||
const repeatCount = parseInt(repeat.join(''), 10); | ||
if (!(format in FORMATS)) { | ||
throw new Error(`Invalid format pattern: ${format}`); | ||
} | ||
return `([${FORMATS[format]}]{${repeatCount}})`; | ||
}) | ||
.join(''); | ||
return new RegExp(`^${pattern}$`); | ||
} | ||
/** | ||
* Converts the given IBAN to a country-specific BBAN. | ||
@@ -88,5 +46,4 @@ * @param iban - The IBAN to convert. | ||
toBBAN(iban: string, separator: string = ' '): string | undefined { | ||
const match = this.regex?.exec(iban.slice(4)); | ||
if (!match) return undefined; | ||
return match.slice(1).join(separator); | ||
const match = this._regex().exec(iban.slice(4)); | ||
return match ? match.slice(1).join(separator) : undefined; | ||
} | ||
@@ -106,3 +63,3 @@ | ||
const prepared = this.iso13616Prepare(this.countryCode + '00' + bban); | ||
const checkDigit = ('0' + (98 - this.iso7064Mod97_10(prepared))).slice(-2); | ||
const checkDigit = String(98 - this.iso7064Mod97_10(prepared)).padStart(2, '0'); | ||
@@ -118,34 +75,79 @@ return this.countryCode + checkDigit + bban; | ||
isValidBBAN(bban: string): boolean { | ||
return this.length - 4 === bban.length && this.regex.test(bban); | ||
return this.length - 4 === bban.length && this._regex().test(bban); | ||
} | ||
/** | ||
* Prepares an IBAN for mod 97 computation as specified in ISO13616. | ||
* Gets a lazily-loaded regex constructed based on the BBAN structure. | ||
* @returns The regular expression for validating the BBAN. | ||
*/ | ||
private _regex(): RegExp { | ||
if (!this._cachedRegex) { | ||
this._cachedRegex = this.parseStructure(this.structure); | ||
} | ||
return this._cachedRegex; | ||
} | ||
/** | ||
* Prepares an IBAN for MOD 97-10 computation as specified in ISO 13616. | ||
* @param iban - The IBAN to prepare. | ||
* @returns The prepared IBAN. | ||
* @returns The prepared numeric IBAN string. | ||
*/ | ||
private iso13616Prepare(iban: string): string { | ||
iban = iban.toUpperCase(); | ||
iban = iban.substr(4) + iban.substr(0, 4); | ||
const rearranged = (iban.slice(4) + iban.slice(0, 4)).toUpperCase(); | ||
return rearranged.replace(/[A-Z]/g, (char) => (char.charCodeAt(0) - 55).toString()); | ||
} | ||
return iban.replace(/[A-Z]/g, (match) => (match.charCodeAt(0) - ASCII_A + 10).toString()); | ||
/** | ||
* Parses the BBAN structure and returns a matching regular expression. | ||
* @param structure - The structure to parse. | ||
* @returns A RegExp derived from the BBAN structure. | ||
* @throws {Error} If the structure format is invalid. | ||
*/ | ||
private parseStructure(structure: string): RegExp { | ||
const formats: { [key: string]: string } = { | ||
A: '0-9A-Za-z', | ||
B: '0-9A-Z', | ||
C: 'A-Za-z', | ||
F: '0-9', | ||
L: 'a-z', | ||
U: 'A-Z', | ||
W: '0-9a-z', | ||
}; | ||
const blocks = structure.match(/.{3}/g); | ||
if (!blocks) { | ||
throw new Error(`Invalid structure format: ${structure}`); | ||
} | ||
const regexParts = blocks.map((block) => { | ||
const pattern = block.charAt(0); | ||
const repeats = parseInt(block.slice(1), 10); | ||
const format = formats[pattern]; | ||
if (!format) { | ||
throw new Error(`Invalid pattern: ${pattern}`); | ||
} | ||
return `([${format}]{${repeats}})`; | ||
}); | ||
return new RegExp(`^${regexParts.join('')}$`); | ||
} | ||
/** | ||
* Calculates the MOD 97 10 of the passed IBAN as specified in ISO7064. | ||
* @param iban - The IBAN to calculate the MOD 97 10 for. | ||
* @returns The MOD 97 10 result. | ||
* Calculates the MOD 97-10 of the passed IBAN as specified in ISO 7064. | ||
* @param iban - The IBAN to calculate the MOD 97-10 for. | ||
* @returns The MOD 97-10 result. | ||
*/ | ||
private iso7064Mod97_10(iban: string): number { | ||
let remainder = 0; | ||
let remainder = iban; | ||
let block: string; | ||
for (let i = 0; i < iban.length; i += 9) { | ||
const num = parseInt(remainder + iban.slice(i, i + 9), 10); | ||
if (isNaN(num)) { | ||
throw new Error(`Invalid numeric block: ${iban.slice(i, i + 9)}`); | ||
} | ||
remainder = num % 97; | ||
while (remainder.length > 2) { | ||
block = remainder.slice(0, 9); | ||
remainder = (parseInt(block, 10) % 97).toString() + remainder.slice(block.length); | ||
} | ||
return remainder; | ||
return parseInt(remainder, 10) % 97; | ||
} | ||
} |
161889
0.4%1192
0.93%