vin-decode 0.2.0

Auto-updating VIN decoder backed by NHTSA vPIC, refreshed monthly via CI
Documentation
use crate::{Error, Vin};

const WEIGHTS: [u32; 17] = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2];

fn translit(c: char) -> u32 {
    match c {
        '0'..='9' => c.to_digit(10).unwrap(),
        'A' | 'J' => 1,
        'B' | 'K' | 'S' => 2,
        'C' | 'L' | 'T' => 3,
        'D' | 'M' | 'U' => 4,
        'E' | 'N' | 'V' => 5,
        'F' | 'W' => 6,
        'G' | 'P' | 'X' => 7,
        'H' | 'Y' => 8,
        'R' | 'Z' => 9,
        _ => 0,
    }
}

pub fn validate(vin: &Vin) -> crate::Result<()> {
    let bytes = vin.as_str().as_bytes();
    let sum: u32 = bytes
        .iter()
        .enumerate()
        .map(|(i, b)| translit(*b as char) * WEIGHTS[i])
        .sum();
    let expected = match sum % 11 {
        10 => 'X',
        n => char::from_digit(n, 10).unwrap(),
    };
    let actual = bytes[8] as char;
    if expected == actual {
        Ok(())
    } else {
        Err(Error::BadCheckDigit { expected, actual })
    }
}

#[allow(dead_code)]
pub fn compute(vin: &str) -> Option<char> {
    if vin.len() != 17 {
        return None;
    }
    let bytes = vin.as_bytes();
    let sum: u32 = bytes
        .iter()
        .enumerate()
        .map(|(i, b)| translit(*b as char) * WEIGHTS[i])
        .sum();
    Some(match sum % 11 {
        10 => 'X',
        n => char::from_digit(n, 10).unwrap(),
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn known_valid_vins_pass() {
        let vins = [
            "1HGCM82633A004352",
            "2FTEF14H8TCA73155",
            "5YJ3E1EA0KF000316",
        ];
        for v in vins {
            let vin = Vin::new(v).unwrap();
            assert!(validate(&vin).is_ok(), "{} should validate", v);
        }
    }

    #[test]
    fn check_digit_x_for_remainder_10() {
        let bad = "1HGCM82633A004353";
        let vin = Vin::new(bad).unwrap();
        let r = validate(&vin);
        assert!(matches!(r, Err(Error::BadCheckDigit { .. })));
    }

    #[test]
    fn translit_letter_groups() {
        assert_eq!(translit('A'), 1);
        assert_eq!(translit('J'), 1);
        assert_eq!(translit('B'), 2);
        assert_eq!(translit('S'), 2);
        assert_eq!(translit('R'), 9);
        assert_eq!(translit('Z'), 9);
        assert_eq!(translit('0'), 0);
        assert_eq!(translit('9'), 9);
    }

    #[test]
    fn compute_matches_validate() {
        let v = "1HGCM82633A004352";
        assert_eq!(compute(v), Some('3'));
    }

    #[test]
    fn compute_rejects_wrong_length() {
        assert_eq!(compute("TOOSHORT"), None);
    }
}