Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { IBAN_LENGTHS } from "./validation_iban_data.ts"; ← this capability’s own data, compiled from data/iban-lengths.json into the same file by fune build
/** ISO 13616 allows 34 characters at most; Norway's 15 is the shortest issued. */
const MAX_IBAN = 34;
const MIN_IBAN = 15;
function isDigit(ch: string): boolean {
return ch >= "0" && ch <= "9";
}
function isUpperLetter(ch: string): boolean {
return ch >= "A" && ch <= "Z";
}
/**
* Uppercase ASCII only: `toUpperCase()` is Unicode-aware and would disagree
* with the Rust sibling on inputs like "ß". Nothing in an IBAN is non-ASCII.
*/
function compact(value: string): string {
let out = "";
for (const ch of value) {
// Only the ASCII space is stripped. IBANs are printed in groups of four
// and pasted that way; hyphens and other punctuation are not a printing
// convention, they are a sign the value came from somewhere unexpected.
if (ch === " ") continue;
out += ch >= "a" && ch <= "z" ? String.fromCharCode(ch.charCodeAt(0) - 32) : ch;
}
return out;
}
/** The registered IBAN length for a country, or -1 if the country has none. */
export function ibanLength(country: string): number {
const code = compact(country);
for (const row of IBAN_LENGTHS) {
if (row.country === code) return row.length;
}
return -1;
}
/**
* Is this a structurally valid IBAN?
*
* Two checks, both necessary. The ISO 13616 mod-97-10 checksum catches
* mistyped and transposed characters, but on its own it would accept a
* correctly-checksummed string of any length; the country's registered length
* is what catches a truncated or padded account number that still happens to
* check out.
*
* Neither check proves the account exists. Only the bank can say that, and
* only a payment (or a confirmation-of-payee service) proves it belongs to
* the person you think it does.
*/
export function isIban(value: string): boolean {
if (typeof value !== "string") return false;
const iban = compact(value);
if (iban.length < MIN_IBAN || iban.length > MAX_IBAN) return false;
// Positions 1-2 are the country, 3-4 the check digits. Testing this before
// the table lookup means a lower-case or punctuated value fails here rather
// than being reported as an unknown country.
if (!isUpperLetter(iban[0]) || !isUpperLetter(iban[1])) return false;
if (!isDigit(iban[2]) || !isDigit(iban[3])) return false;
const expected = ibanLength(iban.slice(0, 2));
if (expected < 0 || iban.length !== expected) return false;
for (let i = 4; i < iban.length; i++) {
const ch = iban[i];
if (!isDigit(ch) && !isUpperLetter(ch)) return false;
}
return mod97(iban) === 1;
}
/**
* ISO 13616 mod-97-10: move the first four characters to the end, replace
* each letter with its position in the alphabet plus 9 (A=10 ... Z=35), and
* take the whole thing modulo 97.
*
* The expansion of a 34-character IBAN is up to 68 digits, far past any
* 64-bit integer, so the remainder is carried forward one character at a
* time. A letter contributes two digits, so it multiplies the running
* remainder by 100; the largest intermediate is 96 * 100 + 35 = 9635, which
* is why this is safe in Rust's i64 and in JavaScript's doubles alike.
*/
function mod97(iban: string): number {
let remainder = 0;
for (let i = 0; i < iban.length; i++) {
// Rotation without building a second string: read from position 4
// onwards, then wrap round to the first four characters.
const ch = iban[(i + 4) % iban.length];
if (isDigit(ch)) {
remainder = (remainder * 10 + (ch.charCodeAt(0) - 48)) % 97;
} else {
remainder = (remainder * 100 + (ch.charCodeAt(0) - 55)) % 97;
}
}
return remainder;
}
/**
* The IBAN in its printed form, groups of four separated by single spaces,
* or null if it does not validate.
*/
export function formatIban(value: string): string | null {
if (!isIban(value)) return null;
const iban = compact(value);
const groups: string[] = [];
for (let i = 0; i < iban.length; i += 4) groups.push(iban.slice(i, i + 4));
return groups.join(" ");
}