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