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
20pub 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}