From 5ef8ef1da13e83efc61702e64d8f75b611afb4e5 Mon Sep 17 00:00:00 2001 From: Matt Mayer <152770+matthewmayer@users.noreply.github.com> Date: Wed, 13 Mar 2024 19:32:53 +0700 Subject: [PATCH] feat(number): add multipleOf to faker.number.int (#2586) Co-authored-by: ST-DDT --- src/modules/number/index.ts | 33 +++++++++++---- test/modules/number.spec.ts | 84 +++++++++++++++++++++++++++++++++++-- 2 files changed, 106 insertions(+), 11 deletions(-) diff --git a/src/modules/number/index.ts b/src/modules/number/index.ts index d8d92c59d23..be5e213f272 100644 --- a/src/modules/number/index.ts +++ b/src/modules/number/index.ts @@ -23,9 +23,11 @@ export class NumberModule extends SimpleModuleBase { * @param options Maximum value or options object. * @param options.min Lower bound for generated number. Defaults to `0`. * @param options.max Upper bound for generated number. Defaults to `Number.MAX_SAFE_INTEGER`. + * @param options.multipleOf Generated number will be a multiple of the given integer. Defaults to `1`. * * @throws When `min` is greater than `max`. - * @throws When there are no integers between `min` and `max`. + * @throws When there are no suitable integers between `min` and `max`. + * @throws When `multipleOf` is not a positive integer. * * @see faker.string.numeric(): For generating a `string` of digits with a given length (range). * @@ -35,6 +37,7 @@ export class NumberModule extends SimpleModuleBase { * faker.number.int({ min: 1000000 }) // 2900970162509863 * faker.number.int({ max: 100 }) // 42 * faker.number.int({ min: 10, max: 100 }) // 57 + * faker.number.int({ min: 10, max: 100, multipleOf: 10 }) // 50 * * @since 8.0.0 */ @@ -54,24 +57,39 @@ export class NumberModule extends SimpleModuleBase { * @default Number.MAX_SAFE_INTEGER */ max?: number; + /** + * Generated number will be a multiple of the given integer. + * + * @default 1 + */ + multipleOf?: number; } = {} ): number { if (typeof options === 'number') { options = { max: options }; } - const { min = 0, max = Number.MAX_SAFE_INTEGER } = options; - const effectiveMin = Math.ceil(min); - const effectiveMax = Math.floor(max); + const { min = 0, max = Number.MAX_SAFE_INTEGER, multipleOf = 1 } = options; + + if (!Number.isInteger(multipleOf)) { + throw new FakerError(`multipleOf should be an integer.`); + } + + if (multipleOf <= 0) { + throw new FakerError(`multipleOf should be greater than 0.`); + } + + const effectiveMin = Math.ceil(min / multipleOf); + const effectiveMax = Math.floor(max / multipleOf); if (effectiveMin === effectiveMax) { - return effectiveMin; + return effectiveMin * multipleOf; } if (effectiveMax < effectiveMin) { if (max >= min) { throw new FakerError( - `No integer value between ${min} and ${max} found.` + `No suitable integer value between ${min} and ${max} found.` ); } @@ -81,7 +99,8 @@ export class NumberModule extends SimpleModuleBase { // @ts-expect-error: access private member field const randomizer = this.faker._randomizer; const real = randomizer.next(); - return Math.floor(real * (effectiveMax + 1 - effectiveMin) + effectiveMin); + const delta = effectiveMax - effectiveMin + 1; // +1 for inclusive max bounds and even distribution + return Math.floor(real * delta + effectiveMin) * multipleOf; } /** diff --git a/test/modules/number.spec.ts b/test/modules/number.spec.ts index 475af30d205..f318fc5be09 100644 --- a/test/modules/number.spec.ts +++ b/test/modules/number.spec.ts @@ -61,6 +61,82 @@ describe('number', () => { expect(actual).lessThanOrEqual(Number.MAX_SAFE_INTEGER); }); + it('should return an even integer', () => { + const actual = faker.number.int({ multipleOf: 2 }); + + expect(actual).toBeTypeOf('number'); + expect(actual).toSatisfy(Number.isInteger); + expect(actual).toSatisfy((x: number) => x % 2 === 0); + expect(actual).toBeGreaterThanOrEqual(0); + expect(actual).toBeLessThanOrEqual(Number.MAX_SAFE_INTEGER); + }); + + it('provides numbers with a given multipleOf of 10 with exclusive ends', () => { + const results = [ + ...new Set( + Array.from({ length: 100 }, () => + faker.number.int({ + min: 12, + max: 37, + multipleOf: 10, + }) + ) + ), + ].sort(); + expect(results).toEqual([20, 30]); + }); + + it('provides numbers with a given multipleOf of 10 with inclusive ends', () => { + const results = [ + ...new Set( + Array.from({ length: 100 }, () => + faker.number.int({ + min: 10, + max: 50, + multipleOf: 10, + }) + ) + ), + ].sort(); + expect(results).toEqual([10, 20, 30, 40, 50]); + }); + + it('throws for float multipleOf', () => { + const input = { + min: 0, + max: 10, + multipleOf: 0.1, + }; + + expect(() => faker.number.int(input)).toThrow( + new FakerError('multipleOf should be an integer.') + ); + }); + + it('throws for negative multipleOf', () => { + const input = { + min: -10, + max: 10, + multipleOf: -1, + }; + + expect(() => faker.number.int(input)).toThrow( + new FakerError('multipleOf should be greater than 0.') + ); + }); + + it('throws for impossible multipleOf', () => { + const input = { + min: 11, + max: 19, + multipleOf: 10, + }; + + expect(() => faker.number.int(input)).toThrow( + new FakerError('No suitable integer value between 11 and 19 found.') + ); + }); + it('should return a random number given a maximum value as Number', () => { const actual = faker.number.int(10); @@ -167,7 +243,7 @@ describe('number', () => { expect(() => { faker.number.int({ min: 2.1, max: 2.9 }); }).toThrow( - new FakerError(`No integer value between 2.1 and 2.9 found.`) + new FakerError(`No suitable integer value between 2.1 and 2.9 found.`) ); }); }); @@ -368,7 +444,7 @@ describe('number', () => { expect(() => { faker.number.binary({ min: 2.1, max: 2.9 }); }).toThrow( - new FakerError(`No integer value between 2.1 and 2.9 found.`) + new FakerError(`No suitable integer value between 2.1 and 2.9 found.`) ); }); }); @@ -419,7 +495,7 @@ describe('number', () => { expect(() => { faker.number.octal({ min: 2.1, max: 2.9 }); }).toThrow( - new FakerError(`No integer value between 2.1 and 2.9 found.`) + new FakerError(`No suitable integer value between 2.1 and 2.9 found.`) ); }); }); @@ -467,7 +543,7 @@ describe('number', () => { expect(() => { faker.number.hex({ min: 2.1, max: 2.9 }); }).toThrow( - new FakerError(`No integer value between 2.1 and 2.9 found.`) + new FakerError(`No suitable integer value between 2.1 and 2.9 found.`) ); }); });