Skip to content

Commit

Permalink
feat: introduce faker.clone and derive
Browse files Browse the repository at this point in the history
  • Loading branch information
ST-DDT committed Feb 9, 2024
1 parent bc3ebb7 commit 93dd407
Show file tree
Hide file tree
Showing 9 changed files with 498 additions and 32 deletions.
22 changes: 14 additions & 8 deletions docs/guide/randomizer.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,19 @@ export function generatePureRandRandomizer(
seed: number | number[] = Date.now() ^ (Math.random() * 0x100000000),
factory: (seed: number) => RandomGenerator = xoroshiro128plus
): Randomizer {
const self = {
next: () => (self.generator.unsafeNext() >>> 0) / 0x100000000,
seed: (seed: number | number[]) => {
self.generator = factory(typeof seed === 'number' ? seed : seed[0]);
},
} as Randomizer & { generator: RandomGenerator };
self.seed(seed);
return self;
function wrapperFactory(generator?: RandomGenerator): Randomizer {
const self = {
next: () => (self.generator.unsafeNext() >>> 0) / 0x100000000,
seed: (seed: number | number[]) => {
self.generator = factory(typeof seed === 'number' ? seed : seed[0]);
},
clone: () => wrapperFactory(self.generator.clone()),
} as Randomizer & { generator: RandomGenerator };
return self;
}

const randomizer = wrapperFactory();
randomizer.seed(seed);
return randomizer;
}
```
185 changes: 185 additions & 0 deletions docs/guide/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,3 +275,188 @@ We will update these docs once a replacement is available.
:::

Congratulations, you should now be able to create any complex object you desire. Happy faking 🥳.

## Create multiple complex objects

Sometimes having a single one of your complex objects isn't enough.
Imagine having a list view/database of some kind you want to populate:

| ID | First Name | Last Name |
| --------- | ---------- | --------- |
| 6fbe024f… | Tatyana | Koch |
| 862f3ccb… | Hans | Donnelly |
| b452acd6… | Judy | Boehm |

The values are directly created using this method:

```ts
import { faker } from '@faker-js/faker';

function createRandomUser(): User {
return {
_id: faker.string.uuid(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
};
}

const users = Array.from({ length: 3 }, () => createRandomUser());
```

After some time you notice that you need a new column `createdDate`.

You modify the method to also create that:

```ts
function createRandomUser(): User {
return {
_id: faker.string.uuid(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
createdDate: faker.date.past(),
};
}
```

Now let's have a look at our table again:

| ID | First Name | Last Name | Created Date |
| --------- | ---------- | --------- | ------------ |
| 6fbe024f… | Tatyana | Koch | 2022-12-28 |
| 62f3ccbf… | Kacie | Pouros | 2023-04-06 |
| 52acd600… | Aron | Von | 2023-05-04 |

Suddenly the second line onwards look different.

Why? Because calling `faker.date.past()` consumes a value from the seed changing all subsequent values.

There are two solutions to that:

1. Set the seed explicitly before creating the data for that row:

```ts
const users = Array.from({ length: 3 }, (_, i) => {
faker.seed(i);
return createRandomUser();
});
```

Which is very straightforward, but comes at the disadvantage, that you change the seed of your faker instance.
This might cause issues, if you have lists of groups that contains lists of users. Each group contains the same users because the seed is reset.

2. Derive a new faker instance for each user you create.

```ts
function createRandomUser(faker: Faker): User {
const derivedFaker = faker.derive();
return {
_id: derivedFaker.string.uuid(),
firstName: derivedFaker.person.firstName(),
lastName: derivedFaker.person.lastName(),
createdDate: derivedFaker.date.past(),
};
}

const users = Array.from({ length: 3 }, () => createRandomUser(faker));
```

The `faker.derive()` call clones the instance and re-initializes the seed of the clone with a generated value from the original.
This decouples the generation of the list from generating a user.
It does not matter how many properties you add to or remove from the `User` the following rows will not change.

::: tip Note
The following is only relevant, if you want to avoid changing your generated objects as much as possible:

When adding one or more new properties, we recommend generating them last, because if you create a new property in the middle of your object, then the properties after that will still change (due to the extra seed consumption).
When removing properties, you can continue calling the old method (or a dummy method) to consume the same amount of seed values.
:::

This also works for deeply nested complex objects:

```ts
function createLegalAgreement(faker: Faker) {
const derivedFaker = faker.derive();
return {
_id: derivedFaker.string.uuid(),
partyA: createRandomUser(derivedFaker),
partyB: createRandomUser(derivedFaker),
};
}

function createRandomUser(faker: Faker): User {
const derivedFaker = faker.derive();
return {
_id: derivedFaker.string.uuid(),
firstName: derivedFaker.person.firstName(),
lastName: derivedFaker.person.lastName(),
createdDate: derivedFaker.date.past(),
address: createRandomAddress(derivedFaker),
};
}

function createRandomAddress(faker: Faker): Address {
const derivedFaker = faker.derive();
return {
_id: derivedFaker.string.uuid(),
streetName: derivedFaker.location.street(),
};
}
```

::: warning Warning
Migrating your existing data generators will still change all data once, but after that they are independent.
So we recommend writing your methods like this from the start.
:::

::: info Note
Depending on your preferences and requirements you can design the methods either like this:

```ts
function createRandomXyz(faker: Faker): Xyz {
return {
_id: faker.string.uuid(),
};
}

createRandomXyz(faker.derive());
createRandomXyz(faker.derive());
createRandomXyz(faker.derive());
```

or this

```ts
function createRandomXyz(faker: Faker): Xyz {
const derivedFaker = faker.derive();
return {
_id: derivedFaker.string.uuid(),
};
}

createRandomXyz(faker);
createRandomXyz(faker);
createRandomXyz(faker);
```

The sole difference being more or less explicit about deriving a faker instance (writing more or less code).
:::

## Create identical complex objects

If you want to create two identical objects, e.g. one to mutate and one to compare it to,
then you can use `faker.clone()` to create a faker instance with the same settings and seed as the original.

```ts
const clonedFaker = faker.clone();
const user1 = createRandomUser(faker);
const user2 = createRandomUser(clonedFaker);
expect(user1).toEqual(user2); ✅

subscribeToNewsletter(user1);
// Check that the user hasn't been modified
expect(user1).toEqual(user2); ✅
```

::: info Note
Calling `faker.clone()` is idempotent. So you can call it as often as you want, it doesn't have an impact on the original faker instance.
:::
15 changes: 15 additions & 0 deletions src/faker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,21 @@ export class Faker extends SimpleFaker {
'This method has been removed. Please use the constructor instead.'
);
}

clone(): Faker {
const instance = new Faker({
locale: this.rawDefinitions,
randomizer: this._randomizer.clone(),
});
instance.setDefaultRefDate(this._defaultRefDate);
return instance;
}

derive(): Faker {
const instance = this.clone();
instance.seed(this.number.int());
return instance;
}
}

export type FakerOptions = ConstructorParameters<typeof Faker>[0];
51 changes: 40 additions & 11 deletions src/internal/mersenne.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,22 @@ export class MersenneTwister19937 {
private mt: number[] = Array.from({ length: this.N }); // the array for the state vector
private mti = this.N + 1; // mti==N+1 means mt[N] is not initialized

/**
* Creates a new instance of MersenneTwister19937.
*
* @param options The required options to initialize the instance.
* @param options.mt The state vector to use. The array will be copied.
* @param options.mti The state vector index to use.
*/
constructor(options?: { mt: number[]; mti: number }) {
if (options != null && 'mt' in options) {
this.mt = [...options.mt];
this.mti = options.mti;
} else {
this.initGenrand(Date.now() ^ (Math.random() * 0x100000000));
}
}

/**
* Returns a 32-bits unsigned integer from an operand to which applied a bit
* operator.
Expand Down Expand Up @@ -166,11 +182,11 @@ export class MersenneTwister19937 {
/**
* Initialize by an array with array-length.
*
* @param initKey is the array for initializing keys
* @param keyLength is its length
* @param initKey Is the array for initializing keys.
*/
initByArray(initKey: number[], keyLength: number): void {
initByArray(initKey: number[]): void {
this.initGenrand(19650218);
const keyLength = initKey.length;
let i = 1;
let j = 0;
let k = this.N > keyLength ? this.N : keyLength;
Expand Down Expand Up @@ -240,11 +256,6 @@ export class MersenneTwister19937 {
// generate N words at one time
let kk: number;

// if initGenrand() has not been called a default initial seed is used
if (this.mti === this.N + 1) {
this.initGenrand(5489);
}

for (kk = 0; kk < this.N - this.M; kk++) {
y = this.unsigned32(
(this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK)
Expand Down Expand Up @@ -324,6 +335,10 @@ export class MersenneTwister19937 {
return (a * 67108864.0 + b) * (1.0 / 9007199254740992.0);
}
// These real versions are due to Isaku Wada, 2002/01/09

clone(): MersenneTwister19937 {
return new MersenneTwister19937({ mt: this.mt, mti: this.mti });
}
}

/**
Expand All @@ -333,10 +348,21 @@ export class MersenneTwister19937 {
* @internal
*/
export function generateMersenne32Randomizer(): Randomizer {
// This method does not expose any internal parameters to users.
const twister = new MersenneTwister19937();
return _generateMersenne32Randomizer(twister);
}

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

/**
* Generates a MersenneTwister19937 randomizer with 32 bits of precision.
*
* @internal
*
* @param twister The twister to use.
*/
function _generateMersenne32Randomizer(
twister: MersenneTwister19937
): Randomizer {
return {
next(): number {
return twister.genrandReal2();
Expand All @@ -345,8 +371,11 @@ export function generateMersenne32Randomizer(): Randomizer {
if (typeof seed === 'number') {
twister.initGenrand(seed);
} else if (Array.isArray(seed)) {
twister.initByArray(seed, seed.length);
twister.initByArray(seed);
}
},
clone(): Randomizer {
return _generateMersenne32Randomizer(twister.clone());
},
};
}
34 changes: 26 additions & 8 deletions src/randomizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,20 @@
* seed: number | number[] = Date.now() ^ (Math.random() * 0x100000000),
* factory: (seed: number) => RandomGenerator = xoroshiro128plus
* ): Randomizer {
* const self = {
* next: () => (self.generator.unsafeNext() >>> 0) / 0x100000000,
* seed: (seed: number | number[]) => {
* self.generator = factory(typeof seed === 'number' ? seed : seed[0]);
* },
* } as Randomizer & { generator: RandomGenerator };
* self.seed(seed);
* return self;
* function wrapperFactory(generator?: RandomGenerator): Randomizer {
* const self = {
* next: () => (self.generator.unsafeNext() >>> 0) / 0x100000000,
* seed: (seed: number | number[]) => {
* self.generator = factory(typeof seed === 'number' ? seed : seed[0]);
* },
* clone: () => wrapperFactory(self.generator.clone()),
* } as Randomizer & { generator: RandomGenerator };
* return self;
* }
*
* const randomizer = wrapperFactory();
* randomizer.seed(seed);
* return randomizer;

Check warning on line 35 in src/randomizer.ts

View check run for this annotation

Codecov / codecov/patch

src/randomizer.ts#L22-L35

Added lines #L22 - L35 were not covered by tests
* }
*
* const randomizer = generatePureRandRandomizer();
Expand Down Expand Up @@ -68,4 +74,16 @@ export interface Randomizer {
* @since 8.2.0
*/
seed(seed: number | number[]): void;

/**
* Creates an exact copy of this Randomizer. Including the current seed state.
*
* @example
* const clone = randomizer.clone();
* randomizer.next() // 0.3404027920160495
* clone.next() // 0.3404027920160495 (same as above)
* randomizer.next() // 0.929890375900335
* clone.next() // 0.929890375900335 (same as above)
*/
clone(): Randomizer;

Check warning on line 88 in src/randomizer.ts

View check run for this annotation

Codecov / codecov/patch

src/randomizer.ts#L77-L88

Added lines #L77 - L88 were not covered by tests
}
Loading

0 comments on commit 93dd407

Please sign in to comment.