use std::fmt;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Vin(String);
impl Vin {
pub fn new(raw: impl Into<String>) -> crate::Result<Self> {
let s = raw.into().to_ascii_uppercase();
crate::wmi::validate_chars(&s)?;
Ok(Vin(s))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn wmi(&self) -> &str {
&self.0[..3]
}
pub fn vds(&self) -> &str {
&self.0[3..9]
}
pub fn vis(&self) -> &str {
&self.0[9..]
}
pub fn check_digit(&self) -> char {
self.0.as_bytes()[8] as char
}
pub fn year_code(&self) -> char {
self.0.as_bytes()[9] as char
}
pub fn plant_code(&self) -> char {
self.0.as_bytes()[10] as char
}
pub fn region_code(&self) -> char {
self.0.as_bytes()[0] as char
}
pub fn country_code(&self) -> &str {
&self.0[..2]
}
pub fn squish_vin(&self) -> String {
let s = &self.0;
let mut out = String::with_capacity(10);
out.push_str(&s[..8]);
out.push_str(&s[9..11]);
out
}
pub fn year_candidates(&self) -> Vec<u32> {
let code = self.year_code();
let Some(base) = crate::year::year_for_code(code) else {
return Vec::new();
};
if code.is_ascii_digit() {
vec![base]
} else {
vec![base + 30, base]
}
}
}
impl fmt::Display for Vin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Vehicle {
pub vin: String,
pub wmi: String,
pub make: Option<String>,
pub model: Option<String>,
pub series: Option<String>,
pub trim: Option<String>,
pub model_year: Option<u32>,
pub body_class: Option<BodyClass>,
pub fuel_primary: Option<FuelType>,
pub fuel_secondary: Option<FuelType>,
pub doors: Option<u8>,
pub engine_cylinders: Option<u8>,
pub engine_model: Option<String>,
pub engine_configuration: Option<String>,
pub engine_manufacturer: Option<String>,
pub displacement_l: Option<f32>,
pub turbo: Option<bool>,
pub drive_type: Option<String>,
pub transmission: Option<String>,
pub battery_type: Option<String>,
pub charger_level: Option<String>,
pub ev_drive_unit: Option<String>,
pub brake_system: Option<String>,
pub gvwr: Option<String>,
pub plant_country: Option<String>,
pub plant_city: Option<String>,
pub plant_state: Option<String>,
pub manufacturer: Option<String>,
pub region: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[allow(missing_docs)]
pub enum BodyClass {
Sedan,
Coupe,
Hatchback,
Wagon,
Convertible,
Suv,
Crossover,
Pickup,
Van,
Minivan,
Bus,
Truck,
Motorcycle,
Trailer,
Incomplete,
Other,
}
impl BodyClass {
pub fn parse(s: &str) -> Self {
let lc = s.to_ascii_lowercase();
match lc.as_str() {
x if x.contains("sedan") => BodyClass::Sedan,
x if x.contains("coupe") => BodyClass::Coupe,
x if x.contains("hatchback") => BodyClass::Hatchback,
x if x.contains("wagon") => BodyClass::Wagon,
x if x.contains("convertible") || x.contains("cabrio") || x.contains("roadster") => {
BodyClass::Convertible
}
x if x.contains("crossover") || x.contains("cuv") => BodyClass::Crossover,
x if x.contains("sport utility") || x.contains("suv") => BodyClass::Suv,
x if x.contains("pickup") => BodyClass::Pickup,
x if x.contains("minivan") => BodyClass::Minivan,
x if x.contains("van") => BodyClass::Van,
x if x.contains("bus") => BodyClass::Bus,
x if x.contains("truck") => BodyClass::Truck,
x if x.contains("motorcycle") || x.contains("motor") => BodyClass::Motorcycle,
x if x.contains("trailer") => BodyClass::Trailer,
x if x.contains("incomplete") => BodyClass::Incomplete,
_ => BodyClass::Other,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[allow(missing_docs)]
pub enum FuelType {
Gasoline,
Diesel,
Electric,
Hybrid,
PluginHybrid,
Ethanol,
FlexFuel,
Cng,
Lng,
Lpg,
Hydrogen,
FuelCell,
Methanol,
NaturalGas,
Other,
}
impl FuelType {
pub fn parse(s: &str) -> Self {
let lc = s.to_ascii_lowercase();
match lc.as_str() {
x if x.contains("gasoline") => FuelType::Gasoline,
x if x.contains("diesel") => FuelType::Diesel,
x if x.contains("plug") => FuelType::PluginHybrid,
x if x.contains("hybrid") => FuelType::Hybrid,
x if x.contains("methanol") || x.contains("m85") => FuelType::Methanol,
x if x.contains("e85") || x.contains("ethanol") => FuelType::Ethanol,
x if x.contains("flex") || x.contains("ffv") => FuelType::FlexFuel,
x if x.contains("cng") || x.contains("compressed natural") => FuelType::Cng,
x if x.contains("lng") || x.contains("liquefied natural") => FuelType::Lng,
x if x.contains("lpg") || x.contains("propane") => FuelType::Lpg,
x if x.contains("fuel cell") => FuelType::FuelCell,
x if x.contains("hydrogen") => FuelType::Hydrogen,
x if x.contains("electric") => FuelType::Electric,
x if x.contains("natural gas") => FuelType::NaturalGas,
_ => FuelType::Other,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vin_uppercases_input() {
let v = Vin::new("1hgcm82633a004352").unwrap();
assert_eq!(v.as_str(), "1HGCM82633A004352");
}
#[test]
fn vin_section_accessors() {
let v = Vin::new("1HGCM82633A004352").unwrap();
assert_eq!(v.wmi(), "1HG");
assert_eq!(v.vds(), "CM8263");
assert_eq!(v.vis(), "3A004352");
assert_eq!(v.check_digit(), '3');
assert_eq!(v.year_code(), '3');
assert_eq!(v.plant_code(), 'A');
}
#[test]
fn vin_display_returns_canonical() {
let v = Vin::new("1hgcm82633a004352").unwrap();
assert_eq!(format!("{}", v), "1HGCM82633A004352");
}
#[test]
fn body_class_full_coverage() {
assert_eq!(BodyClass::parse("4-Door Sedan"), BodyClass::Sedan);
assert_eq!(BodyClass::parse("2-Door Coupe"), BodyClass::Coupe);
assert_eq!(BodyClass::parse("Hatchback"), BodyClass::Hatchback);
assert_eq!(BodyClass::parse("Station Wagon"), BodyClass::Wagon);
assert_eq!(BodyClass::parse("Convertible"), BodyClass::Convertible);
assert_eq!(BodyClass::parse("2-Door Cabriolet"), BodyClass::Convertible);
assert_eq!(BodyClass::parse("Roadster"), BodyClass::Convertible);
assert_eq!(
BodyClass::parse("Crossover Utility Vehicle (CUV)"),
BodyClass::Crossover
);
assert_eq!(
BodyClass::parse("Sport Utility Vehicle (SUV)"),
BodyClass::Suv
);
assert_eq!(BodyClass::parse("Crew Cab Pickup"), BodyClass::Pickup);
assert_eq!(BodyClass::parse("Cargo Van"), BodyClass::Van);
assert_eq!(BodyClass::parse("Minivan"), BodyClass::Minivan);
assert_eq!(BodyClass::parse("School Bus"), BodyClass::Bus);
assert_eq!(BodyClass::parse("Truck"), BodyClass::Truck);
assert_eq!(BodyClass::parse("Motorcycle"), BodyClass::Motorcycle);
assert_eq!(BodyClass::parse("Trailer"), BodyClass::Trailer);
assert_eq!(
BodyClass::parse("Incomplete Vehicle"),
BodyClass::Incomplete
);
assert_eq!(BodyClass::parse("Unknown blob"), BodyClass::Other);
}
#[test]
fn fuel_type_full_coverage() {
assert_eq!(FuelType::parse("Gasoline"), FuelType::Gasoline);
assert_eq!(FuelType::parse("Diesel"), FuelType::Diesel);
assert_eq!(FuelType::parse("Electric"), FuelType::Electric);
assert_eq!(FuelType::parse("Plug-in Hybrid"), FuelType::PluginHybrid);
assert_eq!(FuelType::parse("Hybrid"), FuelType::Hybrid);
assert_eq!(FuelType::parse("E85"), FuelType::Ethanol);
assert_eq!(FuelType::parse("Ethanol (E85)"), FuelType::Ethanol);
assert_eq!(
FuelType::parse("Flexible Fuel Vehicle (FFV)"),
FuelType::FlexFuel
);
assert_eq!(
FuelType::parse("Compressed Natural Gas (CNG)"),
FuelType::Cng
);
assert_eq!(
FuelType::parse("Liquefied Natural Gas (LNG)"),
FuelType::Lng
);
assert_eq!(
FuelType::parse("Liquefied Petroleum Gas (LPG)"),
FuelType::Lpg
);
assert_eq!(
FuelType::parse("Compressed Hydrogen/Hydrogen"),
FuelType::Hydrogen
);
assert_eq!(FuelType::parse("Fuel Cell"), FuelType::FuelCell);
assert_eq!(FuelType::parse("Methanol (M85)"), FuelType::Methanol);
assert_eq!(FuelType::parse("Unknown"), FuelType::Other);
}
}