1use std::fmt;
2
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
12pub struct Vin(String);
13
14impl Vin {
15 pub fn new(raw: impl Into<String>) -> crate::Result<Self> {
17 let s = raw.into().to_ascii_uppercase();
18 crate::wmi::validate_chars(&s)?;
19 Ok(Vin(s))
20 }
21
22 pub fn as_str(&self) -> &str {
24 &self.0
25 }
26
27 pub fn wmi(&self) -> &str {
29 &self.0[..3]
30 }
31
32 pub fn vds(&self) -> &str {
34 &self.0[3..9]
35 }
36
37 pub fn vis(&self) -> &str {
39 &self.0[9..]
40 }
41
42 pub fn check_digit(&self) -> char {
44 self.0.as_bytes()[8] as char
45 }
46
47 pub fn year_code(&self) -> char {
49 self.0.as_bytes()[9] as char
50 }
51
52 pub fn plant_code(&self) -> char {
54 self.0.as_bytes()[10] as char
55 }
56
57 pub fn region_code(&self) -> char {
59 self.0.as_bytes()[0] as char
60 }
61
62 pub fn country_code(&self) -> &str {
64 &self.0[..2]
65 }
66
67 pub fn squish_vin(&self) -> String {
70 let s = &self.0;
71 let mut out = String::with_capacity(10);
72 out.push_str(&s[..8]);
73 out.push_str(&s[9..11]);
74 out
75 }
76
77 pub fn year_candidates(&self) -> Vec<u32> {
92 let code = self.year_code();
93 let Some(base) = crate::year::year_for_code(code) else {
94 return Vec::new();
95 };
96 if code.is_ascii_digit() {
97 vec![base]
98 } else {
99 vec![base + 30, base]
100 }
101 }
102}
103
104impl fmt::Display for Vin {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 f.write_str(&self.0)
107 }
108}
109
110#[derive(Debug, Clone, Default, PartialEq)]
115#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
116pub struct Vehicle {
117 pub vin: String,
119 pub wmi: String,
121 pub make: Option<String>,
123 pub model: Option<String>,
125 pub series: Option<String>,
127 pub trim: Option<String>,
129 pub model_year: Option<u32>,
131 pub body_class: Option<BodyClass>,
133 pub fuel_primary: Option<FuelType>,
135 pub fuel_secondary: Option<FuelType>,
137 pub doors: Option<u8>,
139 pub engine_cylinders: Option<u8>,
141 pub engine_model: Option<String>,
143 pub engine_configuration: Option<String>,
145 pub engine_manufacturer: Option<String>,
147 pub displacement_l: Option<f32>,
149 pub turbo: Option<bool>,
151 pub drive_type: Option<String>,
153 pub transmission: Option<String>,
155 pub battery_type: Option<String>,
157 pub charger_level: Option<String>,
159 pub ev_drive_unit: Option<String>,
161 pub brake_system: Option<String>,
163 pub gvwr: Option<String>,
165 pub plant_country: Option<String>,
167 pub plant_city: Option<String>,
169 pub plant_state: Option<String>,
171 pub manufacturer: Option<String>,
173 pub region: Option<String>,
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
179#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
180#[allow(missing_docs)]
181pub enum BodyClass {
182 Sedan,
183 Coupe,
184 Hatchback,
185 Wagon,
186 Convertible,
187 Suv,
188 Crossover,
189 Pickup,
190 Van,
191 Minivan,
192 Bus,
193 Truck,
194 Motorcycle,
195 Trailer,
196 Incomplete,
197 Other,
198}
199
200impl BodyClass {
201 pub fn parse(s: &str) -> Self {
203 let lc = s.to_ascii_lowercase();
204 match lc.as_str() {
205 x if x.contains("sedan") => BodyClass::Sedan,
206 x if x.contains("coupe") => BodyClass::Coupe,
207 x if x.contains("hatchback") => BodyClass::Hatchback,
208 x if x.contains("wagon") => BodyClass::Wagon,
209 x if x.contains("convertible") || x.contains("cabrio") || x.contains("roadster") => {
210 BodyClass::Convertible
211 }
212 x if x.contains("crossover") || x.contains("cuv") => BodyClass::Crossover,
213 x if x.contains("sport utility") || x.contains("suv") => BodyClass::Suv,
214 x if x.contains("pickup") => BodyClass::Pickup,
215 x if x.contains("minivan") => BodyClass::Minivan,
216 x if x.contains("van") => BodyClass::Van,
217 x if x.contains("bus") => BodyClass::Bus,
218 x if x.contains("truck") => BodyClass::Truck,
219 x if x.contains("motorcycle") || x.contains("motor") => BodyClass::Motorcycle,
220 x if x.contains("trailer") => BodyClass::Trailer,
221 x if x.contains("incomplete") => BodyClass::Incomplete,
222 _ => BodyClass::Other,
223 }
224 }
225}
226
227#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
229#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
230#[allow(missing_docs)]
231pub enum FuelType {
232 Gasoline,
233 Diesel,
234 Electric,
235 Hybrid,
236 PluginHybrid,
237 Ethanol,
238 FlexFuel,
239 Cng,
240 Lng,
241 Lpg,
242 Hydrogen,
243 FuelCell,
244 Methanol,
245 NaturalGas,
246 Other,
247}
248
249impl FuelType {
250 pub fn parse(s: &str) -> Self {
252 let lc = s.to_ascii_lowercase();
253 match lc.as_str() {
254 x if x.contains("gasoline") => FuelType::Gasoline,
255 x if x.contains("diesel") => FuelType::Diesel,
256 x if x.contains("plug") => FuelType::PluginHybrid,
257 x if x.contains("hybrid") => FuelType::Hybrid,
258 x if x.contains("methanol") || x.contains("m85") => FuelType::Methanol,
259 x if x.contains("e85") || x.contains("ethanol") => FuelType::Ethanol,
260 x if x.contains("flex") || x.contains("ffv") => FuelType::FlexFuel,
261 x if x.contains("cng") || x.contains("compressed natural") => FuelType::Cng,
262 x if x.contains("lng") || x.contains("liquefied natural") => FuelType::Lng,
263 x if x.contains("lpg") || x.contains("propane") => FuelType::Lpg,
264 x if x.contains("fuel cell") => FuelType::FuelCell,
265 x if x.contains("hydrogen") => FuelType::Hydrogen,
266 x if x.contains("electric") => FuelType::Electric,
267 x if x.contains("natural gas") => FuelType::NaturalGas,
268 _ => FuelType::Other,
269 }
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn vin_uppercases_input() {
279 let v = Vin::new("1hgcm82633a004352").unwrap();
280 assert_eq!(v.as_str(), "1HGCM82633A004352");
281 }
282
283 #[test]
284 fn vin_section_accessors() {
285 let v = Vin::new("1HGCM82633A004352").unwrap();
286 assert_eq!(v.wmi(), "1HG");
287 assert_eq!(v.vds(), "CM8263");
288 assert_eq!(v.vis(), "3A004352");
289 assert_eq!(v.check_digit(), '3');
290 assert_eq!(v.year_code(), '3');
291 assert_eq!(v.plant_code(), 'A');
292 }
293
294 #[test]
295 fn vin_display_returns_canonical() {
296 let v = Vin::new("1hgcm82633a004352").unwrap();
297 assert_eq!(format!("{}", v), "1HGCM82633A004352");
298 }
299
300 #[test]
301 fn body_class_full_coverage() {
302 assert_eq!(BodyClass::parse("4-Door Sedan"), BodyClass::Sedan);
303 assert_eq!(BodyClass::parse("2-Door Coupe"), BodyClass::Coupe);
304 assert_eq!(BodyClass::parse("Hatchback"), BodyClass::Hatchback);
305 assert_eq!(BodyClass::parse("Station Wagon"), BodyClass::Wagon);
306 assert_eq!(BodyClass::parse("Convertible"), BodyClass::Convertible);
307 assert_eq!(BodyClass::parse("2-Door Cabriolet"), BodyClass::Convertible);
308 assert_eq!(BodyClass::parse("Roadster"), BodyClass::Convertible);
309 assert_eq!(
310 BodyClass::parse("Crossover Utility Vehicle (CUV)"),
311 BodyClass::Crossover
312 );
313 assert_eq!(
314 BodyClass::parse("Sport Utility Vehicle (SUV)"),
315 BodyClass::Suv
316 );
317 assert_eq!(BodyClass::parse("Crew Cab Pickup"), BodyClass::Pickup);
318 assert_eq!(BodyClass::parse("Cargo Van"), BodyClass::Van);
319 assert_eq!(BodyClass::parse("Minivan"), BodyClass::Minivan);
320 assert_eq!(BodyClass::parse("School Bus"), BodyClass::Bus);
321 assert_eq!(BodyClass::parse("Truck"), BodyClass::Truck);
322 assert_eq!(BodyClass::parse("Motorcycle"), BodyClass::Motorcycle);
323 assert_eq!(BodyClass::parse("Trailer"), BodyClass::Trailer);
324 assert_eq!(
325 BodyClass::parse("Incomplete Vehicle"),
326 BodyClass::Incomplete
327 );
328 assert_eq!(BodyClass::parse("Unknown blob"), BodyClass::Other);
329 }
330
331 #[test]
332 fn fuel_type_full_coverage() {
333 assert_eq!(FuelType::parse("Gasoline"), FuelType::Gasoline);
334 assert_eq!(FuelType::parse("Diesel"), FuelType::Diesel);
335 assert_eq!(FuelType::parse("Electric"), FuelType::Electric);
336 assert_eq!(FuelType::parse("Plug-in Hybrid"), FuelType::PluginHybrid);
337 assert_eq!(FuelType::parse("Hybrid"), FuelType::Hybrid);
338 assert_eq!(FuelType::parse("E85"), FuelType::Ethanol);
339 assert_eq!(FuelType::parse("Ethanol (E85)"), FuelType::Ethanol);
340 assert_eq!(
341 FuelType::parse("Flexible Fuel Vehicle (FFV)"),
342 FuelType::FlexFuel
343 );
344 assert_eq!(
345 FuelType::parse("Compressed Natural Gas (CNG)"),
346 FuelType::Cng
347 );
348 assert_eq!(
349 FuelType::parse("Liquefied Natural Gas (LNG)"),
350 FuelType::Lng
351 );
352 assert_eq!(
353 FuelType::parse("Liquefied Petroleum Gas (LPG)"),
354 FuelType::Lpg
355 );
356 assert_eq!(
357 FuelType::parse("Compressed Hydrogen/Hydrogen"),
358 FuelType::Hydrogen
359 );
360 assert_eq!(FuelType::parse("Fuel Cell"), FuelType::FuelCell);
361 assert_eq!(FuelType::parse("Methanol (M85)"), FuelType::Methanol);
362 assert_eq!(FuelType::parse("Unknown"), FuelType::Other);
363 }
364}