diff --git a/src/modules/number/index.ts b/src/modules/number/index.ts index f5f5c27fab6..f1dce04fb45 100644 --- a/src/modules/number/index.ts +++ b/src/modules/number/index.ts @@ -375,14 +375,18 @@ export class NumberModule extends SimpleModuleBase { * @param options Maximum value or options object. * @param options.min Lower bound for generated bigint. Defaults to `0n`. * @param options.max Upper bound for generated bigint. Defaults to `min + 999999999999999n`. + * @param options.multipleOf The generated bigint will be a multiple of this parameter. Defaults to `1n`. * * @throws When `min` is greater than `max`. + * @throws When there are no suitable bigint between `min` and `max`. + * @throws When `multipleOf` is not a positive bigint. * * @example * faker.number.bigInt() // 55422n * faker.number.bigInt(100n) // 52n * faker.number.bigInt({ min: 1000000n }) // 431433n * faker.number.bigInt({ max: 100n }) // 42n + * faker.number.bigInt({ multipleOf: 7n }) // 35n * faker.number.bigInt({ min: 10n, max: 100n }) // 36n * * @since 8.0.0 @@ -406,6 +410,12 @@ export class NumberModule extends SimpleModuleBase { * @default min + 999999999999999n */ max?: bigint | number | string | boolean; + /** + * The generated bigint will be a multiple of this parameter. + * + * @default 1n + */ + multipleOf?: bigint | number | string | boolean; } = {} ): bigint { if ( @@ -421,27 +431,38 @@ export class NumberModule extends SimpleModuleBase { const min = BigInt(options.min ?? 0); const max = BigInt(options.max ?? min + BigInt(999999999999999)); + const multipleOf = BigInt(options.multipleOf ?? 1); - if (max === min) { - return min; + if (max < min) { + throw new FakerError(`Max ${max} should be larger than min ${min}.`); } - if (max < min) { - throw new FakerError(`Max ${max} should be larger then min ${min}.`); + if (multipleOf <= BigInt(0)) { + throw new FakerError(`multipleOf should be greater than 0.`); + } + + const effectiveMin = min / multipleOf + (min % multipleOf > 0n ? 1n : 0n); // Math.ceil(min / multipleOf) + const effectiveMax = max / multipleOf - (max % multipleOf < 0n ? 1n : 0n); // Math.floor(max / multipleOf) + + if (effectiveMin === effectiveMax) { + return effectiveMin * multipleOf; } - const delta = max - min; + if (effectiveMax < effectiveMin) { + throw new FakerError( + `No suitable bigint value between ${min} and ${max} found.` + ); + } + const delta = effectiveMax - effectiveMin + 1n; // +1 for inclusive max bounds and even distribution const offset = BigInt( this.faker.string.numeric({ length: delta.toString(10).length, allowLeadingZeros: true, }) - ) % - (delta + BigInt(1)); - - return min + offset; + ) % delta; + return (effectiveMin + offset) * multipleOf; } /** diff --git a/test/modules/__snapshots__/number.spec.ts.snap b/test/modules/__snapshots__/number.spec.ts.snap index 516abc9ffa8..521fc0566e5 100644 --- a/test/modules/__snapshots__/number.spec.ts.snap +++ b/test/modules/__snapshots__/number.spec.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`number > 42 > bigInt > noArgs 1`] = `397511086709821n`; +exports[`number > 42 > bigInt > noArgs 1`] = `975110867098211n`; exports[`number > 42 > bigInt > with big options 1`] = `19556777749482489605814694n`; @@ -8,6 +8,8 @@ exports[`number > 42 > bigInt > with bigint value 1`] = `25n`; exports[`number > 42 > bigInt > with boolean value 1`] = `1n`; +exports[`number > 42 > bigInt > with multipleOf 1`] = `147890295638632n`; + exports[`number > 42 > bigInt > with number value 1`] = `39n`; exports[`number > 42 > bigInt > with options 1`] = `19n`; @@ -64,7 +66,7 @@ exports[`number > 42 > romanNumeral > with only max 1`] = `"LXII"`; exports[`number > 42 > romanNumeral > with only min 1`] = `"MDI"`; -exports[`number > 1211 > bigInt > noArgs 1`] = `982966736876848n`; +exports[`number > 1211 > bigInt > noArgs 1`] = `829667368768488n`; exports[`number > 1211 > bigInt > with big options 1`] = `25442250580110979794946298n`; @@ -72,6 +74,8 @@ exports[`number > 1211 > bigInt > with bigint value 1`] = `114n`; exports[`number > 1211 > bigInt > with boolean value 1`] = `1n`; +exports[`number > 1211 > bigInt > with multipleOf 1`] = `784113589297853n`; + exports[`number > 1211 > bigInt > with number value 1`] = `12n`; exports[`number > 1211 > bigInt > with options 1`] = `44n`; @@ -128,7 +132,7 @@ exports[`number > 1211 > romanNumeral > with only max 1`] = `"CLIV"`; exports[`number > 1211 > romanNumeral > with only min 1`] = `"MMMDCCXIV"`; -exports[`number > 1337 > bigInt > noArgs 1`] = `212435297136194n`; +exports[`number > 1337 > bigInt > noArgs 1`] = `124352971361947n`; exports[`number > 1337 > bigInt > with big options 1`] = `27379244885156992800029992n`; @@ -136,6 +140,8 @@ exports[`number > 1337 > bigInt > with bigint value 1`] = `88n`; exports[`number > 1337 > bigInt > with boolean value 1`] = `0n`; +exports[`number > 1337 > bigInt > with multipleOf 1`] = `682275118016671n`; + exports[`number > 1337 > bigInt > with number value 1`] = `21n`; exports[`number > 1337 > bigInt > with options 1`] = `58n`; diff --git a/test/modules/number.spec.ts b/test/modules/number.spec.ts index d2762cf467c..278baadb1df 100644 --- a/test/modules/number.spec.ts +++ b/test/modules/number.spec.ts @@ -50,6 +50,7 @@ describe('number', () => { .it('with boolean value', true) .it('with bigint value', 123n) .it('with options', { min: -42, max: 69 }) + .it('with multipleOf', { multipleOf: 7919n }) .it('with big options', { min: 6135715171537515454317351n, max: 32465761264574654845432354n, @@ -631,7 +632,141 @@ describe('number', () => { expect(() => { faker.number.bigInt({ min, max }); }).toThrow( - new FakerError(`Max ${max} should be larger then min ${min}.`) + new FakerError(`Max ${max} should be larger than min ${min}.`) + ); + }); + + it('should generate a random bigint with a given multipleOf of 1n', () => { + const generateBigInt = faker.number.bigInt({ multipleOf: 1n }); + expect(generateBigInt).toBeTypeOf('bigint'); + }); + + it('should generate a random bigint with a given multipleOf of 7919n', () => { + const generateBigInt = faker.number.bigInt({ multipleOf: 7919n }); + expect(generateBigInt).toBeTypeOf('bigint'); + expect(generateBigInt % 7919n).toBe(0n); + }); + + it('should generate a random bigint with a given max value less than multipleOf', () => { + const generatedBigInt = faker.number.bigInt({ + max: 10n, + multipleOf: 20n, + }); + expect(generatedBigInt).toBeTypeOf('bigint'); + expect(generatedBigInt % 20n).toBe(0n); + }); + + it('should generate a suitable bigint value between negative min and max', () => { + const generateBigInt = faker.number.bigInt({ + min: -9, + max: 0, + multipleOf: 5, + }); + expect(generateBigInt).toBeTypeOf('bigint'); + expect(generateBigInt % 5n).toBe(0n); + }); + + it('should generate a suitable bigint value between negative min and negative max', () => { + const generateBigInt = faker.number.bigInt({ + min: -9, + max: -1, + multipleOf: 5, + }); + expect(generateBigInt).toBeTypeOf('bigint'); + expect(generateBigInt).toBe(-5n); + }); + + it('should generate a suitable bigint value between negative min and negative max (edge case)', () => { + const generateBigInt = faker.number.bigInt({ + min: -9, + max: -1, + multipleOf: 9, + }); + expect(generateBigInt).toBeTypeOf('bigint'); + expect(generateBigInt).toBe(-9n); + }); + + it('should return inclusive positive min/max value', () => { + const positive4 = 4n; + const positive5 = 5n; + let foundPositive4 = false; + let foundPositive5 = false; + + for (let iter = 0; iter < 1000; iter++) { + const actual = faker.number.bigInt({ + min: positive4, + max: positive5, + }); + + if (actual === positive4) { + foundPositive4 = true; + } else if (actual === positive5) { + foundPositive5 = true; + } + + expect(actual).toBeTypeOf('bigint'); + expect(actual).toBeGreaterThanOrEqual(positive4); + expect(actual).toBeLessThanOrEqual(positive5); + + if (foundPositive4 && foundPositive5) { + break; + } + } + + expect(foundPositive4).toBeTruthy(); + expect(foundPositive5).toBeTruthy(); + }); + + it('should return inclusive negative min/max value', () => { + const negative4 = -4n; + const negative5 = -5n; + let foundNegative4 = false; + let foundNegative5 = false; + + for (let iter = 0; iter < 1000; iter++) { + const actual = faker.number.bigInt({ + min: negative5, + max: negative4, + }); + + if (actual === negative4) { + foundNegative4 = true; + } else if (actual === negative5) { + foundNegative5 = true; + } + + expect(actual).toBeTypeOf('bigint'); + expect(actual).toBeGreaterThanOrEqual(negative5); + expect(actual).toBeLessThanOrEqual(negative4); + + if (foundNegative4 && foundNegative5) { + break; + } + } + + expect(foundNegative4).toBeTruthy(); + expect(foundNegative5).toBeTruthy(); + }); + + it('should throw for non-positive multipleOf', () => { + expect(() => faker.number.bigInt({ multipleOf: 0n })).toThrow( + new FakerError('multipleOf should be greater than 0.') + ); + }); + + it('should throw if there is no suitable bigint value between min and max', () => { + expect(() => + faker.number.bigInt({ min: 6, max: 9, multipleOf: 5 }) + ).toThrow( + new FakerError('No suitable bigint value between 6 and 9 found.') + ); + }); + + it('should throw if there is no suitable bigint value between same min and max', () => { + expect(() => + faker.number.bigInt({ min: 1, max: 1, multipleOf: 5 }) + ).toThrow( + new FakerError('No suitable bigint value between 1 and 1 found.') ); }); });