diff --git a/src/modules/vehicle/module.ts b/src/modules/vehicle/module.ts index d032465a42d..64bd6467c3e 100644 --- a/src/modules/vehicle/module.ts +++ b/src/modules/vehicle/module.ts @@ -1,5 +1,51 @@ 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 = { + 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. + */ +export 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 +130,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 +143,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..e21b12d9321 100644 --- a/test/modules/vehicle.spec.ts +++ b/test/modules/vehicle.spec.ts @@ -1,11 +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; 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', @@ -102,6 +115,12 @@ describe('vehicle', () => { /^([A-HJ-NPR-Z0-9]{10}[A-HJ-NPR-Z0-9]{1}[A-HJ-NPR-Z0-9]{1}\d{5})$/ ); }); + + it('should add the check digit at the correct position', () => { + const vin = faker.vehicle.vin(); + + expect(vin[8]).toBe(vinCheckDigit(vin)); + }); }); describe('vrm()', () => {