Skip to content

Commit aefd4f0

Browse files
committed
feat: introduce faker.clone and derive
1 parent bc3ebb7 commit aefd4f0

File tree

10 files changed

+511
-32
lines changed

10 files changed

+511
-32
lines changed

docs/guide/randomizer.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,19 @@ export function generatePureRandRandomizer(
104104
seed: number | number[] = Date.now() ^ (Math.random() * 0x100000000),
105105
factory: (seed: number) => RandomGenerator = xoroshiro128plus
106106
): Randomizer {
107-
const self = {
108-
next: () => (self.generator.unsafeNext() >>> 0) / 0x100000000,
109-
seed: (seed: number | number[]) => {
110-
self.generator = factory(typeof seed === 'number' ? seed : seed[0]);
111-
},
112-
} as Randomizer & { generator: RandomGenerator };
113-
self.seed(seed);
114-
return self;
107+
function wrapperFactory(generator?: RandomGenerator): Randomizer {
108+
const self = {
109+
next: () => (self.generator.unsafeNext() >>> 0) / 0x100000000,
110+
seed: (seed: number | number[]) => {
111+
self.generator = factory(typeof seed === 'number' ? seed : seed[0]);
112+
},
113+
clone: () => wrapperFactory(self.generator.clone()),
114+
} as Randomizer & { generator: RandomGenerator };
115+
return self;
116+
}
117+
118+
const randomizer = wrapperFactory();
119+
randomizer.seed(seed);
120+
return randomizer;
115121
}
116122
```

docs/guide/usage.md

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,3 +275,188 @@ We will update these docs once a replacement is available.
275275
:::
276276

277277
Congratulations, you should now be able to create any complex object you desire. Happy faking 🥳.
278+
279+
## Create multiple complex objects
280+
281+
Sometimes having a single one of your complex objects isn't enough.
282+
Imagine having a list view/database of some kind you want to populate:
283+
284+
| ID | First Name | Last Name |
285+
| --------- | ---------- | --------- |
286+
| 6fbe024f… | Tatyana | Koch |
287+
| 862f3ccb… | Hans | Donnelly |
288+
| b452acd6… | Judy | Boehm |
289+
290+
The values are directly created using this method:
291+
292+
```ts
293+
import { faker } from '@faker-js/faker';
294+
295+
function createRandomUser(): User {
296+
return {
297+
_id: faker.string.uuid(),
298+
firstName: faker.person.firstName(),
299+
lastName: faker.person.lastName(),
300+
};
301+
}
302+
303+
const users = Array.from({ length: 3 }, () => createRandomUser());
304+
```
305+
306+
After some time you notice that you need a new column `createdDate`.
307+
308+
You modify the method to also create that:
309+
310+
```ts
311+
function createRandomUser(): User {
312+
return {
313+
_id: faker.string.uuid(),
314+
firstName: faker.person.firstName(),
315+
lastName: faker.person.lastName(),
316+
createdDate: faker.date.past(),
317+
};
318+
}
319+
```
320+
321+
Now let's have a look at our table again:
322+
323+
| ID | First Name | Last Name | Created Date |
324+
| --------- | ---------- | --------- | ------------ |
325+
| 6fbe024f… | Tatyana | Koch | 2022-12-28 |
326+
| 62f3ccbf… | Kacie | Pouros | 2023-04-06 |
327+
| 52acd600… | Aron | Von | 2023-05-04 |
328+
329+
Suddenly the second line onwards look different.
330+
331+
Why? Because calling `faker.date.past()` consumes a value from the seed changing all subsequent values.
332+
333+
There are two solutions to that:
334+
335+
1. Set the seed explicitly before creating the data for that row:
336+
337+
```ts
338+
const users = Array.from({ length: 3 }, (_, i) => {
339+
faker.seed(i);
340+
return createRandomUser();
341+
});
342+
```
343+
344+
Which is very straightforward, but comes at the disadvantage, that you change the seed of your faker instance.
345+
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.
346+
347+
2. Derive a new faker instance for each user you create.
348+
349+
```ts
350+
function createRandomUser(faker: Faker): User {
351+
const derivedFaker = faker.derive();
352+
return {
353+
_id: derivedFaker.string.uuid(),
354+
firstName: derivedFaker.person.firstName(),
355+
lastName: derivedFaker.person.lastName(),
356+
createdDate: derivedFaker.date.past(),
357+
};
358+
}
359+
360+
const users = Array.from({ length: 3 }, () => createRandomUser(faker));
361+
```
362+
363+
The `faker.derive()` call clones the instance and re-initializes the seed of the clone with a generated value from the original.
364+
This decouples the generation of the list from generating a user.
365+
It does not matter how many properties you add to or remove from the `User` the following rows will not change.
366+
367+
::: tip Note
368+
The following is only relevant, if you want to avoid changing your generated objects as much as possible:
369+
370+
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).
371+
When removing properties, you can continue calling the old method (or a dummy method) to consume the same amount of seed values.
372+
:::
373+
374+
This also works for deeply nested complex objects:
375+
376+
```ts
377+
function createLegalAgreement(faker: Faker) {
378+
const derivedFaker = faker.derive();
379+
return {
380+
_id: derivedFaker.string.uuid(),
381+
partyA: createRandomUser(derivedFaker),
382+
partyB: createRandomUser(derivedFaker),
383+
};
384+
}
385+
386+
function createRandomUser(faker: Faker): User {
387+
const derivedFaker = faker.derive();
388+
return {
389+
_id: derivedFaker.string.uuid(),
390+
firstName: derivedFaker.person.firstName(),
391+
lastName: derivedFaker.person.lastName(),
392+
createdDate: derivedFaker.date.past(),
393+
address: createRandomAddress(derivedFaker),
394+
};
395+
}
396+
397+
function createRandomAddress(faker: Faker): Address {
398+
const derivedFaker = faker.derive();
399+
return {
400+
_id: derivedFaker.string.uuid(),
401+
streetName: derivedFaker.location.street(),
402+
};
403+
}
404+
```
405+
406+
::: warning Warning
407+
Migrating your existing data generators will still change all data once, but after that they are independent.
408+
So we recommend writing your methods like this from the start.
409+
:::
410+
411+
::: info Note
412+
Depending on your preferences and requirements you can design the methods either like this:
413+
414+
```ts
415+
function createRandomXyz(faker: Faker): Xyz {
416+
return {
417+
_id: faker.string.uuid(),
418+
};
419+
}
420+
421+
createRandomXyz(faker.derive());
422+
createRandomXyz(faker.derive());
423+
createRandomXyz(faker.derive());
424+
```
425+
426+
or this
427+
428+
```ts
429+
function createRandomXyz(faker: Faker): Xyz {
430+
const derivedFaker = faker.derive();
431+
return {
432+
_id: derivedFaker.string.uuid(),
433+
};
434+
}
435+
436+
createRandomXyz(faker);
437+
createRandomXyz(faker);
438+
createRandomXyz(faker);
439+
```
440+
441+
The sole difference being more or less explicit about deriving a faker instance (writing more or less code).
442+
:::
443+
444+
## Create identical complex objects
445+
446+
If you want to create two identical objects, e.g. one to mutate and one to compare it to,
447+
then you can use `faker.clone()` to create a faker instance with the same settings and seed as the original.
448+
449+
```ts
450+
const clonedFaker = faker.clone();
451+
const user1 = createRandomUser(faker);
452+
const user2 = createRandomUser(clonedFaker);
453+
expect(user1).toEqual(user2); ✅
454+
455+
subscribeToNewsletter(user1);
456+
// Check that the user hasn't been modified
457+
expect(user1).toEqual(user2); ✅
458+
```
459+
460+
::: info Note
461+
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.
462+
:::

src/faker.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,21 @@ export class Faker extends SimpleFaker {
430430
'This method has been removed. Please use the constructor instead.'
431431
);
432432
}
433+
434+
clone(): Faker {
435+
const instance = new Faker({
436+
locale: this.rawDefinitions,
437+
randomizer: this._randomizer.clone(),
438+
});
439+
instance.setDefaultRefDate(this._defaultRefDate);
440+
return instance;
441+
}
442+
443+
derive(): Faker {
444+
const instance = this.clone();
445+
instance.seed(this.number.int());
446+
return instance;
447+
}
433448
}
434449

435450
export type FakerOptions = ConstructorParameters<typeof Faker>[0];

src/internal/mersenne.ts

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,22 @@ export class MersenneTwister19937 {
8282
private mt: number[] = Array.from({ length: this.N }); // the array for the state vector
8383
private mti = this.N + 1; // mti==N+1 means mt[N] is not initialized
8484

85+
/**
86+
* Creates a new instance of MersenneTwister19937.
87+
*
88+
* @param options The required options to initialize the instance.
89+
* @param options.mt The state vector to use. The array will be copied.
90+
* @param options.mti The state vector index to use.
91+
*/
92+
constructor(options?: { mt: number[]; mti: number }) {
93+
if (options != null && 'mt' in options) {
94+
this.mt = [...options.mt];
95+
this.mti = options.mti;
96+
} else {
97+
this.initGenrand(Date.now() ^ (Math.random() * 0x100000000));
98+
}
99+
}
100+
85101
/**
86102
* Returns a 32-bits unsigned integer from an operand to which applied a bit
87103
* operator.
@@ -166,11 +182,11 @@ export class MersenneTwister19937 {
166182
/**
167183
* Initialize by an array with array-length.
168184
*
169-
* @param initKey is the array for initializing keys
170-
* @param keyLength is its length
185+
* @param initKey Is the array for initializing keys.
171186
*/
172-
initByArray(initKey: number[], keyLength: number): void {
187+
initByArray(initKey: number[]): void {
173188
this.initGenrand(19650218);
189+
const keyLength = initKey.length;
174190
let i = 1;
175191
let j = 0;
176192
let k = this.N > keyLength ? this.N : keyLength;
@@ -240,11 +256,6 @@ export class MersenneTwister19937 {
240256
// generate N words at one time
241257
let kk: number;
242258

243-
// if initGenrand() has not been called a default initial seed is used
244-
if (this.mti === this.N + 1) {
245-
this.initGenrand(5489);
246-
}
247-
248259
for (kk = 0; kk < this.N - this.M; kk++) {
249260
y = this.unsigned32(
250261
(this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK)
@@ -324,6 +335,10 @@ export class MersenneTwister19937 {
324335
return (a * 67108864.0 + b) * (1.0 / 9007199254740992.0);
325336
}
326337
// These real versions are due to Isaku Wada, 2002/01/09
338+
339+
clone(): MersenneTwister19937 {
340+
return new MersenneTwister19937({ mt: this.mt, mti: this.mti });
341+
}
327342
}
328343

329344
/**
@@ -333,10 +348,21 @@ export class MersenneTwister19937 {
333348
* @internal
334349
*/
335350
export function generateMersenne32Randomizer(): Randomizer {
351+
// This method does not expose any internal parameters to users.
336352
const twister = new MersenneTwister19937();
353+
return _generateMersenne32Randomizer(twister);
354+
}
337355

338-
twister.initGenrand(Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER));
339-
356+
/**
357+
* Generates a MersenneTwister19937 randomizer with 32 bits of precision.
358+
*
359+
* @internal
360+
*
361+
* @param twister The twister to use.
362+
*/
363+
function _generateMersenne32Randomizer(
364+
twister: MersenneTwister19937
365+
): Randomizer {
340366
return {
341367
next(): number {
342368
return twister.genrandReal2();
@@ -345,8 +371,11 @@ export function generateMersenne32Randomizer(): Randomizer {
345371
if (typeof seed === 'number') {
346372
twister.initGenrand(seed);
347373
} else if (Array.isArray(seed)) {
348-
twister.initByArray(seed, seed.length);
374+
twister.initByArray(seed);
349375
}
350376
},
377+
clone(): Randomizer {
378+
return _generateMersenne32Randomizer(twister.clone());
379+
},
351380
};
352381
}

src/randomizer.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,20 @@
1919
* seed: number | number[] = Date.now() ^ (Math.random() * 0x100000000),
2020
* factory: (seed: number) => RandomGenerator = xoroshiro128plus
2121
* ): Randomizer {
22-
* const self = {
23-
* next: () => (self.generator.unsafeNext() >>> 0) / 0x100000000,
24-
* seed: (seed: number | number[]) => {
25-
* self.generator = factory(typeof seed === 'number' ? seed : seed[0]);
26-
* },
27-
* } as Randomizer & { generator: RandomGenerator };
28-
* self.seed(seed);
29-
* return self;
22+
* function wrapperFactory(generator?: RandomGenerator): Randomizer {
23+
* const self = {
24+
* next: () => (self.generator.unsafeNext() >>> 0) / 0x100000000,
25+
* seed: (seed: number | number[]) => {
26+
* self.generator = factory(typeof seed === 'number' ? seed : seed[0]);
27+
* },
28+
* clone: () => wrapperFactory(self.generator.clone()),
29+
* } as Randomizer & { generator: RandomGenerator };
30+
* return self;
31+
* }
32+
*
33+
* const randomizer = wrapperFactory();
34+
* randomizer.seed(seed);
35+
* return randomizer;
3036
* }
3137
*
3238
* const randomizer = generatePureRandRandomizer();
@@ -68,4 +74,16 @@ export interface Randomizer {
6874
* @since 8.2.0
6975
*/
7076
seed(seed: number | number[]): void;
77+
78+
/**
79+
* Creates an exact copy of this Randomizer. Including the current seed state.
80+
*
81+
* @example
82+
* const clone = randomizer.clone();
83+
* randomizer.next() // 0.3404027920160495
84+
* clone.next() // 0.3404027920160495 (same as above)
85+
* randomizer.next() // 0.929890375900335
86+
* clone.next() // 0.929890375900335 (same as above)
87+
*/
88+
clone(): Randomizer;
7189
}

0 commit comments

Comments
 (0)