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 →