first version
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
// IBAN validation helpers (BBAN length per country + mod-97 checksum).
|
||||
// Deliberately self-contained — avoids pulling in an extra dependency.
|
||||
|
||||
const IBAN_LENGTHS: Record<string, number> = {
|
||||
AD: 24, AE: 23, AL: 28, AT: 20, AZ: 28, BA: 20, BE: 16, BG: 22, BH: 22,
|
||||
BR: 29, BY: 28, CH: 21, CR: 22, CY: 28, CZ: 24, DE: 22, DK: 18, DO: 28,
|
||||
EE: 20, EG: 29, ES: 24, FI: 18, FO: 18, FR: 27, GB: 22, GE: 22, GI: 23,
|
||||
GL: 18, GR: 27, GT: 28, HR: 21, HU: 28, IE: 22, IL: 23, IQ: 23, IS: 26,
|
||||
IT: 27, JO: 30, KW: 30, KZ: 20, LB: 28, LC: 32, LI: 21, LT: 20, LU: 20,
|
||||
LV: 21, LY: 25, 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, SC: 31, SE: 24, SI: 19, SK: 24, SM: 27, ST: 25, SV: 28, TL: 23,
|
||||
TN: 24, TR: 26, UA: 29, VA: 22, VG: 24, XK: 20,
|
||||
};
|
||||
|
||||
/** Strips spaces and uppercases. Returns "" if input is null-ish. */
|
||||
export function normaliseIban(value: string | null | undefined): string {
|
||||
return (value ?? "").replace(/\s+/g, "").toUpperCase();
|
||||
}
|
||||
|
||||
/** Formats an IBAN in the canonical 4-char-grouped form. */
|
||||
export function formatIban(value: string): string {
|
||||
const n = normaliseIban(value);
|
||||
return n.replace(/(.{4})/g, "$1 ").trim();
|
||||
}
|
||||
|
||||
/** Validates IBAN: country length + mod-97 checksum. */
|
||||
export function isValidIban(value: string): boolean {
|
||||
const iban = normaliseIban(value);
|
||||
if (!/^[A-Z]{2}\d{2}[A-Z0-9]+$/.test(iban)) return false;
|
||||
const country = iban.slice(0, 2);
|
||||
const expectedLength = IBAN_LENGTHS[country];
|
||||
if (!expectedLength || iban.length !== expectedLength) return false;
|
||||
|
||||
// Move first 4 chars to the end, convert letters to digits (A=10..Z=35).
|
||||
const rearranged = iban.slice(4) + iban.slice(0, 4);
|
||||
let numeric = "";
|
||||
for (const ch of rearranged) {
|
||||
if (ch >= "0" && ch <= "9") numeric += ch;
|
||||
else numeric += (ch.charCodeAt(0) - 55).toString(); // 'A' (65) -> 10
|
||||
}
|
||||
// mod 97 over a long numeric string — chunked to fit safely in JS numbers.
|
||||
let remainder = 0;
|
||||
for (let i = 0; i < numeric.length; i += 7) {
|
||||
const block = remainder.toString() + numeric.substring(i, i + 7);
|
||||
remainder = parseInt(block, 10) % 97;
|
||||
}
|
||||
return remainder === 1;
|
||||
}
|
||||
|
||||
/** True for BIC formats: 8 or 11 alphanumeric uppercase characters. */
|
||||
export function isValidBic(value: string): boolean {
|
||||
const v = (value ?? "").replace(/\s+/g, "").toUpperCase();
|
||||
return /^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$/.test(v);
|
||||
}
|
||||
|
||||
/** Austrian UID: ATU followed by 8 digits (case-insensitive). */
|
||||
export function isValidAtVatId(value: string): boolean {
|
||||
const v = (value ?? "").replace(/\s+/g, "").toUpperCase();
|
||||
return /^ATU\d{8}$/.test(v);
|
||||
}
|
||||
Reference in New Issue
Block a user