diff --git a/src/internal/mersenne.ts b/src/internal/mersenne.ts index 6be5825d32c..2372e364d78 100644 --- a/src/internal/mersenne.ts +++ b/src/internal/mersenne.ts @@ -73,7 +73,7 @@ import type { Randomizer } from '../randomizer'; * * @internal */ -class MersenneTwister19937 { +export class MersenneTwister19937 { private readonly N = 624; private readonly M = 397; private readonly MATRIX_A = 0x9908b0df; // constant vector a diff --git a/test/__snapshots__/mersenne.spec.ts.snap b/test/internal/__snapshots__/mersenne.spec.ts.snap similarity index 100% rename from test/__snapshots__/mersenne.spec.ts.snap rename to test/internal/__snapshots__/mersenne.spec.ts.snap diff --git a/test/internal/mersenne-test-utils.ts b/test/internal/mersenne-test-utils.ts new file mode 100644 index 00000000000..ee7a43da455 --- /dev/null +++ b/test/internal/mersenne-test-utils.ts @@ -0,0 +1,17 @@ +// Moved to a separate file to avoid importing the tests + +/** + * The maximum value that can be returned by `MersenneTwister19937.genrandReal2()`. + * This is the max possible value with 32 bits of precision that is less than 1. + */ +export const TWISTER_32CO_MAX_VALUE = 0.9999999997671694; +/** + * The maximum value that can be returned by `MersenneTwister19937.genrandRes53()`. + * This is the max possible value with 53 bits of precision that is less than 1. + */ +export const TWISTER_53CO_MAX_VALUE = 0.9999999999999999; +// Re-exported because the value might change in the future +/** + * The maximum value that can be returned by `next()`. + */ +export const MERSENNE_MAX_VALUE = TWISTER_32CO_MAX_VALUE; diff --git a/test/internal/mersenne.spec.ts b/test/internal/mersenne.spec.ts new file mode 100644 index 00000000000..bbee050258f --- /dev/null +++ b/test/internal/mersenne.spec.ts @@ -0,0 +1,136 @@ +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { + generateMersenne32Randomizer, + MersenneTwister19937, +} from '../../src/internal/mersenne'; +import type { Randomizer } from '../../src/randomizer'; +import { seededRuns } from '../support/seeded-runs'; +import { times } from '../support/times'; +import { + MERSENNE_MAX_VALUE, + TWISTER_32CO_MAX_VALUE, + TWISTER_53CO_MAX_VALUE, +} from './mersenne-test-utils'; + +const NON_SEEDED_BASED_RUN = 25; + +function newTwister( + seed: number = Math.random() * Number.MAX_SAFE_INTEGER +): MersenneTwister19937 { + const twister = new MersenneTwister19937(); + twister.initGenrand(seed); + return twister; +} + +describe('MersenneTwister19937', () => { + describe('genrandInt32()', () => { + it('should be able to return 0', () => { + const twister = newTwister(257678572); + + // There is no single value seed that can produce 0 in the first call + for (let i = 0; i < 5; i++) { + twister.genrandInt32(); + } + + const actual = twister.genrandInt32(); + expect(actual).toBe(0); + }); + + it('should be able to return 2^32-1', () => { + const twister = newTwister(2855577693); + const actual = twister.genrandInt32(); + expect(actual).toBe(2 ** 32 - 1); + }); + }); + + describe('genrandReal2()', () => { + it('should be able to return 0', () => { + const twister = newTwister(); + // shortcut to return minimal value + // the test above shows that it is possible to return 0 + twister.genrandInt32 = () => 0; + const actual = twister.genrandReal2(); + expect(actual).toBe(0); + }); + + it('should be able to return almost 1', () => { + const twister = newTwister(); + // shortcut to return maximal value + // the test above shows that it is possible to return 2^32-1 + twister.genrandInt32 = () => 2 ** 32 - 1; + const actual = twister.genrandReal2(); + expect(actual).toBe(TWISTER_32CO_MAX_VALUE); + }); + }); + + describe('genrandRes53()', () => { + it('should be able to return 0', () => { + const twister = newTwister(); + // shortcut to return minimal value + // the test above shows that it is possible to return 0 + twister.genrandInt32 = () => 0; + const actual = twister.genrandRes53(); + expect(actual).toBe(0); + }); + + it('should be able to return almost 1', () => { + const twister = newTwister(); + // shortcut to return maximal value + // the test above shows that it is possible to return 2^32-1 + twister.genrandInt32 = () => 2 ** 32 - 1; + const actual = twister.genrandRes53(); + expect(actual).toBe(TWISTER_53CO_MAX_VALUE); + }); + }); +}); + +describe('generateMersenne32Randomizer()', () => { + const randomizer: Randomizer = generateMersenne32Randomizer(); + + it('should return a result matching the interface', () => { + expect(randomizer).toBeDefined(); + expect(randomizer).toBeTypeOf('object'); + expect(randomizer.next).toBeTypeOf('function'); + expect(randomizer.seed).toBeTypeOf('function'); + }); + + describe.each( + [...seededRuns, ...seededRuns.map((v) => [v, 1, 2])].map((v) => [v]) + )('seed: %j', (seed) => { + beforeEach(() => { + randomizer.seed(seed); + }); + + it('should return deterministic value for next()', () => { + const actual = randomizer.next(); + + expect(actual).toMatchSnapshot(); + }); + }); + + function randomSeed(): number { + return Math.ceil(Math.random() * 1_000_000_000); + } + + // Create and log-back the seed for debug purposes + describe.each( + times(NON_SEEDED_BASED_RUN).flatMap(() => [ + [randomSeed()], + [[randomSeed(), randomSeed()]], + ]) + )('random seeded tests %j', (seed) => { + beforeAll(() => { + randomizer.seed(seed); + }); + + describe('next', () => { + it('should return random number from interval [0, 1)', () => { + const actual = randomizer.next(); + + expect(actual).toBeGreaterThanOrEqual(0); + expect(actual).toBeLessThanOrEqual(MERSENNE_MAX_VALUE); + expect(actual).toBeLessThan(1); + }); + }); + }); +}); diff --git a/test/mersenne.spec.ts b/test/mersenne.spec.ts deleted file mode 100644 index 6c7d5a18579..00000000000 --- a/test/mersenne.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; -import { generateMersenne32Randomizer } from '../src/internal/mersenne'; -import type { Randomizer } from '../src/randomizer'; -import { seededRuns } from './support/seeded-runs'; -import { times } from './support/times'; - -const NON_SEEDED_BASED_RUN = 25; - -describe('generateMersenne32Randomizer()', () => { - const randomizer: Randomizer = generateMersenne32Randomizer(); - - it('should return a result matching the interface', () => { - expect(randomizer).toBeDefined(); - expect(randomizer).toBeTypeOf('object'); - expect(randomizer.next).toBeTypeOf('function'); - expect(randomizer.seed).toBeTypeOf('function'); - }); - - describe.each( - [...seededRuns, ...seededRuns.map((v) => [v, 1, 2])].map((v) => [v]) - )('seed: %j', (seed) => { - beforeEach(() => { - randomizer.seed(seed); - }); - - it('should return deterministic value for next()', () => { - const actual = randomizer.next(); - - expect(actual).toMatchSnapshot(); - }); - }); - - function randomSeed(): number { - return Math.ceil(Math.random() * 1_000_000_000); - } - - // Create and log-back the seed for debug purposes - describe.each( - times(NON_SEEDED_BASED_RUN).flatMap(() => [ - [randomSeed()], - [[randomSeed(), randomSeed()]], - ]) - )('random seeded tests %j', (seed) => { - beforeAll(() => { - randomizer.seed(seed); - }); - - describe('next', () => { - it('should return random number from interval [0, 1)', () => { - const actual = randomizer.next(); - - expect(actual).toBeGreaterThanOrEqual(0); - expect(actual).toBeLessThan(1); - }); - }); - }); -}); diff --git a/test/modules/number.spec.ts b/test/modules/number.spec.ts index 66018ee61bd..699200b22f4 100644 --- a/test/modules/number.spec.ts +++ b/test/modules/number.spec.ts @@ -1,6 +1,7 @@ import validator from 'validator'; import { describe, expect, it } from 'vitest'; -import { faker, FakerError } from '../../src'; +import { faker, FakerError, SimpleFaker } from '../../src'; +import { MERSENNE_MAX_VALUE } from '../internal/mersenne-test-utils'; import { seededTests } from '../support/seeded-runs'; describe('number', () => { @@ -507,4 +508,38 @@ describe('number', () => { }); }); }); + + describe('value range tests', () => { + const customFaker = new SimpleFaker(); + // @ts-expect-error: access private member field + const randomizer = customFaker._randomizer; + describe('int', () => { + it('should be able to return 0', () => { + randomizer.next = () => 0; + const actual = customFaker.number.int(); + expect(actual).toBe(0); + }); + + // TODO @ST-DDT 2023-10-12: This requires a randomizer with 53 bits of precision + it.todo('should be able to return MAX_SAFE_INTEGER', () => { + randomizer.next = () => MERSENNE_MAX_VALUE; + const actual = customFaker.number.int(); + expect(actual).toBe(Number.MAX_SAFE_INTEGER); + }); + }); + + describe('float', () => { + it('should be able to return 0', () => { + randomizer.next = () => 0; + const actual = customFaker.number.float(); + expect(actual).toBe(0); + }); + + it('should be able to return almost 1', () => { + randomizer.next = () => MERSENNE_MAX_VALUE; + const actual = customFaker.number.float(); + expect(actual).toBe(MERSENNE_MAX_VALUE); + }); + }); + }); });