vin_parser/
lib.rs

1//! # Vehicle Identification Number parser
2//!
3//! Parser and checksum verifier for VIN.
4//!
5//! Provides information about region, manufacturer, country of origin, possible years of assembling
6//! and checksum validation of given Vehicle Identification Number.
7//!
8//! # Examples
9//! ```
10//! // Check whether VIN is ok (without checksum validation)
11//! let vin_number = "WP0ZZZ99ZTS392124";
12//! assert!(vin::check_validity(vin_number).is_ok());
13//! ```
14//!
15//! ```
16//! // Check VIN with checksum validation
17//! let vin_number = "1M8GDM9AXKP042788";
18//! assert!(vin::verify_checksum(vin_number).is_ok());
19//! ```
20//!
21//! ```
22//! // Get VIN information
23//! let vin_number = "wp0zzz998ts392124";
24//! let result = vin::get_info(vin_number).unwrap();
25//! assert_eq!(result.vin, vin_number.to_uppercase());
26//! assert_eq!(result.country, "Germany/West Germany");
27//! assert_eq!(result.manufacturer, "Porsche car");
28//! assert_eq!(result.region, "Europe");
29//! assert!(result.valid_checksum.is_ok());
30//! ```
31#[macro_use]
32extern crate lazy_static;
33
34use std::collections::HashSet;
35use std::fmt;
36use std::time::SystemTime;
37
38use crate::VINError::{ChecksumError, IncorrectLength, InvalidCharacters};
39use crate::dicts::{get_region, get_country, get_manufacturer};
40
41mod dicts;
42
43
44/// Provides information about invalid checksum calculation from the VIN
45#[derive(Debug, Copy, Clone)]
46pub struct ChecksumErrorInfo {
47    /// Expected symbol at the 9-nth place
48    pub expected: char,
49
50    /// Received symbol at the 9-th place
51    pub received: char,
52}
53
54/// Provides possible errors during VIN parsing
55#[derive(Debug)]
56pub enum VINError {
57    /// Provided number length != 17
58    IncorrectLength,
59
60    /// Provided number contains invalid characters
61    InvalidCharacters(HashSet<char>),
62
63    /// Provided number did not pass checksum validation (notice, that only North American VINs
64    /// must pass this validation, for others it is not obligatory)
65    ChecksumError(ChecksumErrorInfo),
66}
67
68impl fmt::Display for VINError {
69    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
70        match self {
71            VINError::IncorrectLength =>
72                write!(f, "Incorrect length of given string, 17 chars expected."),
73            VINError::InvalidCharacters(chars) =>
74                write!(f, "Invalid characters received in given string: {:?}.", chars),
75            VINError::ChecksumError(err) =>
76                write!(f, "Invalid checksum symbol on 9th place, {} expected, {} received.", err.expected, err.received),
77        }
78    }
79}
80
81/// Holds parsed information about the vehicle
82#[derive(Debug, Clone)]
83pub struct VIN {
84    /// Copy of provided VIN number
85    pub vin: String,
86
87    /// Country of the manufacturer
88    pub country: String,
89
90    /// Name of the manufacturer
91    pub manufacturer: String,
92
93    /// Region of the manufacturer
94    pub region: String,
95
96    /// Whether checksum of the VIN is valid
97    pub valid_checksum: Result<(), ChecksumErrorInfo>,
98}
99
100
101impl VIN {
102    /// Returns WMI part of VIN
103    pub fn wmi(&self) -> &str { &self.vin[..3] }
104
105    /// Returns VDS part of VIN
106    pub fn vds(&self) -> &str { &self.vin[3..9] }
107
108    /// Returns VIS part of VIN
109    pub fn vis(&self) -> &str { &self.vin[9..] }
110
111    /// Returns whether manufacturer is small and does not have its own ID in VIN
112    pub fn small_manufacturer(&self) -> bool { &self.wmi()[2..] == "9" }
113
114    /// Returns region VIN code
115    pub fn region_code(&self) -> &str { &self.wmi()[..1] }
116
117    /// Returns country VIN code
118    pub fn country_code(&self) -> &str { &self.wmi()[1..] }
119
120    /// Returns possible years of assembling
121    pub fn years(&self) -> Vec<u32> {
122        let letters = "ABCDEFGHJKLMNPRSTVWXY123456789";
123        let year_letter = &self.vis().chars().nth(0).unwrap();
124
125        let mut year: u32 = 1979;
126        let cur_year = SystemTime::now()
127            .duration_since(SystemTime::UNIX_EPOCH)
128            .unwrap()
129            .as_secs_f64();
130        let cur_year = cur_year / 3600.0 / 24.0 / 365.25 + 1970.0;  // get year
131        let cur_year = (cur_year.round() + 2.0) as u32;             // add 2 years in advance
132
133        let mut result = vec![];
134        for letter in letters.chars().cycle() {
135            year += 1;
136
137            if letter == *year_letter {
138                result.push(year);
139            }
140
141            if year == cur_year { break; }
142        }
143
144        result
145    }
146}
147
148
149/// Validates Vehicle Identification Number without computing the checksum
150/// (check used symbols and length of the number)
151///
152/// # Examples
153/// ```
154/// let vin_number = "WP0ZZZ99ZTS392124";
155/// assert!(vin::check_validity(vin_number).is_ok());
156///
157/// let vin_number = "W$0ZZZ99ZTS392124";  // notice $ sign on 2-nd place
158/// assert!(vin::check_validity(vin_number).is_err())
159/// ```
160pub fn check_validity(vin: &str) -> Result<(), VINError> {
161    let vin = vin.to_uppercase();
162
163    // check length
164    if vin.chars().count() != 17 {
165        return Err(IncorrectLength);
166    }
167
168    // check alphabet
169    let used_chars: HashSet<char> = vin.chars().collect();
170    let odd_chars: HashSet<char> = used_chars.difference(&dicts::ALLOWED_CHARS).cloned().collect();
171    if odd_chars.len() > 0 {
172        return Err(InvalidCharacters(odd_chars));
173    }
174
175    Ok(())
176}
177
178
179/// Validates Vehicle Identification Number AND validates the checksum
180///
181/// # Examples
182/// ```
183/// let vin_number = "1M8GDM9AXKP042788";
184/// assert!(vin::verify_checksum(vin_number).is_ok());
185///
186/// let vin_number = "WP0ZZZ99ZTS392124";
187/// assert!(match vin::verify_checksum(vin_number) {
188///     Err(vin::VINError::ChecksumError(vin::ChecksumErrorInfo {
189///         expected: '8',
190///         received: 'Z',
191///     })) => true,
192///     _ => false,
193/// })
194/// ```
195pub fn verify_checksum(vin: &str) -> Result<(), VINError> {
196    let vin = vin.to_uppercase();
197    check_validity(&vin)?;
198
199    // verify checksum
200    let checksum: u32 = vin
201        .chars()
202        .map(|x| dicts::VALUE_MAP.get(&x).unwrap())
203        .zip(dicts::WEIGHTS.iter())
204        .map(|(l, r)| l * r)
205        .sum();
206
207
208    let checknumber = match checksum % 11 {
209        10 => 'X',
210        i => std::char::from_digit(i, 10).unwrap()
211    };
212
213    let pr_number = vin.chars().nth(8).unwrap();
214    if pr_number == checknumber {
215        Ok(())
216    } else {
217        Err(ChecksumError(ChecksumErrorInfo {
218            expected: checknumber,
219            received: pr_number,
220        }))
221    }
222}
223
224
225/// Return basic information about manufacturer of the vehicle
226///
227/// # Examples
228/// ```
229/// let vin_number = "wp0zzz998ts392124";
230/// let result = vin::get_info(vin_number).unwrap();
231/// assert_eq!(result.vin, vin_number.to_uppercase());
232/// assert_eq!(result.country, "Germany/West Germany");
233/// assert_eq!(result.manufacturer, "Porsche car");
234/// assert_eq!(result.region, "Europe");
235/// assert!(result.valid_checksum.is_ok())
236/// ```
237pub fn get_info(vin: &str) -> Result<VIN, VINError> {
238    let vin = vin.to_uppercase();
239    check_validity(&vin)?;
240
241    return Ok(VIN {
242        vin: vin.clone(),
243        country: get_country(&vin[..2]),
244        manufacturer: get_manufacturer(&vin[..3]),
245        region: get_region(&vin[..1]),
246        valid_checksum: match verify_checksum(&vin) {
247            Ok(()) => Ok(()),
248            Err(VINError::ChecksumError(x)) => Err(x),
249            _ => Ok(())     // unreachable
250        }
251    })
252}
253
254