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()))
}