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
/// Letter values from 49 CFR 565.15, indexed by letter A-Z. I, O and Q are
/// never used and hold -1; the others run 1-9 in three passes (A-I, J-R, S-Z)
/// with the forbidden letters' slots skipped.
const LETTER_VALUES: [i64; 26] = [1, 2, 3, 4, 5, 6, 7, 8, -1, 1, 2, 3, 4, 5, -1, 7, -1, 9, 2, 3, 4, 5, 6, 7, 8, 9];
/// Position weights; position 9 is the check digit itself and weighs nothing.
const WEIGHTS: [i64; 17] = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2];
fn invalid(reason: &str) -> VinCheck {
VinCheck {
valid: false,
normalised: None,
wmi: None,
check_digit_matches: None,
reason: Some(reason.to_string()),
}
}
/// Check a 17-character VIN.
///
/// The check digit is enforced only for North American VINs (first character
/// 1-5), where the law requires it; elsewhere a mismatch is reported in
/// `check_digit_matches` but does not make the VIN invalid, because many
/// European and Asian VINs never carried one. Validators answer rather than
/// panic.
pub fn validate_vin(value: &str) -> VinCheck {
let vin: Vec<char> = value
.chars()
.filter(|ch| *ch != ' ')
.map(|ch| ch.to_ascii_uppercase())
.collect();
if vin.is_empty() {
return invalid("empty");
}
if vin.iter().any(|ch| !(ch.is_ascii_digit() || ch.is_ascii_uppercase())) {
return invalid("bad-character");
}
if vin.len() != 17 {
return invalid("bad-length");
}
let mut total: i64 = 0;
for (i, ch) in vin.iter().enumerate() {
let n = if ch.is_ascii_digit() {
*ch as i64 - 48
} else {
let n = LETTER_VALUES[(*ch as u8 - b'A') as usize];
// I, O and Q read as 1, 0 and 0, so the standard never issues them.
if n < 0 {
return invalid("forbidden-letter");
}
n
};
total += n * WEIGHTS[i];
}
let remainder = total % 11;
let expected = if remainder == 10 {
'X'
} else {
char::from_digit(remainder as u32, 10).unwrap()
};
let matches = vin[8] == expected;
// WMIs starting 1-5 are North American, where 49 CFR 565 makes the check
// digit mandatory; anywhere else it is optional, so only report it.
if !matches && ('1'..='5').contains(&vin[0]) {
return invalid("bad-check-digit");
}
VinCheck {
valid: true,
normalised: Some(vin.iter().collect()),
wmi: Some(vin[..3].iter().collect()),
check_digit_matches: Some(matches),
reason: None,
}
}
/// Object keys are camelCase to match the shared vectors.
pub fn vin_check_to_value(result: &VinCheck) -> Value {
let text = |field: &Option<String>| match field {
Some(s) => Value::str(s),
None => Value::Null,
};
Value::obj(vec![
("valid", Value::Bool(result.valid)),
("normalised", text(&result.normalised)),
("wmi", text(&result.wmi)),
(
"checkDigitMatches",
match result.check_digit_matches {
Some(b) => Value::Bool(b),
None => Value::Null,
},
),
("reason", text(&result.reason)),
])
}
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: empty.
vin_check_to_value(&validate_vin(args[0].as_str()))
}