2,429 bytes · the TypeScript implementation · view raw
import { type VinCheck } from"./validation_vin_types.ts";
/** * 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. */const 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. */const WEIGHTS = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2];
function invalid(reason: string): VinCheck {
return { valid: false, normalised: null, wmi: null, checkDigitMatches: null, reason };
}
/** * 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 * checkDigitMatches but does not make the VIN invalid, because many European * and Asian VINs never carried one. Validators answer rather than throw. */exportfunction validateVin(value: string): VinCheck {
if (typeof value !== "string") return invalid("empty");
let vin = "";
for (const ch of value) {
if (ch === " ") continue;
// Folded by hand: toUpperCase() is Unicode-aware and Rust's ASCII fold is not.
vin += ch >= "a" && ch <= "z" ? String.fromCharCode(ch.charCodeAt(0) - 32) : ch;
}
if (vin.length === 0) return invalid("empty");
for (const ch of vin) {
if (!((ch >= "0" && ch <= "9") || (ch >= "A" && ch <= "Z"))) return invalid("bad-character");
}
if (vin.length !== 17) return invalid("bad-length");
let total = 0;
for (let i = 0; i < 17; i++) {
const code = vin.charCodeAt(i);
let n: number;
if (code <= 57) n = code - 48;
else {
n = LETTER_VALUES[code - 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];
}
const remainder = total % 11;
const expected = remainder === 10 ? "X" : String(remainder);
const 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 (!matches && vin[0] >= "1" && vin[0] <= "5") return invalid("bad-check-digit");
return { valid: true, normalised: vin, wmi: vin.slice(0, 3), checkDigitMatches: matches, reason: null };
}