Functional Weave
Code in Python

validation.vin@1.0.0

impl/typescript.ts

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.
 */
export function 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 };
}