Skip to content

Commit

Permalink
feat(number): add multipleOf to faker.number.int (#2586)
Browse files Browse the repository at this point in the history
Co-authored-by: ST-DDT <ST-DDT@gmx.de>
  • Loading branch information
matthewmayer and ST-DDT committed Mar 13, 2024
1 parent 1169a05 commit 5ef8ef1
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 11 deletions.
33 changes: 26 additions & 7 deletions src/modules/number/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*
Expand All @@ -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
*/
Expand All @@ -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.`
);
}

Expand All @@ -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;
}

/**
Expand Down
84 changes: 80 additions & 4 deletions test/modules/number.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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.`)
);
});
});
Expand Down Expand Up @@ -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.`)
);
});
});
Expand Down Expand Up @@ -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.`)
);
});
});
Expand Down Expand Up @@ -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.`)
);
});
});
Expand Down

0 comments on commit 5ef8ef1

Please sign in to comment.