Important: This documentation applies to v2 of this package.
For v3 docs see vpic.shaggytech.com

Source

utils/isValidVin.ts

/**
 * @module utils/isValidVin
 * @category Utils
 */

/*
 * There will need to be some way to translate vin digits that are alphabetic
 * into their number value in the VIN algorithm transliteration table.
 * Later, during the creation of the checksum variable, those digits will be
 * multiplied against their corresponding weight (by index) in the WEIGHTS_ARRAY.
 * This transliteration table is a key part of the VIN validation algorithm.
 */
const TRANSLITERATION_TABLE: Record<string, number> = {
  A: 1,
  B: 2,
  C: 3,
  D: 4,
  E: 5,
  F: 6,
  G: 7,
  H: 8,
  J: 1,
  K: 2,
  L: 3,
  M: 4,
  N: 5,
  P: 7,
  R: 9,
  S: 2,
  T: 3,
  U: 4,
  V: 5,
  W: 6,
  X: 7,
  Y: 8,
  Z: 9,
};

/*
 * Later, during the creation of the 'checksum' variable, these weights will be
 * multiplied by the value of their mirrored index vin digits.
 * The array index of each weight corresponds to the same index of each
 * digit in the 'vin'.
 */
const WEIGHTS_ARRAY: number[] = [
  8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2,
];

/**
 * Provides **offline** validation of Vehicle Identification Numbers (VINs) using the
 * [VIN Check Algorithm](https://en.wikibooks.org/wiki/Vehicle_Identification_Numbers_(VIN_codes)/Check_digit).
 *
 * @param {string} vin - Vehicle Identification Number.
 * @returns {boolean} True for a valid VIN, false for an invalid VIN.
 *
 * @example <caption>When loaded from the browser via html script tags</caption>
 * // <script type="text/javascript" src="https://www.npmjs.com/package/@shaggytools/nhtsa-api-wrapper"></script>
 * const isValid = NHTSA.isValidVin('3VWD07AJ5EM388202')
 * console.log(isValid) // true
 *
 * @example <caption>When loaded as a module</caption>
 * import { isValidVin } from '@shaggytools/nhtsa-api-wrapper'
 * const isValid = isValidVin('3VWD07AJ5EM388202')
 * console.log(isValid) // true
 *
 */
export function isValidVin(vin: string): boolean {
  /* A valid VIN must be a string and is always exactly 17 digits */
  if (typeof vin !== 'string' || vin.length != 17) {
    return false;
  }

  /* Normalize the vin to all uppercase letters */
  vin = vin.toUpperCase();

  /* split the vin digits into an array */
  const vinArray: string[] = vin.split('');

  /* checkDigit will be tested against the checkSum later */
  const checkDigit: string = vinArray[8];

  /*
   * In a valid VIN, the checkDigit can either be:
   * a number, 0-9 inclusive OR the character 'X'
   */
  if (isNaN(parseInt(checkDigit)) && checkDigit !== 'X') {
    return false;
  }

  /*
   * The checkValue must be a digit and 'X' is the only valid alphabetic check value.
   * As per the algorithm, a checkDigit of 'X' is equal to a checkValue of `10` and needs
   * to be converted as such.
   */
  const checkValue: number = checkDigit === 'X' ? 10 : parseInt(checkDigit);

  /*
   * Maps the vinArray and converts any values (digits) that are alphabetic,
   * into numbers, using the TRANSLITERATION_TABLE.
   * Then these numbers are multiplied against their corresponding weight
   * in the WEIGHTS_ARRAY, matched by index position.
   * All 17 of those digitValues are then added together and divided by 11.
   * The remainder, or % modulo, of that division will be the final 'checksum'.
   */
  const checksum: number =
    vinArray
      .map((digit: string, index: number) => {
        let digitValue: number;
        /* Use the transliteration table to convert any Not a Number(NaN) values to numbers */
        isNaN(parseInt(digit))
          ? (digitValue = TRANSLITERATION_TABLE[digit])
          : (digitValue = parseInt(digit));

        /* Convert the digitValue to a weighted number corresponding to it's position, by index, in the weightsArray. */
        const weight: number = WEIGHTS_ARRAY[index];

        /* The final step for each digit is to multiply the digit by it's corresponding weight */
        return digitValue * weight;
      })
      /* Finally, get the sum of all digits and divide by 11, the remainder of that operation is the checksum */
      .reduce((acc, currValue) => acc + currValue, 0) % 11;

  /*
   * The checksum is compared against the checkValue we set earlier (the 9th digit of the VIN)
   * As per the algorithm, if they are equal to each other, then the VIN must be valid and
   * we return true, otherwise the VIN is invalid and we return false.
   */
  return checksum === checkValue;
}