Functional Weave
Code in Python

validation.vin@1.0.0

impl/python.py

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.
    """
    if not 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 != " ")
    if not 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 = 0
    for i, ch in enumerate(vin):
        if ch <= "9":
            n = ord(ch) - 48
        else:
            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 == 10 else 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.
    if not 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)