Skip to main content

vin_decode/
wmi.rs

1use crate::Error;
2
3const FORBIDDEN: &[char] = &['I', 'O', 'Q'];
4
5pub fn validate_chars(s: &str) -> crate::Result<()> {
6    if s.len() != 17 {
7        return Err(Error::InvalidLength(s.len()));
8    }
9    for c in s.chars() {
10        if !c.is_ascii_alphanumeric() {
11            return Err(Error::InvalidChar(c));
12        }
13        if FORBIDDEN.contains(&c) {
14            return Err(Error::ForbiddenChar(c));
15        }
16    }
17    Ok(())
18}
19
20/// Map a VIN's first character (region code) to its ISO 3779 region name.
21///
22/// Returns `None` for unassigned codes (`0`, non-alphanumeric).
23pub fn region(first: char) -> Option<&'static str> {
24    match first {
25        'A'..='H' => Some("Africa"),
26        'J'..='R' => Some("Asia"),
27        'S'..='Z' => Some("Europe"),
28        '1'..='5' => Some("North America"),
29        '6'..='7' => Some("Oceania"),
30        '8'..='9' => Some("South America"),
31        _ => None,
32    }
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38
39    #[test]
40    fn validate_accepts_clean_vin() {
41        assert!(validate_chars("1HGCM82633A004352").is_ok());
42    }
43
44    #[test]
45    fn validate_rejects_short_or_long() {
46        assert!(matches!(
47            validate_chars("TOOSHORT"),
48            Err(Error::InvalidLength(_))
49        ));
50        assert!(matches!(
51            validate_chars("WAYTOOLONG12345678"),
52            Err(Error::InvalidLength(_))
53        ));
54    }
55
56    #[test]
57    fn validate_rejects_forbidden_iqo() {
58        for forbidden in ['I', 'O', 'Q'] {
59            let mut s = String::from("1HGCM82633A004352");
60            unsafe { s.as_bytes_mut()[5] = forbidden as u8 };
61            let res = validate_chars(&s);
62            assert!(
63                matches!(res, Err(Error::ForbiddenChar(c)) if c == forbidden),
64                "expected ForbiddenChar({}), got {:?}",
65                forbidden,
66                res
67            );
68        }
69    }
70
71    #[test]
72    fn validate_rejects_non_alnum() {
73        let s = "1HGCM82633A00435!";
74        assert!(matches!(validate_chars(s), Err(Error::InvalidChar(_))));
75    }
76
77    #[test]
78    fn region_buckets_full_coverage() {
79        assert_eq!(region('A'), Some("Africa"));
80        assert_eq!(region('H'), Some("Africa"));
81        assert_eq!(region('J'), Some("Asia"));
82        assert_eq!(region('R'), Some("Asia"));
83        assert_eq!(region('S'), Some("Europe"));
84        assert_eq!(region('Z'), Some("Europe"));
85        assert_eq!(region('1'), Some("North America"));
86        assert_eq!(region('5'), Some("North America"));
87        assert_eq!(region('6'), Some("Oceania"));
88        assert_eq!(region('7'), Some("Oceania"));
89        assert_eq!(region('8'), Some("South America"));
90        assert_eq!(region('9'), Some("South America"));
91        assert_eq!(region('0'), None);
92        assert_eq!(region('!'), None);
93    }
94}