2,294 bytes · the Python implementation · view raw
from .validation_vin_types import VinCheck
#: Letter values from 49 CFR 565.15, indexed by letter A-Z. I, O and Q are#: never used and hold -1; the others run 1-9 in three passes (A-I, J-R, S-Z)#: with the forbidden letters' slots skipped.
_LETTER_VALUES = (1, 2, 3, 4, 5, 6, 7, 8, -1, 1, 2, 3, 4, 5, -1, 7, -1, 9, 2, 3, 4, 5, 6, 7, 8, 9)
#: Position weights; position 9 is the check digit itself and weighs nothing.
_WEIGHTS = (8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2)
def _invalid(reason: str) -> VinCheck:
return VinCheck(valid=False, normalised=None, wmi=None, check_digit_matches=None, reason=reason)
def validate_vin(value: str) -> VinCheck:
"""Check a 17-character VIN. The check digit is enforced only for North American VINs (first character 1-5), where the law requires it; elsewhere a mismatch is reported in check_digit_matches but does not make the VIN invalid, because many European and Asian VINs never carried one. Validators answer rather than raise. """ifnot isinstance(value, str):
return _invalid("empty")
# Folded by hand: str.upper() is Unicode-aware and Rust's ASCII fold is not.
vin = "".join(chr(ord(ch) - 32) if"a" <= ch <= "z"else ch for ch in value if ch != " ")
ifnot vin:
return _invalid("empty")
if any(not ("0" <= ch <= "9"or"A" <= ch <= "Z") for ch in vin):
return _invalid("bad-character")
if len(vin) != 17:
return _invalid("bad-length")
total = 0for i, ch in enumerate(vin):
if ch <= "9":
n = ord(ch) - 48else:
n = _LETTER_VALUES[ord(ch) - 65]
# I, O and Q read as 1, 0 and 0, so the standard never issues them.if n < 0:
return _invalid("forbidden-letter")
total += n * _WEIGHTS[i]
remainder = total % 11
expected = "X"if remainder == 10else str(remainder)
matches = vin[8] == expected
# WMIs starting 1-5 are North American, where 49 CFR 565 makes the check# digit mandatory; anywhere else it is optional, so only report it.ifnot matches and"1" <= vin[0] <= "5":
return _invalid("bad-check-digit")
return VinCheck(valid=True, normalised=vin, wmi=vin[:3], check_digit_matches=matches, reason=None)