vin_info/
lib.rs

1//! VIN information library
2
3#![no_std]
4#![warn(missing_docs)]
5#![cfg_attr(feature = "cargo-clippy", allow(clippy::style))]
6
7use core::{fmt, str, slice, char};
8
9#[cfg(debug_assertions)]
10macro_rules! unreach {
11    () => {
12        unreachable!()
13    }
14}
15
16#[cfg(not(debug_assertions))]
17macro_rules! unreach {
18    () => {
19        unsafe {
20            core::hint::unreachable_unchecked()
21        }
22    }
23}
24
25mod dicts;
26pub use dicts::Region;
27
28const VIN_LEN: usize = 17;
29
30///Error parsing VIN
31#[derive(Copy, Clone, Debug, PartialEq, Eq)]
32pub enum VinError {
33    ///VIN must be 17 characters long
34    InvalidLen,
35    ///VIN contains invalid character.
36    InvalidChar(usize, char),
37}
38
39impl fmt::Display for VinError {
40    #[inline(always)]
41    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
42        match self {
43            Self::InvalidLen => fmt.write_str("VIN MUST be 17 characters long"),
44            Self::InvalidChar(idx, ch) => fmt.write_fmt(format_args!("VIN contains invalid character '{ch}' at idx={idx}")),
45        }
46    }
47}
48
49#[repr(transparent)]
50#[derive(Copy, Clone, PartialEq, Eq)]
51///Vehicle identifier number
52pub struct Vin<'a>(&'a str);
53
54impl<'a> Vin<'a> {
55    ///Creates new instance with panic on invalid input.
56    pub const fn new(vin: &'a str) -> Self {
57        match Self::try_new(vin) {
58            Ok(this) => this,
59            Err(VinError::InvalidLen) => panic!("Invalid length of VIN"),
60            Err(VinError::InvalidChar(_, _)) => panic!("VIN contains invalid character"),
61        }
62    }
63
64    ///Creates new instance
65    pub const fn try_new(vin: &'a str) -> Result<Self, VinError> {
66        if vin.len() != VIN_LEN {
67            return Err(VinError::InvalidLen);
68        }
69
70        let mut idx = 0;
71        while idx < vin.len() {
72            let ch = vin.as_bytes()[idx];
73            match ch as char {
74                //these are exceptions to letters
75                'I' | 'O' | 'Q' => return Err(VinError::InvalidChar(idx, ch as char)),
76                'A'..='Z' | '0'..='9' => idx += 1,
77                ch => return Err(VinError::InvalidChar(idx, ch)),
78            }
79        }
80
81        Ok(Self(vin))
82    }
83
84    ///Calculates checksum of the provided VIN
85    pub const fn calculate_checksum(&self) -> u32 {
86        let mut result = 0u32;
87        let mut idx = 0;
88
89        while idx < self.0.len() {
90            let ch = self.0.as_bytes()[idx];
91            //Example from wiki
92            //https://en.wikipedia.org/wiki/Vehicle_identification_number#Worked_example
93            result = result.wrapping_add(dicts::vin_char_weight(ch as char) * dicts::WEIGHTS[idx]);
94
95            idx += 1;
96        }
97
98        result
99    }
100
101    ///Calculates checksum and transforms it into corresponding digit
102    ///
103    ///Note that this is only valid for North America/Asia VINs while eruopean VINs omit it completely and has no concept of checksum digit
104    pub const fn calculate_checksum_digit(&self) -> char {
105        let checksum = self.calculate_checksum();
106        match checksum % 11 {
107            10 => 'X',
108            digit => match char::from_digit(digit, 10) {
109                Some(ch) => ch,
110                None => unreach!(),
111            }
112        }
113    }
114
115    #[inline(always)]
116    ///Calculates checksum and compares it against VIN's checksum, returning true if equal
117    pub const fn is_checksum_valid(&self) -> bool {
118        let expected = self.calculate_checksum_digit();
119        self.0.as_bytes()[8] as char == expected
120    }
121
122    const fn slice(&self, start: usize, len: usize) -> &str {
123        unsafe {
124            str::from_utf8_unchecked(
125                slice::from_raw_parts(self.0.as_ptr().add(start), len)
126            )
127        }
128    }
129
130    #[inline(always)]
131    ///Returns 3 letter World manufacturer identifier
132    ///
133    ///For some WMI may always ending with digit 9 (Check wikipedia for details)
134    pub const fn wmi(&self) -> &str {
135        self.slice(0, 3)
136    }
137
138    #[inline(always)]
139    ///Returns vehicle description section
140    ///
141    ///Per ISO3779 it is characters from 4 to 9.
142    ///But for North America/Asia character 9 usually acts as check digit.
143    pub const fn vds(&self) -> &str {
144        self.slice(3, 6)
145    }
146
147    #[inline(always)]
148    ///Returns Vehicle identifier section (Characters 10 to 17)
149    ///
150    ///For North America/Asia it includes model year and plant code/manufacturer identifier
151    ///Hence actual serial number starts from character 12, always numeric
152    ///
153    ///For Europe you can consider whole VIC as serial number.
154    pub const fn vic(&self) -> &str {
155        self.slice(9, 8)
156    }
157
158    #[inline(always)]
159    ///Returns region manufacturer, if VIN is valid
160    pub const fn manufacturer_region(&self) -> Option<Region> {
161        Region::from_wmi_region(self.0.as_bytes()[0])
162    }
163
164    #[inline(always)]
165    ///Returns manufacturer country, if VIN is valid and it is known value, otherwise 'Unknown'.
166    pub const fn manufacturer_country(&self) -> &'static str {
167        dicts::map_wmi_to_country(self.wmi())
168    }
169
170    #[inline(always)]
171    ///Returns manufacturer name, if VIN is valid and it is known value, otherwise 'Unknown'.
172    pub const fn manufacturer_name(&self) -> &'static str {
173        dicts::map_wmi_to_manufacturer(self.wmi())
174    }
175}
176
177impl fmt::Debug for Vin<'_> {
178    #[inline(always)]
179    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
180        fmt::Debug::fmt(self.0, fmt)
181    }
182}
183
184impl fmt::Display for Vin<'_> {
185    #[inline(always)]
186    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
187        fmt::Display::fmt(self.0, fmt)
188    }
189}