diff --git a/src/modules/commerce/index.ts b/src/modules/commerce/index.ts index f00684153a8..2464af03020 100644 --- a/src/modules/commerce/index.ts +++ b/src/modules/commerce/index.ts @@ -2,6 +2,79 @@ import type { Faker } from '../../faker'; import { bindThisToMemberFunctions } from '../../internal/bind-this-to-member-functions'; import { deprecated } from '../../internal/deprecated'; +// Source for official prefixes: https://www.isbn-international.org/range_file_generation +const ISBN_LENGTH_RULES: Record< + string, + Array<[rangeMaximum: number, length: number]> +> = { + '0': [ + [1999999, 2], + [2279999, 3], + [2289999, 4], + [3689999, 3], + [3699999, 4], + [6389999, 3], + [6397999, 4], + [6399999, 7], + [6449999, 3], + [6459999, 7], + [6479999, 3], + [6489999, 7], + [6549999, 3], + [6559999, 4], + [6999999, 3], + [8499999, 4], + [8999999, 5], + [9499999, 6], + [9999999, 7], + ], + '1': [ + [99999, 3], + [299999, 2], + [349999, 3], + [399999, 4], + [499999, 3], + [699999, 2], + [999999, 4], + [3979999, 3], + [5499999, 4], + [6499999, 5], + [6799999, 4], + [6859999, 5], + [7139999, 4], + [7169999, 3], + [7319999, 4], + [7399999, 7], + [7749999, 5], + [7753999, 7], + [7763999, 5], + [7764999, 7], + [7769999, 5], + [7782999, 7], + [7899999, 5], + [7999999, 4], + [8004999, 5], + [8049999, 5], + [8379999, 5], + [8384999, 7], + [8671999, 5], + [8675999, 4], + [8697999, 5], + [9159999, 6], + [9165059, 7], + [9168699, 6], + [9169079, 7], + [9195999, 6], + [9196549, 7], + [9729999, 6], + [9877999, 4], + [9911499, 6], + [9911999, 7], + [9989899, 6], + [9999999, 7], + ], +}; + /** * Module to generate commerce and product related entries. * @@ -258,4 +331,83 @@ export class CommerceModule { this.faker.definitions.commerce.product_description ); } + + /** + * Returns a random [ISBN](https://en.wikipedia.org/wiki/ISBN) identifier. + * + * @param options The variant to return or an options object. Defaults to `{}`. + * @param options.variant The variant to return. Can be either `10` (10-digit format) + * or `13` (13-digit format). Defaults to `13`. + * @param options.separator The separator to use in the format. Defaults to `'-'`. + * + * @example + * faker.commerce.isbn() // '978-0-692-82459-7' + * faker.commerce.isbn(10) // '1-155-36404-X' + * faker.commerce.isbn(13) // '978-1-60808-867-6' + * faker.commerce.isbn({ separator: ' ' }) // '978 0 452 81498 1' + * faker.commerce.isbn({ variant: 10, separator: ' ' }) // '0 940319 49 7' + * faker.commerce.isbn({ variant: 13, separator: ' ' }) // '978 1 6618 9122 0' + * + * @since 8.1.0 + */ + isbn( + options: + | 10 + | 13 + | { + /** + * The variant of the identifier to return. + * Can be either `10` (10-digit format) + * or `13` (13-digit format). + * + * @default 13 + */ + variant?: 10 | 13; + + /** + * The separator to use in the format. + * + * @default '-' + */ + separator?: string; + } = {} + ): string { + if (typeof options === 'number') { + options = { variant: options }; + } + + const { variant = 13, separator = '-' } = options; + + const prefix = '978'; + const [group, groupRules] = + this.faker.helpers.objectEntry(ISBN_LENGTH_RULES); + const element = this.faker.string.numeric(8); + const elementValue = parseInt(element.slice(0, -1)); + + const registrantLength = groupRules.find( + ([rangeMaximum]) => elementValue <= rangeMaximum + )[1]; + + const registrant = element.slice(0, registrantLength); + const publication = element.slice(registrantLength); + + const data = [prefix, group, registrant, publication]; + if (variant === 10) { + data.shift(); + } + + const isbn = data.join(''); + + let checksum = 0; + for (let i = 0; i < variant - 1; i++) { + const weight = variant === 10 ? i + 1 : i % 2 ? 3 : 1; + checksum += weight * parseInt(isbn[i]); + } + + checksum = variant === 10 ? checksum % 11 : (10 - (checksum % 10)) % 10; + + data.push(checksum === 10 ? 'X' : checksum.toString()); + + return data.join(separator); + } } diff --git a/test/modules/__snapshots__/commerce.spec.ts.snap b/test/modules/__snapshots__/commerce.spec.ts.snap index 1dde678565e..00b8f08e58a 100644 --- a/test/modules/__snapshots__/commerce.spec.ts.snap +++ b/test/modules/__snapshots__/commerce.spec.ts.snap @@ -2,6 +2,16 @@ exports[`commerce > 42 > department 1`] = `"Tools"`; +exports[`commerce > 42 > isbn > noArgs 1`] = `"978-0-7917-7551-6"`; + +exports[`commerce > 42 > isbn > with space separators 1`] = `"978 0 7917 7551 6"`; + +exports[`commerce > 42 > isbn > with variant 10 1`] = `"0-7917-7551-8"`; + +exports[`commerce > 42 > isbn > with variant 10 and space separators 1`] = `"0 7917 7551 8"`; + +exports[`commerce > 42 > isbn > with variant 13 1`] = `"978-0-7917-7551-6"`; + exports[`commerce > 42 > price > noArgs 1`] = `"375.00"`; exports[`commerce > 42 > price > with max 1`] = `"375.00"`; @@ -36,6 +46,16 @@ exports[`commerce > 42 > productName 1`] = `"Fantastic Soft Sausages"`; exports[`commerce > 1211 > department 1`] = `"Automotive"`; +exports[`commerce > 1211 > isbn > noArgs 1`] = `"978-1-4872-1906-2"`; + +exports[`commerce > 1211 > isbn > with space separators 1`] = `"978 1 4872 1906 2"`; + +exports[`commerce > 1211 > isbn > with variant 10 1`] = `"1-4872-1906-7"`; + +exports[`commerce > 1211 > isbn > with variant 10 and space separators 1`] = `"1 4872 1906 7"`; + +exports[`commerce > 1211 > isbn > with variant 13 1`] = `"978-1-4872-1906-2"`; + exports[`commerce > 1211 > price > noArgs 1`] = `"929.00"`; exports[`commerce > 1211 > price > with max 1`] = `"929.00"`; @@ -70,6 +90,16 @@ exports[`commerce > 1211 > productName 1`] = `"Unbranded Cotton Salad"`; exports[`commerce > 1337 > department 1`] = `"Computers"`; +exports[`commerce > 1337 > isbn > noArgs 1`] = `"978-0-512-25403-0"`; + +exports[`commerce > 1337 > isbn > with space separators 1`] = `"978 0 512 25403 0"`; + +exports[`commerce > 1337 > isbn > with variant 10 1`] = `"0-512-25403-6"`; + +exports[`commerce > 1337 > isbn > with variant 10 and space separators 1`] = `"0 512 25403 6"`; + +exports[`commerce > 1337 > isbn > with variant 13 1`] = `"978-0-512-25403-0"`; + exports[`commerce > 1337 > price > noArgs 1`] = `"263.00"`; exports[`commerce > 1337 > price > with max 1`] = `"263.00"`; diff --git a/test/modules/commerce.spec.ts b/test/modules/commerce.spec.ts index 15ad7aee857..0588e228f58 100644 --- a/test/modules/commerce.spec.ts +++ b/test/modules/commerce.spec.ts @@ -1,3 +1,4 @@ +import validator from 'validator'; import { describe, expect, it } from 'vitest'; import { faker } from '../../src'; import { seededTests } from './../support/seededRuns'; @@ -38,6 +39,17 @@ describe('commerce', () => { symbol: '$', }); }); + + t.describe('isbn', (t) => { + t.it('noArgs') + .it('with variant 10', 10) + .it('with variant 13', 13) + .it('with variant 10 and space separators', { + variant: 10, + separator: ' ', + }) + .it('with space separators', { separator: ' ' }); + }); }); describe.each(times(NON_SEEDED_BASED_RUN).map(() => faker.seed()))( @@ -158,6 +170,60 @@ describe('commerce', () => { ); }); }); + + describe(`isbn()`, () => { + it('should return ISBN-13 with hyphen separators when not passing arguments', () => { + const isbn = faker.commerce.isbn(); + + expect(isbn).toBeTruthy(); + expect(isbn).toBeTypeOf('string'); + expect( + isbn, + 'The expected match should be ISBN-13 with hyphens' + ).toMatch(/^978-[01]-[\d-]{9}-\d$/); + expect(isbn).toSatisfy((isbn: string) => validator.isISBN(isbn, 13)); + }); + + it('should return ISBN-10 with hyphen separators when passing variant 10 as argument', () => { + const isbn = faker.commerce.isbn(10); + + expect( + isbn, + 'The expected match should be ISBN-10 with hyphens' + ).toMatch(/^[01]-[\d-]{9}-[\dX]$/); + expect(isbn).toSatisfy((isbn: string) => validator.isISBN(isbn, 10)); + }); + + it('should return ISBN-13 with hyphen separators when passing variant 13 as argument', () => { + const isbn = faker.commerce.isbn(13); + + expect( + isbn, + 'The expected match should be ISBN-13 with hyphens' + ).toMatch(/^978-[01]-[\d-]{9}-\d$/); + expect(isbn).toSatisfy((isbn: string) => validator.isISBN(isbn, 13)); + }); + + it('should return ISBN-10 with space separators when passing variant 10 and space separators as argument', () => { + const isbn = faker.commerce.isbn({ variant: 10, separator: ' ' }); + + expect( + isbn, + 'The expected match should be ISBN-10 with space separators' + ).toMatch(/^[01] [\d ]{9} [\dX]$/); + expect(isbn).toSatisfy((isbn: string) => validator.isISBN(isbn, 10)); + }); + + it('should return ISBN-13 with space separators when passing space separators as argument', () => { + const isbn = faker.commerce.isbn({ separator: ' ' }); + + expect( + isbn, + 'The expected match should be ISBN-13 with space separators' + ).toMatch(/^978 [01] [\d ]{9} \d$/); + expect(isbn).toSatisfy((isbn: string) => validator.isISBN(isbn, 13)); + }); + }); } ); });