Functional Weave
Code in TypeScript

validation.uk-modulus-table@1.0.0

impl/rust.rs

7,197 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

const ALGORITHMS: [&str; 3] = ["MOD10", "MOD11", "DBLAL"];

/// The fields of each non-blank line, with its 1-based number. Only ASCII
/// spaces and tabs separate fields, and a trailing carriage return is
/// dropped, so a file saved with Windows line endings reads the same and
/// every language splits alike.
fn lines(text: &str) -> Vec<(usize, Vec<&str>)> {
    let body = text.strip_prefix('\u{FEFF}').unwrap_or(text);
    let mut out = Vec::new();
    for (i, raw) in body.split('\n').enumerate() {
        let line = raw.strip_suffix('\r').unwrap_or(raw);
        let fields: Vec<&str> = line.split(|c| c == ' ' || c == '\t').filter(|f| !f.is_empty()).collect();
        if !fields.is_empty() {
            out.push((i + 1, fields));
        }
    }
    out
}

fn sort_code(file: &str, line: usize, value: &str) -> String {
    if value.len() != 6 || !value.bytes().all(|b| b.is_ascii_digit()) {
        panic!("{} line {}: \"{}\" is not a six-digit sort code", file, line, value);
    }
    value.to_string()
}

/// `-?[0-9]{1,4}`, parsed; None for anything else.
fn weight(value: &str) -> Option<i64> {
    let digits = value.strip_prefix('-').unwrap_or(value);
    if digits.is_empty() || digits.len() > 4 || !digits.bytes().all(|b| b.is_ascii_digit()) {
        return None;
    }
    let n: i64 = digits.parse().ok()?;
    Some(if value.starts_with('-') { -n } else { n })
}

fn parse_row(line: usize, fields: &[&str]) -> UkModulusRow {
    let wh = format!("VALACDOS.txt line {}", line);
    if fields.len() != 17 && fields.len() != 18 {
        panic!(
            "{}: expected 17 or 18 fields (start, end, algorithm, 14 weights, optional exception), found {}",
            wh,
            fields.len()
        );
    }
    let start = sort_code("VALACDOS.txt", line, fields[0]);
    let end = sort_code("VALACDOS.txt", line, fields[1]);
    if end < start {
        panic!("{}: range {} to {} ends before it starts", wh, start, end);
    }
    let algorithm = fields[2];
    if !ALGORITHMS.contains(&algorithm) {
        panic!("{}: unknown algorithm \"{}\"; expected MOD10, MOD11 or DBLAL", wh, algorithm);
    }
    let weights: Vec<i64> = fields[3..17]
        .iter()
        .map(|w| match weight(w) {
            Some(n) => n,
            None => panic!("{}: weight \"{}\" is not a whole number", wh, w),
        })
        .collect();
    // A digit sum of a negative product means different things in different
    // languages' remainder rules; the specification never needs one.
    if algorithm == "DBLAL" && weights.iter().any(|w| *w < 0) {
        panic!("{}: a DBLAL row cannot have a negative weight", wh);
    }
    let mut exception = None;
    if fields.len() == 18 {
        let e = fields[17];
        let n = if e.is_empty() || e.len() > 2 || !e.bytes().all(|b| b.is_ascii_digit()) {
            0
        } else {
            e.parse::<i64>().unwrap_or(0)
        };
        if !(1..=14).contains(&n) {
            panic!("{}: exception \"{}\" is not a number from 1 to 14", wh, e);
        }
        exception = Some(n);
    }
    UkModulusRow {
        start,
        end,
        algorithm: algorithm.to_string(),
        weights,
        exception,
    }
}

fn parse_substitution(line: usize, fields: &[&str]) -> UkSortCodeSubstitution {
    if fields.len() != 2 {
        panic!("SCSUBTAB.txt line {}: expected two sort codes, found {} fields", line, fields.len());
    }
    UkSortCodeSubstitution {
        original: sort_code("SCSUBTAB.txt", line, fields[0]),
        substitute: sort_code("SCSUBTAB.txt", line, fields[1]),
    }
}

/// Parse the text of Vocalink's VALACDOS.txt (the modulus weight table) and
/// SCSUBTAB.txt (exception 5's sort code substitutions) into the table
/// `validate_uk_sort_code_account` checks against.
///
/// Rows keep their file order, which matters: where a sort code falls in two
/// rows, the first is the first check. Blank lines are skipped; anything else
/// malformed panics naming the file and the line, because a silently dropped
/// row turns a checkable sort code into an unchecked one.
pub fn parse_uk_modulus_table(valacdos: &str, scsubtab: &str) -> UkModulusTable {
    let rows: Vec<UkModulusRow> = lines(valacdos).iter().map(|(n, f)| parse_row(*n, f)).collect();
    if rows.is_empty() {
        panic!("VALACDOS.txt has no rows");
    }
    let substitutions = lines(scsubtab).iter().map(|(n, f)| parse_substitution(*n, f)).collect();
    UkModulusTable { rows, substitutions }
}

/// Object keys are camelCase to match the shared vectors.
pub fn uk_modulus_table_to_value(table: &UkModulusTable) -> Value {
    Value::obj(vec![
        (
            "rows",
            Value::Arr(
                table
                    .rows
                    .iter()
                    .map(|r| {
                        Value::obj(vec![
                            ("start", Value::str(&r.start)),
                            ("end", Value::str(&r.end)),
                            ("algorithm", Value::str(&r.algorithm)),
                            ("weights", Value::Arr(r.weights.iter().map(|w| Value::Int(*w)).collect())),
                            ("exception", r.exception.map_or(Value::Null, Value::Int)),
                        ])
                    })
                    .collect(),
            ),
        ),
        (
            "substitutions",
            Value::Arr(
                table
                    .substitutions
                    .iter()
                    .map(|s| {
                        Value::obj(vec![
                            ("original", Value::str(&s.original)),
                            ("substitute", Value::str(&s.substitute)),
                        ])
                    })
                    .collect(),
            ),
        ),
    ])
}

/// The table from its JSON form, for the vector adapters of capabilities that
/// take one. A missing list is empty and a missing exception is null.
pub fn uk_modulus_table_from_value(v: &Value) -> UkModulusTable {
    let list = |key: &str| -> Vec<Value> {
        let field = v.get(key);
        if field.is_null() {
            Vec::new()
        } else {
            field.as_arr().to_vec()
        }
    };
    UkModulusTable {
        rows: list("rows")
            .iter()
            .map(|r| UkModulusRow {
                start: r.get("start").as_str().to_string(),
                end: r.get("end").as_str().to_string(),
                algorithm: r.get("algorithm").as_str().to_string(),
                weights: r.get("weights").as_arr().iter().map(|w| w.as_i64()).collect(),
                exception: if r.get("exception").is_null() { None } else { Some(r.get("exception").as_i64()) },
            })
            .collect(),
        substitutions: list("substitutions")
            .iter()
            .map(|s| UkSortCodeSubstitution {
                original: s.get("original").as_str().to_string(),
                substitute: s.get("substitute").as_str().to_string(),
            })
            .collect(),
    }
}

pub fn fune_vector(args: &[Value]) -> Value {
    uk_modulus_table_to_value(&parse_uk_modulus_table(args[0].as_str(), args[1].as_str()))
}