Functional Weave
Code in TypeScript

validation.vin@1.0.0

impl/rust.rs

3,519 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

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