Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion src/modules/vehicle/module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,51 @@
import { ModuleBase } from '../../internal/module-base';
Comment thread
ST-DDT marked this conversation as resolved.

// 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be stupid, but this link does not lead to any valid PDF for me 🤷‍♀️

const vinWeights = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2];
Comment thread
ST-DDT marked this conversation as resolved.

const vinTransliteration: 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,
};

/**
* 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);
}
Comment thread
ST-DDT marked this conversation as resolved.

/**
* Module to generate vehicle related entries.
*
Expand Down Expand Up @@ -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,
Expand All @@ -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)}`;
}

/**
Expand Down
6 changes: 3 additions & 3 deletions test/modules/__snapshots__/vehicle.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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"`;

Expand All @@ -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"`;

Expand All @@ -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"`;
19 changes: 19 additions & 0 deletions test/modules/vehicle.spec.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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()', () => {
Expand Down