Functional Weave
Code in Rust

validation.iban@1.0.0

impl/rust.rs

4,308 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::validation_iban_data::IBAN_LENGTHS;  ← 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: usize = 34;
const MIN_IBAN: usize = 15;

fn is_digit(ch: char) -> bool {
    ('0'..='9').contains(&ch)
}

fn is_upper_letter(ch: char) -> bool {
    ('A'..='Z').contains(&ch)
}

/// Strip spaces and fold to upper case, ASCII only, so that this agrees with
/// the TypeScript and Python siblings whose built-in case folding is
/// Unicode-aware. Nothing in an IBAN is non-ASCII.
fn compact(value: &str) -> Vec<char> {
    value
        .chars()
        // 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.
        .filter(|ch| *ch != ' ')
        .map(|ch| ch.to_ascii_uppercase())
        .collect()
}

/// The registered IBAN length for a country, or -1 if the country has none.
pub fn iban_length(country: &str) -> i64 {
    let code: String = compact(country).into_iter().collect();
    for row in IBAN_LENGTHS {
        if row.country == code {
            return row.length;
        }
    }
    -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.
pub fn is_iban(value: &str) -> bool {
    let iban = compact(value);
    if iban.len() < MIN_IBAN || iban.len() > 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 !is_upper_letter(iban[0]) || !is_upper_letter(iban[1]) {
        return false;
    }
    if !is_digit(iban[2]) || !is_digit(iban[3]) {
        return false;
    }

    let country: String = iban[0..2].iter().collect();
    let expected = iban_length(&country);
    if expected < 0 || iban.len() as i64 != expected {
        return false;
    }

    for ch in &iban[4..] {
        if !is_digit(*ch) && !is_upper_letter(*ch) {
            return false;
        }
    }

    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, which no 64-bit
/// integer can hold, 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, nowhere
/// near an overflow.
fn mod97(iban: &[char]) -> i64 {
    let mut remainder: i64 = 0;
    let n = iban.len();
    for i in 0..n {
        // Rotation without building a second string: read from position 4
        // onwards, then wrap round to the first four characters.
        let ch = iban[(i + 4) % n];
        if is_digit(ch) {
            remainder = (remainder * 10 + (ch as i64 - 48)) % 97;
        } else {
            remainder = (remainder * 100 + (ch as i64 - 55)) % 97;
        }
    }
    remainder
}

/// The IBAN in its printed form, groups of four separated by single spaces,
/// or `None` if it does not validate.
pub fn format_iban(value: &str) -> Option<String> {
    if !is_iban(value) {
        return None;
    }
    let iban = compact(value);
    let groups: Vec<String> = iban.chunks(4).map(|c| c.iter().collect()).collect();
    Some(groups.join(" "))
}

pub fn fune_vector(args: &[Value]) -> Value {
    // A non-string argument arrives here as an empty string, which is exactly
    // the answer TypeScript and Python give for a non-string: not an IBAN.
    Value::Bool(is_iban(args[0].as_str()))
}