From 45223dfd2b5ae2c9b73c0c30040de0fc599f2f9a Mon Sep 17 00:00:00 2001 From: tianrking Date: Fri, 17 Jul 2026 02:37:51 +0800 Subject: [PATCH 1/3] fix(vehicle): calculate VIN check digit --- src/modules/vehicle/module.ts | 47 ++++++++++++++++++- .../__snapshots__/vehicle.spec.ts.snap | 6 +-- test/modules/vehicle.spec.ts | 47 +++++++++++++++++++ 3 files changed, 96 insertions(+), 4 deletions(-) diff --git a/src/modules/vehicle/module.ts b/src/modules/vehicle/module.ts index d032465a42d..97b958af1d9 100644 --- a/src/modules/vehicle/module.ts +++ b/src/modules/vehicle/module.ts @@ -1,5 +1,48 @@ import { ModuleBase } from '../../internal/module-base'; +const vinWeights = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2]; + +const vinTransliteration: Record = { + 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, +}; + +/** + * Calculates a Vehicle Identification Number (VIN) check digit. + * + * @param vin The VIN to calculate the check digit for. + */ +function vinCheckDigit(vin: string): string { + let checksum = 0; + for (const [index, character] of vin.split('').entries()) { + const value = vinTransliteration[character] ?? Number(character); + checksum += value * vinWeights[index]; + } + + return checksum % 11 === 10 ? 'X' : String(checksum % 11); +} + /** * Module to generate vehicle related entries. * @@ -84,7 +127,7 @@ export class VehicleModule extends ModuleBase { */ vin(): string { const exclude = ['o', 'i', 'q', 'O', 'I', 'Q']; - return `${this.faker.string.alphanumeric({ + const vin = `${this.faker.string.alphanumeric({ length: 10, casing: 'upper', exclude, @@ -97,6 +140,8 @@ export class VehicleModule extends ModuleBase { casing: 'upper', exclude, })}${this.faker.string.numeric({ length: 5, allowLeadingZeros: true })}`; + + return `${vin.slice(0, 8)}${vinCheckDigit(vin)}${vin.slice(9)}`; } /** diff --git a/test/modules/__snapshots__/vehicle.spec.ts.snap b/test/modules/__snapshots__/vehicle.spec.ts.snap index 81f91f79683..260b49919fa 100644 --- a/test/modules/__snapshots__/vehicle.spec.ts.snap +++ b/test/modules/__snapshots__/vehicle.spec.ts.snap @@ -14,7 +14,7 @@ exports[`vehicle > 42 > type 1`] = `"Extended Cab Pickup"`; exports[`vehicle > 42 > vehicle 1`] = `"Jeep Wrangler"`; -exports[`vehicle > 42 > vin 1`] = `"CYRK551VKPAZ82113"`; +exports[`vehicle > 42 > vin 1`] = `"CYRK551V7PAZ82113"`; exports[`vehicle > 42 > vrm 1`] = `"JY75EEB"`; @@ -32,7 +32,7 @@ exports[`vehicle > 1211 > type 1`] = `"Wagon"`; exports[`vehicle > 1211 > vehicle 1`] = `"Toyota Spyder"`; -exports[`vehicle > 1211 > vin 1`] = `"XW7ZNNSBNTUM84882"`; +exports[`vehicle > 1211 > vin 1`] = `"XW7ZNNSB8TUM84882"`; exports[`vehicle > 1211 > vrm 1`] = `"YX29RRT"`; @@ -50,6 +50,6 @@ exports[`vehicle > 1337 > type 1`] = `"Coupe"`; exports[`vehicle > 1337 > vehicle 1`] = `"Fiat Aventador"`; -exports[`vehicle > 1337 > vin 1`] = `"859FAH8ZR3JL19477"`; +exports[`vehicle > 1337 > vin 1`] = `"859FAH8Z23JL19477"`; exports[`vehicle > 1337 > vrm 1`] = `"GE24ING"`; diff --git a/test/modules/vehicle.spec.ts b/test/modules/vehicle.spec.ts index c189acb6095..4c1f2147228 100644 --- a/test/modules/vehicle.spec.ts +++ b/test/modules/vehicle.spec.ts @@ -5,6 +5,44 @@ import { times } from '../support/times'; const NON_SEEDED_BASED_RUN = 5; +const vinWeights = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2]; + +const vinTransliteration: Record = { + 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, +}; + +function calculateVinCheckDigit(vin: string): string { + let checksum = 0; + for (const [index, character] of vin.split('').entries()) { + const value = vinTransliteration[character] ?? Number(character); + checksum += value * vinWeights[index]; + } + + return checksum % 11 === 10 ? 'X' : String(checksum % 11); +} + describe('vehicle', () => { seededTests(faker, 'vehicle', (t) => { t.itEach( @@ -102,6 +140,15 @@ describe('vehicle', () => { /^([A-HJ-NPR-Z0-9]{10}[A-HJ-NPR-Z0-9]{1}[A-HJ-NPR-Z0-9]{1}\d{5})$/ ); }); + + it('should return a VIN with a valid check digit', () => { + expect(calculateVinCheckDigit('1M8GDM9AXKP042788')).toBe('X'); + + for (let index = 0; index < 100; index++) { + const vin = faker.vehicle.vin(); + expect(vin[8]).toBe(calculateVinCheckDigit(vin)); + } + }); }); describe('vrm()', () => { From dfa1b332aeee39d49fb5c809bf273e6285a89564 Mon Sep 17 00:00:00 2001 From: tianrking <10758833+tianrking@users.noreply.github.com> Date: Fri, 17 Jul 2026 10:19:45 +0800 Subject: [PATCH 2/3] docs(vehicle): cite VIN check digit source --- src/modules/vehicle/module.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/modules/vehicle/module.ts b/src/modules/vehicle/module.ts index 97b958af1d9..9e81239468d 100644 --- a/src/modules/vehicle/module.ts +++ b/src/modules/vehicle/module.ts @@ -1,5 +1,8 @@ import { ModuleBase } from '../../internal/module-base'; +// NHTSA 49 CFR ยง 565.15(c), Tables III and IV define the transliteration +// values and position weights used here: +// https://www.govinfo.gov/content/pkg/CFR-2025-title49-vol6/pdf/CFR-2025-title49-vol6-sec565-15.pdf const vinWeights = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2]; const vinTransliteration: Record = { From e2a8856da17a8498c31fbb21ce136db879dc612f Mon Sep 17 00:00:00 2001 From: tianrking <10758833+tianrking@users.noreply.github.com> Date: Sun, 26 Jul 2026 22:39:34 +0800 Subject: [PATCH 3/3] test(vehicle): address VIN review feedback --- src/modules/vehicle/module.ts | 2 +- test/modules/vehicle.spec.ts | 60 ++++++++++------------------------- 2 files changed, 17 insertions(+), 45 deletions(-) diff --git a/src/modules/vehicle/module.ts b/src/modules/vehicle/module.ts index 9e81239468d..64bd6467c3e 100644 --- a/src/modules/vehicle/module.ts +++ b/src/modules/vehicle/module.ts @@ -36,7 +36,7 @@ const vinTransliteration: Record = { * * @param vin The VIN to calculate the check digit for. */ -function vinCheckDigit(vin: string): string { +export function vinCheckDigit(vin: string): string { let checksum = 0; for (const [index, character] of vin.split('').entries()) { const value = vinTransliteration[character] ?? Number(character); diff --git a/test/modules/vehicle.spec.ts b/test/modules/vehicle.spec.ts index 4c1f2147228..e21b12d9321 100644 --- a/test/modules/vehicle.spec.ts +++ b/test/modules/vehicle.spec.ts @@ -1,49 +1,24 @@ import { describe, expect, it } from 'vitest'; import { faker } from '../../src'; +import { vinCheckDigit } from '../../src/modules/vehicle/module'; import { seededTests } from '../support/seeded-runs'; import { times } from '../support/times'; const NON_SEEDED_BASED_RUN = 5; -const vinWeights = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2]; - -const vinTransliteration: Record = { - 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, -}; - -function calculateVinCheckDigit(vin: string): string { - let checksum = 0; - for (const [index, character] of vin.split('').entries()) { - const value = vinTransliteration[character] ?? Number(character); - checksum += value * vinWeights[index]; - } - - return checksum % 11 === 10 ? 'X' : String(checksum % 11); -} - describe('vehicle', () => { + describe('vinCheckDigit()', () => { + it.each([ + ['1M8GDM9AXKP042788', 'X'], + ['1HGCM82633A004352', '3'], + ['CYRK551V7PAZ82113', '7'], + ['XW7ZNNSB8TUM84882', '8'], + ['859FAH8Z23JL19477', '2'], + ])('calculates the check digit for %s', (vin, expected) => { + expect(vinCheckDigit(vin)).toBe(expected); + }); + }); + seededTests(faker, 'vehicle', (t) => { t.itEach( 'vehicle', @@ -141,13 +116,10 @@ describe('vehicle', () => { ); }); - it('should return a VIN with a valid check digit', () => { - expect(calculateVinCheckDigit('1M8GDM9AXKP042788')).toBe('X'); + it('should add the check digit at the correct position', () => { + const vin = faker.vehicle.vin(); - for (let index = 0; index < 100; index++) { - const vin = faker.vehicle.vin(); - expect(vin[8]).toBe(calculateVinCheckDigit(vin)); - } + expect(vin[8]).toBe(vinCheckDigit(vin)); }); });