Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add initial seed parameter to constructors #3220

Merged
merged 15 commits into from
Nov 12, 2024
Merged
16 changes: 15 additions & 1 deletion src/faker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ export class Faker extends SimpleFaker {
* Specify this only if you want to use it to achieve a specific goal,
* such as sharing the same random generator with other instances/tools.
* Defaults to faker's Mersenne Twister based pseudo random number generator.
* @param options.seed The initial seed to use.
* The seed can be used to generate reproducible values.
* Refer to the `seed()` method for more information.
* Defaults to a random seed.
*
* @example
* import { Faker, es } from '@faker-js/faker';
Expand Down Expand Up @@ -157,8 +161,18 @@ export class Faker extends SimpleFaker {
* @default generateMersenne53Randomizer()
*/
randomizer?: Randomizer;

/**
* The initial seed to use.
* The seed can be used to generate reproducible values.
*
* Refer to the `seed()` method for more information.
*
* @default randomSeed()
*/
seed?: number;
}) {
super({ randomizer: options.randomizer });
super({ randomizer: options.randomizer, seed: options.seed });

let { locale } = options;

Expand Down
8 changes: 8 additions & 0 deletions src/internal/seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Generates a random seed.
*
* @internal
*/
export function randomSeed(): number {
return Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER);
}
27 changes: 22 additions & 5 deletions src/simple-faker.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { randomSeed } from './internal/seed';
import { DatatypeModule } from './modules/datatype';
import { SimpleDateModule } from './modules/date';
import { SimpleHelpersModule } from './modules/helpers';
Expand Down Expand Up @@ -97,6 +98,10 @@ export class SimpleFaker {
* Specify this only if you want to use it to achieve a specific goal,
* such as sharing the same random generator with other instances/tools.
* Defaults to faker's Mersenne Twister based pseudo random number generator.
* @param options.seed The initial seed to use.
* The seed can be used to generate reproducible values.
* Refer to the `seed()` method for more information.
* Defaults to a random seed.
*
* @example
* import { SimpleFaker } from '@faker-js/faker';
Expand All @@ -120,11 +125,25 @@ export class SimpleFaker {
* @default generateMersenne53Randomizer()
*/
randomizer?: Randomizer;

/**
* The initial seed to use.
* The seed can be used to generate reproducible values.
*
* Refer to the `seed()` method for more information.
*
* @default randomSeed()
*/
seed?: number;
} = {}
) {
const { randomizer = generateMersenne53Randomizer() } = options;
const { randomizer, seed } = options;

if (randomizer != null && seed != null) {
randomizer.seed(seed);
}

this._randomizer = randomizer;
this._randomizer = randomizer ?? generateMersenne53Randomizer(seed);
}

/**
Expand Down Expand Up @@ -247,9 +266,7 @@ export class SimpleFaker {
* @since 6.0.0
*/
seed(seed?: number | number[]): number | number[];
seed(
seed: number | number[] = Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER)
): number | number[] {
seed(seed: number | number[] = randomSeed()): number | number[] {
this._randomizer.seed(seed);

return seed;
Expand Down
17 changes: 13 additions & 4 deletions src/utils/mersenne.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { MersenneTwister19937 } from '../internal/mersenne';
import { randomSeed } from '../internal/seed';
import type { Randomizer } from '../randomizer';

/**
* Generates a MersenneTwister19937 randomizer with 32 bits of precision.
* This is the default randomizer used by faker prior to v9.0.
*
* @param seed The initial seed to use. Defaults to a random number.
*/
export function generateMersenne32Randomizer(): Randomizer {
export function generateMersenne32Randomizer(
seed: number = randomSeed()
): Randomizer {
const twister = new MersenneTwister19937();

twister.initGenrand(Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER));
twister.initGenrand(seed);

return {
next(): number {
Expand All @@ -27,11 +32,15 @@ export function generateMersenne32Randomizer(): Randomizer {
/**
* Generates a MersenneTwister19937 randomizer with 53 bits of precision.
* This is the default randomizer used by faker starting with v9.0.
*
* @param seed The initial seed to use. Defaults to a random number.
*/
export function generateMersenne53Randomizer(): Randomizer {
export function generateMersenne53Randomizer(
seed: number = randomSeed()
): Randomizer {
const twister = new MersenneTwister19937();

twister.initGenrand(Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER));
twister.initGenrand(seed);

return {
next(): number {
Expand Down
83 changes: 63 additions & 20 deletions test/faker.spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import type { MockInstance } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import { Faker, faker } from '../src';
import { Faker, faker, generateMersenne32Randomizer } from '../src';
import { FakerError } from '../src/errors/faker-error';
import { keys } from '../src/internal/keys';

describe('faker', () => {
it('should throw error if no locales passed', () => {
expect(() => new Faker({ locale: [] })).toThrow(
new FakerError(
'The locale option must contain at least one locale definition.'
)
);
});

it('should not log anything on startup', async () => {
const spies: MockInstance[] = keys(console)
.filter((key) => typeof console[key] === 'function')
Expand Down Expand Up @@ -69,19 +61,70 @@ describe('faker', () => {
});
});

describe('randomizer', () => {
it('should be possible to provide a custom Randomizer', () => {
const customFaker = new Faker({
locale: {},
randomizer: {
next: () => 0,
seed: () => void 0,
},
describe('constructor()', () => {
describe('locale', () => {
it('should throw error if no locales passed', () => {
expect(() => new Faker({ locale: [] })).toThrow(
new FakerError(
'The locale option must contain at least one locale definition.'
)
);
});
});

describe('randomizer', () => {
it('should be possible to provide a custom Randomizer', () => {
const customFaker = new Faker({
locale: {},
randomizer: {
next: () => 0,
seed: () => void 0,
},
});

expect(customFaker.number.int()).toBe(0);
expect(customFaker.number.int()).toBe(0);
expect(customFaker.number.int()).toBe(0);
});
});

describe('seed', () => {
it('should be possible to provide an initial seed', () => {
const customFaker = new Faker({
locale: {},
seed: 12345,
});

expect(customFaker.number.int()).toBe(8373237378417847);
expect(customFaker.number.int()).toBe(2849657659447330);
expect(customFaker.number.int()).toBe(1656593383470774);

customFaker.seed(12345);

expect(customFaker.number.int()).toBe(8373237378417847);
expect(customFaker.number.int()).toBe(2849657659447330);
expect(customFaker.number.int()).toBe(1656593383470774);
});
});

expect(customFaker.number.int()).toBe(0);
expect(customFaker.number.int()).toBe(0);
expect(customFaker.number.int()).toBe(0);
describe('randomizer+seed', () => {
it('should take apply both the randomizer and seed', () => {
const customFaker = new Faker({
locale: {},
randomizer: generateMersenne32Randomizer(67890),
seed: 12345,
});

expect(customFaker.number.int()).toBe(8373237322874880);
expect(customFaker.number.int()).toBe(8017800868134912);
expect(customFaker.number.int()).toBe(2849657711493120);

customFaker.seed(12345); // Retry with the expected seed

expect(customFaker.number.int()).toBe(8373237322874880);
expect(customFaker.number.int()).toBe(8017800868134912);
expect(customFaker.number.int()).toBe(2849657711493120);
});
});
});

Expand Down
11 changes: 11 additions & 0 deletions test/internal/seed.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { describe, expect, it } from 'vitest';
import { randomSeed } from '../../src/internal/seed';

describe('seed', () => {
it('should generate a random seed', () => {
const actual = randomSeed();

expect(actual).toBeTypeOf('number');
expect(actual).not.toBe(randomSeed());
});
});
56 changes: 55 additions & 1 deletion test/simple-faker.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { MockInstance } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import { SimpleFaker, simpleFaker } from '../src';
import { generateMersenne32Randomizer, SimpleFaker, simpleFaker } from '../src';
import { keys } from '../src/internal/keys';

describe('simpleFaker', () => {
Expand All @@ -20,6 +20,60 @@ describe('simpleFaker', () => {
}
});

describe('constructor()', () => {
describe('randomizer', () => {
it('should be possible to provide a custom Randomizer', () => {
const customFaker = new SimpleFaker({
randomizer: {
next: () => 0,
seed: () => void 0,
},
});

expect(customFaker.number.int()).toBe(0);
expect(customFaker.number.int()).toBe(0);
expect(customFaker.number.int()).toBe(0);
});
});

describe('seed', () => {
it('should be possible to provide an initial seed', () => {
const customFaker = new SimpleFaker({
seed: 12345,
});

expect(customFaker.number.int()).toBe(8373237378417847);
expect(customFaker.number.int()).toBe(2849657659447330);
expect(customFaker.number.int()).toBe(1656593383470774);

customFaker.seed(12345); // Retry with the expected seed

expect(customFaker.number.int()).toBe(8373237378417847);
expect(customFaker.number.int()).toBe(2849657659447330);
expect(customFaker.number.int()).toBe(1656593383470774);
});
});

describe('randomizer+seed', () => {
it('should take apply both the randomizer and seed', () => {
const customFaker = new SimpleFaker({
randomizer: generateMersenne32Randomizer(67890),
seed: 12345,
});

expect(customFaker.number.int()).toBe(8373237322874880);
expect(customFaker.number.int()).toBe(8017800868134912);
expect(customFaker.number.int()).toBe(2849657711493120);

customFaker.seed(12345); // Retry with the expected seed

expect(customFaker.number.int()).toBe(8373237322874880);
expect(customFaker.number.int()).toBe(8017800868134912);
expect(customFaker.number.int()).toBe(2849657711493120);
});
});
});

// This is only here for coverage
// The actual test is in mersenne.spec.ts
describe('seed()', () => {
Expand Down