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