Skip to content

Commit 57b2d78

Browse files
Dhanush-K-GowdamatthewmayerxDivisionByZerox
authored
feat: Add support for UPC (#3648)
Co-authored-by: Matt Mayer <[email protected]> Co-authored-by: DivisionByZero <[email protected]>
1 parent bfb1bdb commit 57b2d78

File tree

5 files changed

+290
-1
lines changed

5 files changed

+290
-1
lines changed

src/modules/commerce/index.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { FakerError } from '../../errors/faker-error';
22
import { ModuleBase } from '../../internal/module-base';
3+
import { calculateUPCCheckDigit } from './upc-check-digit';
34

45
// Source for official prefixes: https://www.isbn-international.org/range_file_generation
56
const ISBN_LENGTH_RULES: Record<
@@ -84,6 +85,8 @@ const ISBN_LENGTH_RULES: Record<
8485
* For a department in a shop or product category, use [`department()`](https://fakerjs.dev/api/commerce.html#department).
8586
*
8687
* You can also create a price using [`price()`](https://fakerjs.dev/api/commerce.html#price).
88+
*
89+
* To work with product identifiers, generate an ISBN via [`isbn()`](https://fakerjs.dev/api/commerce.html#isbn) or a 12‑digit UPC via [`upc()`](https://fakerjs.dev/api/commerce.html#upc).
8790
*/
8891
export class CommerceModule extends ModuleBase {
8992
/**
@@ -349,4 +352,51 @@ export class CommerceModule extends ModuleBase {
349352

350353
return data.join(separator);
351354
}
355+
356+
/**
357+
* Returns a valid [UPC‑A](https://en.wikipedia.org/wiki/Universal_Product_Code) (12 digits).
358+
*
359+
* When a `prefix` is provided, it is padded with random digits so that the body
360+
* has 11 digits. The 12th digit (check digit) is computed using the Modulo 10 algorithm.
361+
*
362+
* @param options An options object.
363+
* @param options.prefix Optional numeric prefix for the UPC body (0–11 digits).
364+
*
365+
* @returns A 12‑digit UPC‑A string.
366+
*
367+
* @throws {FakerError} If `prefix` contains non-digit characters or more than 11 digits.
368+
*
369+
* @example
370+
* faker.commerce.upc() // '036000291452'
371+
* faker.commerce.upc({ prefix: '01234' }) // '012345678905'
372+
*
373+
* @since 10.2.0
374+
*/
375+
upc(
376+
options: {
377+
/**
378+
* Optional numeric prefix for the UPC body (0–11 digits).
379+
*/
380+
prefix?: string;
381+
} = {}
382+
): string {
383+
const { prefix = '' } = options;
384+
if (prefix && /\D/.test(prefix)) {
385+
throw new FakerError('Prefix must contain only numeric digits');
386+
}
387+
388+
if (prefix.length > 11) {
389+
throw new FakerError('Prefix must be at most 11 numeric digits');
390+
}
391+
392+
const remaining = 11 - prefix.length;
393+
const rand = this.faker.string.numeric({
394+
length: remaining,
395+
allowLeadingZeros: true,
396+
});
397+
398+
const body = `${prefix}${rand}`; // 11 digits
399+
const check = calculateUPCCheckDigit(body);
400+
return `${body}${check}`; // 12-digit UPC-A
401+
}
352402
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { FakerError } from '../../errors/faker-error';
2+
3+
/**
4+
* Calculates the check digit for a UPC‑A using the Modulo 10 algorithm.
5+
*
6+
* @param digits The first 11 digits (UPC body) as a numeric string.
7+
*
8+
* @returns The check digit (0–9).
9+
*
10+
* @throws {FakerError} If `digits` is not exactly 11 numeric characters.
11+
*
12+
* @see upc
13+
*
14+
* @since 10.2.0
15+
*/
16+
export function calculateUPCCheckDigit(digits: string): number {
17+
if (!/^\d{11}$/.test(digits)) {
18+
throw new FakerError(
19+
'calculateUPCCheckDigit expects exactly 11 numeric digits'
20+
);
21+
}
22+
23+
let sum = 0;
24+
let idx = 0;
25+
for (const digit of digits) {
26+
const n = Number.parseInt(digit, 10);
27+
sum += n * (idx % 2 === 0 ? 3 : 1);
28+
idx++;
29+
}
30+
31+
return (10 - (sum % 10)) % 10;
32+
}

test/modules/__snapshots__/commerce.spec.ts.snap

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ exports[`commerce > 42 > productMaterial 1`] = `"Cotton"`;
3636

3737
exports[`commerce > 42 > productName 1`] = `"Handcrafted Wooden Sausages"`;
3838

39+
exports[`commerce > 42 > upc > noArgs 1`] = `"397511086709"`;
40+
41+
exports[`commerce > 42 > upc > with 5 digit prefix 1`] = `"012343975112"`;
42+
43+
exports[`commerce > 42 > upc > with 11 digit prefix 1`] = `"012345678905"`;
44+
45+
exports[`commerce > 42 > upc > with empty prefix 1`] = `"397511086709"`;
46+
47+
exports[`commerce > 42 > upc > with single digit prefix 1`] = `"039751108673"`;
48+
3949
exports[`commerce > 1211 > department 1`] = `"Tools"`;
4050

4151
exports[`commerce > 1211 > isbn > noArgs 1`] = `"978-1-82966-736-0"`;
@@ -72,6 +82,16 @@ exports[`commerce > 1211 > productMaterial 1`] = `"Steel"`;
7282

7383
exports[`commerce > 1211 > productName 1`] = `"Tasty Steel Cheese"`;
7484

85+
exports[`commerce > 1211 > upc > noArgs 1`] = `"982966736875"`;
86+
87+
exports[`commerce > 1211 > upc > with 5 digit prefix 1`] = `"012349829662"`;
88+
89+
exports[`commerce > 1211 > upc > with 11 digit prefix 1`] = `"012345678905"`;
90+
91+
exports[`commerce > 1211 > upc > with empty prefix 1`] = `"982966736875"`;
92+
93+
exports[`commerce > 1211 > upc > with single digit prefix 1`] = `"098296673688"`;
94+
7595
exports[`commerce > 1337 > department 1`] = `"Computers"`;
7696

7797
exports[`commerce > 1337 > isbn > noArgs 1`] = `"978-0-12-435297-1"`;
@@ -107,3 +127,13 @@ exports[`commerce > 1337 > productDescription 1`] = `"Innovative Car featuring l
107127
exports[`commerce > 1337 > productMaterial 1`] = `"Ceramic"`;
108128

109129
exports[`commerce > 1337 > productName 1`] = `"Frozen Bronze Chicken"`;
130+
131+
exports[`commerce > 1337 > upc > noArgs 1`] = `"212435297133"`;
132+
133+
exports[`commerce > 1337 > upc > with 5 digit prefix 1`] = `"012342124351"`;
134+
135+
exports[`commerce > 1337 > upc > with 11 digit prefix 1`] = `"012345678905"`;
136+
137+
exports[`commerce > 1337 > upc > with empty prefix 1`] = `"212435297133"`;
138+
139+
exports[`commerce > 1337 > upc > with single digit prefix 1`] = `"021243529714"`;

test/modules/commerce.spec.ts

Lines changed: 177 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
1-
import { isISBN } from 'validator';
1+
import { isEAN, isISBN } from 'validator';
22
import { describe, expect, it } from 'vitest';
33
import { faker } from '../../src';
44
import { seededTests } from '../support/seeded-runs';
55
import { times } from './../support/times';
66

77
const NON_SEEDED_BASED_RUN = 5;
88

9+
/**
10+
* Helper function to verify UPC check digit
11+
*
12+
* @param upc The UPC string to verify.
13+
*/
14+
function verifyUPCCheckDigit(upc: string): boolean {
15+
if (!/^\d{12}$/.test(upc)) {
16+
return false;
17+
}
18+
19+
return isEAN(`0${upc}`);
20+
}
21+
922
describe('commerce', () => {
1023
seededTests(faker, 'commerce', (t) => {
1124
t.itEach(
@@ -46,6 +59,14 @@ describe('commerce', () => {
4659
})
4760
.it('with space separators', { separator: ' ' });
4861
});
62+
63+
t.describe('upc', (t) => {
64+
t.it('noArgs')
65+
.it('with empty prefix', { prefix: '' })
66+
.it('with single digit prefix', { prefix: '0' })
67+
.it('with 5 digit prefix', { prefix: '01234' })
68+
.it('with 11 digit prefix', { prefix: '01234567890' });
69+
});
4970
});
5071

5172
describe.each(times(NON_SEEDED_BASED_RUN).map(() => faker.seed()))(
@@ -247,6 +268,161 @@ describe('commerce', () => {
247268
expect(isbn).toSatisfy((isbn: string) => isISBN(isbn, 13));
248269
});
249270
});
271+
272+
describe(`upc()`, () => {
273+
it('should return a 12-digit UPC-A string when not passing arguments', () => {
274+
const upc = faker.commerce.upc();
275+
276+
expect(upc).toBeTruthy();
277+
expect(upc).toBeTypeOf('string');
278+
expect(upc, 'UPC should be exactly 12 digits').toHaveLength(12);
279+
expect(upc, 'UPC should contain only digits').toMatch(/^\d{12}$/);
280+
expect(
281+
verifyUPCCheckDigit(upc),
282+
'UPC check digit should be valid'
283+
).toBe(true);
284+
});
285+
286+
it('should return a 12-digit UPC-A string with empty prefix', () => {
287+
const upc = faker.commerce.upc({ prefix: '' });
288+
289+
expect(upc).toBeTruthy();
290+
expect(upc).toBeTypeOf('string');
291+
expect(upc, 'UPC should be exactly 12 digits').toHaveLength(12);
292+
expect(upc, 'UPC should contain only digits').toMatch(/^\d{12}$/);
293+
expect(
294+
verifyUPCCheckDigit(upc),
295+
'UPC check digit should be valid'
296+
).toBe(true);
297+
});
298+
299+
it('should return a 12-digit UPC-A string with single digit prefix', () => {
300+
const prefix = '0';
301+
const upc = faker.commerce.upc({ prefix });
302+
303+
expect(upc).toBeTruthy();
304+
expect(upc).toBeTypeOf('string');
305+
expect(upc, 'UPC should be exactly 12 digits').toHaveLength(12);
306+
expect(upc, 'UPC should contain only digits').toMatch(/^\d{12}$/);
307+
expect(
308+
upc.startsWith(prefix),
309+
'UPC should start with the provided prefix'
310+
).toBe(true);
311+
expect(
312+
verifyUPCCheckDigit(upc),
313+
'UPC check digit should be valid'
314+
).toBe(true);
315+
});
316+
317+
it('should return a 12-digit UPC-A string with 5-digit prefix', () => {
318+
const prefix = '01234';
319+
const upc = faker.commerce.upc({ prefix });
320+
321+
expect(upc).toBeTruthy();
322+
expect(upc).toBeTypeOf('string');
323+
expect(upc, 'UPC should be exactly 12 digits').toHaveLength(12);
324+
expect(upc, 'UPC should contain only digits').toMatch(/^\d{12}$/);
325+
expect(
326+
upc.startsWith(prefix),
327+
'UPC should start with the provided prefix'
328+
).toBe(true);
329+
expect(
330+
verifyUPCCheckDigit(upc),
331+
'UPC check digit should be valid'
332+
).toBe(true);
333+
});
334+
335+
it('should return a 12-digit UPC-A string with 11-digit prefix', () => {
336+
const prefix = '01234567890';
337+
const upc = faker.commerce.upc({ prefix });
338+
339+
expect(upc).toBe('012345678905');
340+
});
341+
342+
it('should handle prefix with leading zeros', () => {
343+
const prefix = '00000';
344+
const upc = faker.commerce.upc({ prefix });
345+
346+
expect(upc).toBeTruthy();
347+
expect(upc, 'UPC should be exactly 12 digits').toHaveLength(12);
348+
expect(upc.startsWith(prefix)).toBe(true);
349+
expect(verifyUPCCheckDigit(upc)).toBe(true);
350+
});
351+
352+
it('should generate valid UPCs with various prefix lengths', () => {
353+
const prefixLengths = [0, 1, 2, 5, 8, 11];
354+
355+
for (const length of prefixLengths) {
356+
const prefix = length > 0 ? '0'.repeat(length) : '';
357+
const upc = faker.commerce.upc({ prefix });
358+
359+
expect(
360+
upc,
361+
`UPC with prefix length ${length} should be 12 digits`
362+
).toHaveLength(12);
363+
expect(
364+
verifyUPCCheckDigit(upc),
365+
`UPC with prefix length ${length} should have valid check digit`
366+
).toBe(true);
367+
if (prefix) {
368+
expect(
369+
upc.startsWith(prefix),
370+
`UPC should start with prefix of length ${length}`
371+
).toBe(true);
372+
}
373+
}
374+
});
375+
376+
it('should throw FakerError when prefix contains non-digit characters', () => {
377+
expect(() => {
378+
faker.commerce.upc({ prefix: 'abc' });
379+
}).toThrow('Prefix must contain only numeric digits');
380+
381+
expect(() => {
382+
faker.commerce.upc({ prefix: '123abc' });
383+
}).toThrow('Prefix must contain only numeric digits');
384+
385+
expect(() => {
386+
faker.commerce.upc({ prefix: '12-34' });
387+
}).toThrow('Prefix must contain only numeric digits');
388+
389+
expect(() => {
390+
faker.commerce.upc({ prefix: ' 123' });
391+
}).toThrow('Prefix must contain only numeric digits');
392+
});
393+
394+
it('should throw FakerError when prefix is longer than 11 digits', () => {
395+
expect(() => {
396+
faker.commerce.upc({ prefix: '012345678901' });
397+
}).toThrow('Prefix must be at most 11 numeric digits');
398+
399+
expect(() => {
400+
faker.commerce.upc({ prefix: '012345678901234' });
401+
}).toThrow('Prefix must be at most 11 numeric digits');
402+
});
403+
404+
it('should throw FakerError with correct error message for invalid prefix types', () => {
405+
expect(() => {
406+
faker.commerce.upc({ prefix: '12a' });
407+
}).toThrow('Prefix must contain only numeric digits');
408+
409+
expect(() => {
410+
faker.commerce.upc({ prefix: '012345678901' });
411+
}).toThrow('Prefix must be at most 11 numeric digits');
412+
});
413+
414+
it('should generate valid UPCs that pass check digit validation for multiple calls', () => {
415+
const results = faker.helpers.multiple(() => faker.commerce.upc(), {
416+
count: 100,
417+
});
418+
419+
for (const upc of results) {
420+
expect(upc).toHaveLength(12);
421+
expect(upc).toMatch(/^\d{12}$/);
422+
expect(verifyUPCCheckDigit(upc)).toBe(true);
423+
}
424+
});
425+
});
250426
}
251427
);
252428
});

test/scripts/apidocs/__snapshots__/verify-jsdoc-tags.spec.ts.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ exports[`check docs completeness > all modules and methods are present 1`] = `
105105
"productDescription",
106106
"productMaterial",
107107
"productName",
108+
"upc",
108109
],
109110
],
110111
[

0 commit comments

Comments
 (0)