Skip to content

Commit cb4ef28

Browse files
feat(commerce): add method for generating ISBN-10 and ISBN-13 (#2240)
1 parent 8a6ce49 commit cb4ef28

3 files changed

Lines changed: 248 additions & 0 deletions

File tree

src/modules/commerce/index.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,79 @@ import type { Faker } from '../../faker';
22
import { bindThisToMemberFunctions } from '../../internal/bind-this-to-member-functions';
33
import { deprecated } from '../../internal/deprecated';
44

5+
// Source for official prefixes: https://www.isbn-international.org/range_file_generation
6+
const ISBN_LENGTH_RULES: Record<
7+
string,
8+
Array<[rangeMaximum: number, length: number]>
9+
> = {
10+
'0': [
11+
[1999999, 2],
12+
[2279999, 3],
13+
[2289999, 4],
14+
[3689999, 3],
15+
[3699999, 4],
16+
[6389999, 3],
17+
[6397999, 4],
18+
[6399999, 7],
19+
[6449999, 3],
20+
[6459999, 7],
21+
[6479999, 3],
22+
[6489999, 7],
23+
[6549999, 3],
24+
[6559999, 4],
25+
[6999999, 3],
26+
[8499999, 4],
27+
[8999999, 5],
28+
[9499999, 6],
29+
[9999999, 7],
30+
],
31+
'1': [
32+
[99999, 3],
33+
[299999, 2],
34+
[349999, 3],
35+
[399999, 4],
36+
[499999, 3],
37+
[699999, 2],
38+
[999999, 4],
39+
[3979999, 3],
40+
[5499999, 4],
41+
[6499999, 5],
42+
[6799999, 4],
43+
[6859999, 5],
44+
[7139999, 4],
45+
[7169999, 3],
46+
[7319999, 4],
47+
[7399999, 7],
48+
[7749999, 5],
49+
[7753999, 7],
50+
[7763999, 5],
51+
[7764999, 7],
52+
[7769999, 5],
53+
[7782999, 7],
54+
[7899999, 5],
55+
[7999999, 4],
56+
[8004999, 5],
57+
[8049999, 5],
58+
[8379999, 5],
59+
[8384999, 7],
60+
[8671999, 5],
61+
[8675999, 4],
62+
[8697999, 5],
63+
[9159999, 6],
64+
[9165059, 7],
65+
[9168699, 6],
66+
[9169079, 7],
67+
[9195999, 6],
68+
[9196549, 7],
69+
[9729999, 6],
70+
[9877999, 4],
71+
[9911499, 6],
72+
[9911999, 7],
73+
[9989899, 6],
74+
[9999999, 7],
75+
],
76+
};
77+
578
/**
679
* Module to generate commerce and product related entries.
780
*
@@ -258,4 +331,83 @@ export class CommerceModule {
258331
this.faker.definitions.commerce.product_description
259332
);
260333
}
334+
335+
/**
336+
* Returns a random [ISBN](https://en.wikipedia.org/wiki/ISBN) identifier.
337+
*
338+
* @param options The variant to return or an options object. Defaults to `{}`.
339+
* @param options.variant The variant to return. Can be either `10` (10-digit format)
340+
* or `13` (13-digit format). Defaults to `13`.
341+
* @param options.separator The separator to use in the format. Defaults to `'-'`.
342+
*
343+
* @example
344+
* faker.commerce.isbn() // '978-0-692-82459-7'
345+
* faker.commerce.isbn(10) // '1-155-36404-X'
346+
* faker.commerce.isbn(13) // '978-1-60808-867-6'
347+
* faker.commerce.isbn({ separator: ' ' }) // '978 0 452 81498 1'
348+
* faker.commerce.isbn({ variant: 10, separator: ' ' }) // '0 940319 49 7'
349+
* faker.commerce.isbn({ variant: 13, separator: ' ' }) // '978 1 6618 9122 0'
350+
*
351+
* @since 8.1.0
352+
*/
353+
isbn(
354+
options:
355+
| 10
356+
| 13
357+
| {
358+
/**
359+
* The variant of the identifier to return.
360+
* Can be either `10` (10-digit format)
361+
* or `13` (13-digit format).
362+
*
363+
* @default 13
364+
*/
365+
variant?: 10 | 13;
366+
367+
/**
368+
* The separator to use in the format.
369+
*
370+
* @default '-'
371+
*/
372+
separator?: string;
373+
} = {}
374+
): string {
375+
if (typeof options === 'number') {
376+
options = { variant: options };
377+
}
378+
379+
const { variant = 13, separator = '-' } = options;
380+
381+
const prefix = '978';
382+
const [group, groupRules] =
383+
this.faker.helpers.objectEntry(ISBN_LENGTH_RULES);
384+
const element = this.faker.string.numeric(8);
385+
const elementValue = parseInt(element.slice(0, -1));
386+
387+
const registrantLength = groupRules.find(
388+
([rangeMaximum]) => elementValue <= rangeMaximum
389+
)[1];
390+
391+
const registrant = element.slice(0, registrantLength);
392+
const publication = element.slice(registrantLength);
393+
394+
const data = [prefix, group, registrant, publication];
395+
if (variant === 10) {
396+
data.shift();
397+
}
398+
399+
const isbn = data.join('');
400+
401+
let checksum = 0;
402+
for (let i = 0; i < variant - 1; i++) {
403+
const weight = variant === 10 ? i + 1 : i % 2 ? 3 : 1;
404+
checksum += weight * parseInt(isbn[i]);
405+
}
406+
407+
checksum = variant === 10 ? checksum % 11 : (10 - (checksum % 10)) % 10;
408+
409+
data.push(checksum === 10 ? 'X' : checksum.toString());
410+
411+
return data.join(separator);
412+
}
261413
}

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
exports[`commerce > 42 > department 1`] = `"Tools"`;
44

5+
exports[`commerce > 42 > isbn > noArgs 1`] = `"978-0-7917-7551-6"`;
6+
7+
exports[`commerce > 42 > isbn > with space separators 1`] = `"978 0 7917 7551 6"`;
8+
9+
exports[`commerce > 42 > isbn > with variant 10 1`] = `"0-7917-7551-8"`;
10+
11+
exports[`commerce > 42 > isbn > with variant 10 and space separators 1`] = `"0 7917 7551 8"`;
12+
13+
exports[`commerce > 42 > isbn > with variant 13 1`] = `"978-0-7917-7551-6"`;
14+
515
exports[`commerce > 42 > price > noArgs 1`] = `"375.00"`;
616

717
exports[`commerce > 42 > price > with max 1`] = `"375.00"`;
@@ -36,6 +46,16 @@ exports[`commerce > 42 > productName 1`] = `"Fantastic Soft Sausages"`;
3646

3747
exports[`commerce > 1211 > department 1`] = `"Automotive"`;
3848

49+
exports[`commerce > 1211 > isbn > noArgs 1`] = `"978-1-4872-1906-2"`;
50+
51+
exports[`commerce > 1211 > isbn > with space separators 1`] = `"978 1 4872 1906 2"`;
52+
53+
exports[`commerce > 1211 > isbn > with variant 10 1`] = `"1-4872-1906-7"`;
54+
55+
exports[`commerce > 1211 > isbn > with variant 10 and space separators 1`] = `"1 4872 1906 7"`;
56+
57+
exports[`commerce > 1211 > isbn > with variant 13 1`] = `"978-1-4872-1906-2"`;
58+
3959
exports[`commerce > 1211 > price > noArgs 1`] = `"929.00"`;
4060

4161
exports[`commerce > 1211 > price > with max 1`] = `"929.00"`;
@@ -70,6 +90,16 @@ exports[`commerce > 1211 > productName 1`] = `"Unbranded Cotton Salad"`;
7090

7191
exports[`commerce > 1337 > department 1`] = `"Computers"`;
7292

93+
exports[`commerce > 1337 > isbn > noArgs 1`] = `"978-0-512-25403-0"`;
94+
95+
exports[`commerce > 1337 > isbn > with space separators 1`] = `"978 0 512 25403 0"`;
96+
97+
exports[`commerce > 1337 > isbn > with variant 10 1`] = `"0-512-25403-6"`;
98+
99+
exports[`commerce > 1337 > isbn > with variant 10 and space separators 1`] = `"0 512 25403 6"`;
100+
101+
exports[`commerce > 1337 > isbn > with variant 13 1`] = `"978-0-512-25403-0"`;
102+
73103
exports[`commerce > 1337 > price > noArgs 1`] = `"263.00"`;
74104

75105
exports[`commerce > 1337 > price > with max 1`] = `"263.00"`;

test/modules/commerce.spec.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import validator from 'validator';
12
import { describe, expect, it } from 'vitest';
23
import { faker } from '../../src';
34
import { seededTests } from './../support/seededRuns';
@@ -38,6 +39,17 @@ describe('commerce', () => {
3839
symbol: '$',
3940
});
4041
});
42+
43+
t.describe('isbn', (t) => {
44+
t.it('noArgs')
45+
.it('with variant 10', 10)
46+
.it('with variant 13', 13)
47+
.it('with variant 10 and space separators', {
48+
variant: 10,
49+
separator: ' ',
50+
})
51+
.it('with space separators', { separator: ' ' });
52+
});
4153
});
4254

4355
describe.each(times(NON_SEEDED_BASED_RUN).map(() => faker.seed()))(
@@ -158,6 +170,60 @@ describe('commerce', () => {
158170
);
159171
});
160172
});
173+
174+
describe(`isbn()`, () => {
175+
it('should return ISBN-13 with hyphen separators when not passing arguments', () => {
176+
const isbn = faker.commerce.isbn();
177+
178+
expect(isbn).toBeTruthy();
179+
expect(isbn).toBeTypeOf('string');
180+
expect(
181+
isbn,
182+
'The expected match should be ISBN-13 with hyphens'
183+
).toMatch(/^978-[01]-[\d-]{9}-\d$/);
184+
expect(isbn).toSatisfy((isbn: string) => validator.isISBN(isbn, 13));
185+
});
186+
187+
it('should return ISBN-10 with hyphen separators when passing variant 10 as argument', () => {
188+
const isbn = faker.commerce.isbn(10);
189+
190+
expect(
191+
isbn,
192+
'The expected match should be ISBN-10 with hyphens'
193+
).toMatch(/^[01]-[\d-]{9}-[\dX]$/);
194+
expect(isbn).toSatisfy((isbn: string) => validator.isISBN(isbn, 10));
195+
});
196+
197+
it('should return ISBN-13 with hyphen separators when passing variant 13 as argument', () => {
198+
const isbn = faker.commerce.isbn(13);
199+
200+
expect(
201+
isbn,
202+
'The expected match should be ISBN-13 with hyphens'
203+
).toMatch(/^978-[01]-[\d-]{9}-\d$/);
204+
expect(isbn).toSatisfy((isbn: string) => validator.isISBN(isbn, 13));
205+
});
206+
207+
it('should return ISBN-10 with space separators when passing variant 10 and space separators as argument', () => {
208+
const isbn = faker.commerce.isbn({ variant: 10, separator: ' ' });
209+
210+
expect(
211+
isbn,
212+
'The expected match should be ISBN-10 with space separators'
213+
).toMatch(/^[01] [\d ]{9} [\dX]$/);
214+
expect(isbn).toSatisfy((isbn: string) => validator.isISBN(isbn, 10));
215+
});
216+
217+
it('should return ISBN-13 with space separators when passing space separators as argument', () => {
218+
const isbn = faker.commerce.isbn({ separator: ' ' });
219+
220+
expect(
221+
isbn,
222+
'The expected match should be ISBN-13 with space separators'
223+
).toMatch(/^978 [01] [\d ]{9} \d$/);
224+
expect(isbn).toSatisfy((isbn: string) => validator.isISBN(isbn, 13));
225+
});
226+
});
161227
}
162228
);
163229
});

0 commit comments

Comments
 (0)