Skip to content

IBAN Validator — Rust source

Validate International Bank Account Numbers (IBAN) with the mod-97 checksum, verify the country-specific length, and format the result. 100% client-side, no network.

This is the Rust implementation — the same logic the interactive tool runs, in a shareable, citable form.

//! Pure IBAN validation logic — ISO 13616 mod-97 checksum.
//!
//! Language: Rust
//! CosmoDev polyglot showcase port of the `iban-validator` tool.
//! Ported from src/lib/iban.ts — display source, part of CosmoDev's
//! polyglot tool pages.
//!
//! Normalizes the input (uppercase, strip whitespace/dashes), checks the
//! structure (country code + 2 check digits + 1-30 BBAN chars), verifies
//! the per-country length, then runs the ISO 13616 mod-97 checksum: move
//! the first 4 chars to the end, map A=10..Z=35, and confirm the
//! resulting integer is congruent to 1 mod 97. The remainder is folded
//! one decimal digit at a time (it stays < 97), so the standard library
//! alone suffices — no regex or big-integer crate is required.

use std::collections::HashMap;

/// Per-country IBAN lengths (ISO 13616) — a representative subset.
fn iban_lengths() -> HashMap<&'static str, u32> {
    let mut m = HashMap::new();
    for (cc, n) in [
        ("AL", 28), ("AD", 24), ("AT", 20), ("AZ", 28), ("BH", 22), ("BY", 28),
        ("BE", 16), ("BA", 20), ("BR", 29), ("BG", 22), ("CR", 22), ("HR", 21),
        ("CY", 28), ("CZ", 24), ("DK", 18), ("DO", 28), ("EE", 20), ("FO", 18),
        ("FI", 18), ("FR", 27), ("GE", 22), ("DE", 22), ("GI", 23), ("GR", 27),
        ("GL", 18), ("GT", 28), ("HU", 28), ("IS", 26), ("IE", 22), ("IL", 23),
        ("IT", 27), ("JO", 30), ("KZ", 20), ("XK", 20), ("KW", 30), ("LV", 21),
        ("LB", 28), ("LI", 21), ("LT", 20), ("LU", 20), ("MK", 19), ("MT", 31),
        ("MR", 27), ("MU", 30), ("MC", 27), ("MD", 24), ("ME", 22), ("NL", 18),
        ("NO", 15), ("PK", 24), ("PS", 29), ("PL", 28), ("PT", 25), ("QA", 29),
        ("RO", 24), ("LC", 32), ("SM", 27), ("ST", 25), ("SA", 24), ("RS", 22),
        ("SC", 31), ("SK", 24), ("SI", 19), ("SG", 19), ("ES", 24), ("SE", 24),
        ("CH", 21), ("TL", 23), ("TN", 24), ("TR", 26), ("UA", 29), ("AE", 23),
        ("GB", 22), ("VG", 24),
    ] {
        m.insert(cc, n);
    }
    m
}

/// Result of validation. `Option` fields mirror the TS nullable fields.
pub struct IbanInfo {
    pub input: String,
    pub cleaned: String,
    pub country_code: Option<String>,
    pub valid: bool,
    pub checksum_ok: bool,
    pub length_ok: bool,
    pub expected_length: Option<u32>,
    pub formatted: String,
    pub error: Option<String>,
}

/// ISO 13616 mod-97 checksum over a CLEANED iban (uppercase, no spaces).
pub fn mod97_check(cleaned: &str) -> bool {
    let b = cleaned.as_bytes();
    if b.len() < 4 {
        return false;
    }
    // Rearranged = bytes[4..] ++ bytes[..4] (country + check moved to end).
    let mut numeric = String::with_capacity(b.len() * 2);
    for &c in b[4..].iter().chain(b[..4].iter()) {
        if c.is_ascii_digit() {
            numeric.push(c as char);
        } else if c.is_ascii_uppercase() {
            // A=10 .. Z=35
            numeric.push_str(&format!("{}", c - b'A' + 10));
        } else {
            return false; // invalid character
        }
    }
    let mut rem: u32 = 0;
    for c in numeric.bytes() {
        rem = (rem * 10 + (c - b'0') as u32) % 97;
    }
    rem == 1
}

/// Insert a space every 4 characters (last group may be short) — the
/// hand-rolled equivalent of the TS regex /(.{4})(?=.)/g + trim.
fn format_groups(cleaned: &str) -> String {
    let mut out = String::with_capacity(cleaned.len() + cleaned.len() / 4);
    for (i, b) in cleaned.bytes().enumerate() {
        if i > 0 && i % 4 == 0 {
            out.push(' ');
        }
        out.push(b as char);
    }
    out
}

/// Validate an IBAN. Never panics — all failures are reported via `error`.
pub fn validate_iban(input: &str) -> IbanInfo {
    let cleaned: String = input
        .to_ascii_uppercase()
        .bytes()
        .filter(|&b| b != b' ' && b != b'-' && b != b'\t' && b != b'\n' && b != b'\r')
        .map(|b| b as char)
        .collect();

    let cb = cleaned.as_bytes();
    let country_code = if cb.len() >= 2 && cb[0].is_ascii_uppercase() && cb[1].is_ascii_uppercase() {
        Some(cleaned[..2].to_string())
    } else {
        None
    };
    let expected_length = country_code.as_deref().and_then(|cc| iban_lengths().get(cc).copied());

    let mut info = IbanInfo {
        input: input.to_string(),
        formatted: format_groups(&cleaned),
        country_code,
        expected_length,
        cleaned,
        valid: false,
        checksum_ok: false,
        length_ok: false,
        error: None,
    };

    // Structure: 2 letters, 2 digits, then 1..30 alnum, total len 5..34.
    let c = info.cleaned.as_bytes();
    let struct_ok = (5..=34).contains(&c.len())
        && c[0].is_ascii_uppercase()
        && c[1].is_ascii_uppercase()
        && c[2].is_ascii_digit()
        && c[3].is_ascii_digit()
        && c[4..].iter().all(|&b| b.is_ascii_uppercase() || b.is_ascii_digit());
    if !struct_ok {
        info.error = Some("Invalid IBAN format.".into());
        return info;
    }

    let checksum_ok = mod97_check(&info.cleaned);
    info.checksum_ok = checksum_ok;
    info.length_ok = expected_length.map_or(true, |n| c.len() as u32 == n);

    if !info.length_ok {
        info.error = Some(format!("Length should be {} for {}.", expected_length.unwrap(),
                                  info.country_code.as_deref().unwrap_or("")));
        return info;
    }
    if !checksum_ok {
        info.error = Some("Checksum failed.".into());
        return info;
    }
    info.valid = true;
    info
}

fn main() {
    // GB82 WEST 1234 5698 7654 32 — a known-good reference IBAN.
    let r = validate_iban("GB82WEST12345698765432");
    println!("valid={} error={:?}", r.valid, r.error);
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →