use super::format::group_fixed;
use super::iban_countries::lookup;
use super::luhn::transliterate_alnum;
use super::{sanitize, Verdict};
use anyhow::{anyhow, Result};
fn mod97_of_digits(s: &str) -> Result<u32> {
let mut rem: u32 = 0;
for c in s.chars() {
let d = c.to_digit(10).ok_or_else(|| anyhow!("non-digit '{}'", c))?;
rem = (rem * 10 + d) % 97;
}
Ok(rem)
}
fn iban_mod97(iban: &str) -> Result<u32> {
if iban.len() < 4 {
return Err(anyhow!("IBAN too short"));
}
let rearranged = format!("{}{}", &iban[4..], &iban[..4]);
let numeric = transliterate_alnum(&rearranged)?;
mod97_of_digits(&numeric)
}
pub fn verify_iban(input: &str) -> Verdict {
let clean = sanitize(input, true);
if !clean.chars().all(|c| c.is_ascii_alphanumeric()) {
return Verdict::Invalid { reason: "non-alphanumeric input".into() };
}
if !(4..=34).contains(&clean.len()) {
return Verdict::Invalid { reason: format!("length {} out of IBAN range 4..=34", clean.len()) };
}
if !clean[..2].chars().all(|c| c.is_ascii_alphabetic()) {
return Verdict::Invalid { reason: "first 2 chars must be country code (letters)".into() };
}
let country_code = &clean[..2];
let detected = match lookup(country_code) {
Some(c) => {
if clean.len() != c.length {
return Verdict::Invalid {
reason: format!("{} IBAN must be {} chars, got {}", c.code, c.length, clean.len()),
};
}
format!("IBAN ({} — {})", c.code, c.name)
}
None => format!("IBAN ({} — unknown country)", country_code),
};
match iban_mod97(&clean) {
Ok(1) => Verdict::Valid {
formatted: group_fixed(&clean, 4, ' '),
detected,
comment: String::new(),
},
Ok(r) => Verdict::Invalid {
reason: format!("IBAN mod-97 check failed (got {}, expected 1)", r),
},
Err(e) => Verdict::Invalid { reason: e.to_string() },
}
}
pub fn create_iban(input: &str, _raw: bool) -> Result<String> {
let clean = sanitize(input, true);
if clean.len() < 2 {
return Err(anyhow!("input too short"));
}
let country_code = &clean[..2];
let country = lookup(country_code).ok_or_else(|| anyhow!("unknown country '{}'", country_code))?;
let expected_len = country.length;
let full_no_check = if clean.len() == expected_len {
format!("{}00{}", &clean[..2], &clean[4..])
} else if clean.len() == expected_len - 2 {
format!("{}00{}", &clean[..2], &clean[2..])
} else {
return Err(anyhow!(
"expected {} (with 00 placeholder) or {} (omit form) chars, got {}",
expected_len,
expected_len - 2,
clean.len()
));
};
let r = iban_mod97(&full_no_check)?;
let check_digits = 98 - r;
let full = format!("{}{:02}{}", &full_no_check[..2], check_digits, &full_no_check[4..]);
Ok(group_fixed(&full, 4, ' '))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn iban_se_valid() {
match verify_iban("SE3550000000054910000003") {
Verdict::Valid { formatted, .. } => assert_eq!(formatted, "SE35 5000 0000 0549 1000 0003"),
v => panic!("{:?}", v),
}
}
#[test]
fn iban_gb_valid() {
match verify_iban("GB82WEST12345698765432") {
Verdict::Valid { formatted, .. } => assert_eq!(formatted, "GB82 WEST 1234 5698 7654 32"),
v => panic!("{:?}", v),
}
}
#[test]
fn iban_de_valid() {
match verify_iban("DE89370400440532013000") {
Verdict::Valid { .. } => {}
v => panic!("{:?}", v),
}
}
#[test]
fn iban_no_valid() {
match verify_iban("NO9386011117947") {
Verdict::Valid { .. } => {}
v => panic!("{:?}", v),
}
}
#[test]
fn iban_fr_valid() {
match verify_iban("FR1420041010050500013M02606") {
Verdict::Valid { .. } => {}
v => panic!("{:?}", v),
}
}
#[test]
fn iban_accepts_spaces_in_input() {
match verify_iban("SE35 5000 0000 0549 1000 0003") {
Verdict::Valid { .. } => {}
v => panic!("{:?}", v),
}
}
#[test]
fn iban_invalid_checksum() {
match verify_iban("SE3550000000054910000004") {
Verdict::Invalid { .. } => {}
_ => panic!(),
}
}
#[test]
fn iban_wrong_length_for_country() {
match verify_iban("SE355000000005491000000") {
Verdict::Invalid { reason } => assert!(reason.contains("SE IBAN")),
v => panic!("{:?}", v),
}
}
#[test]
fn iban_unknown_country_still_validates_mod97() {
match verify_iban("ZZ82WEST12345698765432") {
Verdict::Invalid { .. } => {}
Verdict::Valid { detected, .. } => assert!(detected.contains("unknown")),
}
}
#[test]
fn create_iban_placeholder_form() {
let result = create_iban("SE0050000000054910000003", false).unwrap();
assert_eq!(result, "SE35 5000 0000 0549 1000 0003");
}
#[test]
fn create_iban_omit_form() {
let result = create_iban("SE50000000054910000003", false).unwrap();
assert_eq!(result, "SE35 5000 0000 0549 1000 0003");
}
}