From 65d5802b44c3c2e0dde48a1418023179f33e4eee Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Sun, 11 Dec 2022 10:34:27 +0700 Subject: [PATCH 01/29] feat: add new faker.helpers.weightedArrayElement function --- src/modules/helpers/index.ts | 43 +++++++++++++++++++++ test/__snapshots__/helpers.spec.ts.snap | 6 +++ test/helpers.spec.ts | 51 +++++++++++++++++++++++++ 3 files changed, 100 insertions(+) diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts index 4cf9ad38b45..0b8eb51dbd6 100644 --- a/src/modules/helpers/index.ts +++ b/src/modules/helpers/index.ts @@ -445,6 +445,49 @@ export class HelpersModule { return array[index]; } + /** + * Returns a weighted random element from the given array. Each element of the array should be an array with two elements: the first is the value, the second is an integer weight. + * For example, if there are two elements A and B, with weights 1 and 2 respectively, then the probability of picking A is 1/3 and the probability of picking B is 2/3. + * + * @template T The type of the entries to pick from. + * @param array Array to pick the value from. + * + * @example + * faker.helpers.weightedArrayElement([['sunny', 5], ['rainy', 4],['snowy', 1]]) // 'sunny', 50% of the time, 'rainy' 40% of the time, 'snowy' 10% of the time + * + * @since 8.0.0 + */ + weightedArrayElement( + array: ReadonlyArray<[T, number]> = [] as unknown as ReadonlyArray< + [T, number] + > + ): T { + if (array.length === 0) { + throw new Error( + 'weightedArrayElement expects an array with at least one element' + ); + } + if (!array.every((elt) => elt.length === 2)) { + throw new Error( + 'weightedArrayElement expects an array of [value, weight] pairs' + ); + } + if (!array.every((elt) => typeof elt[1] === 'number' && elt[1] > 0)) { + throw new Error( + 'weightedArrayElement expects an array of [value, weight] pairs where weight is a positive number' + ); + } + const total = array.reduce((acc, [, weight]) => acc + weight, 0); + const random = this.faker.number.int({ min: 0, max: total - 1 }); + let current = 0; + for (const [value, weight] of array) { + current += weight; + if (random < current) { + return value; + } + } + } + /** * Returns a subset with random elements of the given array in random order. * diff --git a/test/__snapshots__/helpers.spec.ts.snap b/test/__snapshots__/helpers.spec.ts.snap index 68fba3bcc08..5eb6b346d8b 100644 --- a/test/__snapshots__/helpers.spec.ts.snap +++ b/test/__snapshots__/helpers.spec.ts.snap @@ -171,6 +171,8 @@ exports[`helpers > 42 > uniqueArray > with array 1`] = ` ] `; +exports[`helpers > 42 > weightedArrayElement > with array 1`] = `"sunny"`; + exports[`helpers > 1211 > arrayElement > noArgs 1`] = `"c"`; exports[`helpers > 1211 > arrayElement > with array 1`] = `"!"`; @@ -356,6 +358,8 @@ exports[`helpers > 1211 > uniqueArray > with array 1`] = ` ] `; +exports[`helpers > 1211 > weightedArrayElement > with array 1`] = `"snowy"`; + exports[`helpers > 1337 > arrayElement > noArgs 1`] = `"a"`; exports[`helpers > 1337 > arrayElement > with array 1`] = `"l"`; @@ -523,3 +527,5 @@ exports[`helpers > 1337 > uniqueArray > with array 1`] = ` "d", ] `; + +exports[`helpers > 1337 > weightedArrayElement > with array 1`] = `"sunny"`; diff --git a/test/helpers.spec.ts b/test/helpers.spec.ts index 24a8d6dac48..e17afc7b4e3 100644 --- a/test/helpers.spec.ts +++ b/test/helpers.spec.ts @@ -59,6 +59,14 @@ describe('helpers', () => { t.it('noArgs').it('with array', 'Hello World!'.split('')); }); + t.describe('weightedArrayElement', (t) => { + t.it('with array', [ + ['sunny', 5], + ['rainy', 4], + ['snowy', 1], + ]); + }); + t.describe('arrayElements', (t) => { t.it('noArgs') .it('with array', 'Hello World!'.split('')) @@ -140,6 +148,49 @@ describe('helpers', () => { }); }); + describe('weightedArrayElement', () => { + it('should return a weighted random element in the array', () => { + const testArray: [string, number][] = [ + ['hello', 10], + ['to', 5], + ['you', 3], + ['my', 2], + ['friend', 1], + ]; + const actual = faker.helpers.weightedArrayElement(testArray); + + expect(testArray.map((a) => a[0])).toContain(actual); + }); + + it('should return the only element in the array when there is only 1', () => { + const testArray: [string, number][] = [['hello', 10]]; + const actual = faker.helpers.weightedArrayElement(testArray); + + expect(actual).toBe('hello'); + }); + it('should throw if the array is empty', () => { + expect(() => faker.helpers.weightedArrayElement([])).to.throw(); + }); + it('should throw if any weight is zero', () => { + const testArray: [string, number][] = [ + ['hello', 0], + ['to', 5], + ]; + expect(() => + faker.helpers.weightedArrayElement(testArray) + ).to.throw(); + }); + it('should throw if any weight is negative', () => { + const testArray: [string, number][] = [ + ['hello', -1], + ['to', 5], + ]; + expect(() => + faker.helpers.weightedArrayElement(testArray) + ).to.throw(); + }); + }); + describe('arrayElements', () => { it('should return a subset with random elements in the array', () => { const testArray = ['hello', 'to', 'you', 'my', 'friend']; From 10d5985cb0d3dd4da955827e30045130993f832a Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Sun, 11 Dec 2022 19:47:27 +0700 Subject: [PATCH 02/29] Update test/helpers.spec.ts Co-authored-by: Shinigami --- test/helpers.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/helpers.spec.ts b/test/helpers.spec.ts index e17afc7b4e3..7924ac923cd 100644 --- a/test/helpers.spec.ts +++ b/test/helpers.spec.ts @@ -180,6 +180,7 @@ describe('helpers', () => { faker.helpers.weightedArrayElement(testArray) ).to.throw(); }); + it('should throw if any weight is negative', () => { const testArray: [string, number][] = [ ['hello', -1], From 5c4292f00918f8f70a0d721a69093be2e489f7f6 Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Sun, 11 Dec 2022 19:47:33 +0700 Subject: [PATCH 03/29] Update test/helpers.spec.ts Co-authored-by: Shinigami --- test/helpers.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/helpers.spec.ts b/test/helpers.spec.ts index 7924ac923cd..d88313abae1 100644 --- a/test/helpers.spec.ts +++ b/test/helpers.spec.ts @@ -171,6 +171,7 @@ describe('helpers', () => { it('should throw if the array is empty', () => { expect(() => faker.helpers.weightedArrayElement([])).to.throw(); }); + it('should throw if any weight is zero', () => { const testArray: [string, number][] = [ ['hello', 0], From 93da6210a0bcbfb98f934a20430aa4b60e53ad51 Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Sun, 11 Dec 2022 19:47:39 +0700 Subject: [PATCH 04/29] Update test/helpers.spec.ts Co-authored-by: Shinigami --- test/helpers.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/helpers.spec.ts b/test/helpers.spec.ts index d88313abae1..da4dc6a368f 100644 --- a/test/helpers.spec.ts +++ b/test/helpers.spec.ts @@ -168,6 +168,7 @@ describe('helpers', () => { expect(actual).toBe('hello'); }); + it('should throw if the array is empty', () => { expect(() => faker.helpers.weightedArrayElement([])).to.throw(); }); From 897c3fb9fba73ba8fe60bef28efb2dbeaa838637 Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Sun, 11 Dec 2022 19:55:36 +0700 Subject: [PATCH 05/29] Update src/modules/helpers/index.ts Co-authored-by: ST-DDT --- src/modules/helpers/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts index 0b8eb51dbd6..5b9427ab5c2 100644 --- a/src/modules/helpers/index.ts +++ b/src/modules/helpers/index.ts @@ -453,7 +453,7 @@ export class HelpersModule { * @param array Array to pick the value from. * * @example - * faker.helpers.weightedArrayElement([['sunny', 5], ['rainy', 4],['snowy', 1]]) // 'sunny', 50% of the time, 'rainy' 40% of the time, 'snowy' 10% of the time + * faker.helpers.weightedArrayElement([['sunny', 5], ['rainy', 4], ['snowy', 1]]) // 'sunny', 50% of the time, 'rainy' 40% of the time, 'snowy' 10% of the time * * @since 8.0.0 */ From d3c4decd7f85877253c8305f9315e48e6dd8a97b Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Sun, 11 Dec 2022 20:14:11 +0700 Subject: [PATCH 06/29] feat(helpers): allow longer tuples --- src/modules/helpers/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts index 5b9427ab5c2..7b2eb4f2668 100644 --- a/src/modules/helpers/index.ts +++ b/src/modules/helpers/index.ts @@ -467,7 +467,7 @@ export class HelpersModule { 'weightedArrayElement expects an array with at least one element' ); } - if (!array.every((elt) => elt.length === 2)) { + if (!array.every((elt) => elt.length >= 2)) { throw new Error( 'weightedArrayElement expects an array of [value, weight] pairs' ); From 33dea713b0683aee8663f67f8bf6e45ce5861deb Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Sun, 11 Dec 2022 20:14:30 +0700 Subject: [PATCH 07/29] feat(helpers): allow floats --- src/modules/helpers/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts index 7b2eb4f2668..857871b5a73 100644 --- a/src/modules/helpers/index.ts +++ b/src/modules/helpers/index.ts @@ -478,7 +478,7 @@ export class HelpersModule { ); } const total = array.reduce((acc, [, weight]) => acc + weight, 0); - const random = this.faker.number.int({ min: 0, max: total - 1 }); + const random = this.faker.number.float({ min: 0, max: total }); let current = 0; for (const [value, weight] of array) { current += weight; @@ -486,6 +486,8 @@ export class HelpersModule { return value; } } + // In case of rounding errors, return the last element + return array[array.length - 1][0]; } /** From 690c871ef57d80cbed130e1ee1e64a67309bca8f Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Sun, 11 Dec 2022 20:18:08 +0700 Subject: [PATCH 08/29] feat(helpers): allow float weights --- src/modules/helpers/index.ts | 2 +- test/helpers.spec.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts index 857871b5a73..4585d6ccc79 100644 --- a/src/modules/helpers/index.ts +++ b/src/modules/helpers/index.ts @@ -446,7 +446,7 @@ export class HelpersModule { } /** - * Returns a weighted random element from the given array. Each element of the array should be an array with two elements: the first is the value, the second is an integer weight. + * Returns a weighted random element from the given array. Each element of the array should be an array with two elements: the first is the value, the second is a weight, which can be an integer or a float. * For example, if there are two elements A and B, with weights 1 and 2 respectively, then the probability of picking A is 1/3 and the probability of picking B is 2/3. * * @template T The type of the entries to pick from. diff --git a/test/helpers.spec.ts b/test/helpers.spec.ts index da4dc6a368f..2283adbbb99 100644 --- a/test/helpers.spec.ts +++ b/test/helpers.spec.ts @@ -162,6 +162,19 @@ describe('helpers', () => { expect(testArray.map((a) => a[0])).toContain(actual); }); + it('should return a weighted random element in the array using floats', () => { + const testArray: [string, number][] = [ + ['hello', 0.1], + ['to', 0.05], + ['you', 0.03], + ['my', 0.02], + ['friend', 0.01], + ]; + const actual = faker.helpers.weightedArrayElement(testArray); + + expect(testArray.map((a) => a[0])).toContain(actual); + }); + it('should return the only element in the array when there is only 1', () => { const testArray: [string, number][] = [['hello', 10]]; const actual = faker.helpers.weightedArrayElement(testArray); From 929cb9a2aafe5ed43f4d526385e721ac9c3756ff Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Sun, 11 Dec 2022 20:23:59 +0700 Subject: [PATCH 09/29] feat(helpers): Object.freeze test --- test/helpers.spec.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/helpers.spec.ts b/test/helpers.spec.ts index 2283adbbb99..1331fff35b1 100644 --- a/test/helpers.spec.ts +++ b/test/helpers.spec.ts @@ -205,6 +205,16 @@ describe('helpers', () => { faker.helpers.weightedArrayElement(testArray) ).to.throw(); }); + it('should not throw with a frozen array', () => { + const testArray: [string, number][] = [ + ['ice', 7], + ['snow', 3], + ]; + const frozenArray = Object.freeze(testArray); + expect(() => + faker.helpers.weightedArrayElement(frozenArray) + ).to.not.throw(); + }); }); describe('arrayElements', () => { From 48da306e5da75c99212576f689d969152fb0a425 Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Sun, 11 Dec 2022 20:24:45 +0700 Subject: [PATCH 10/29] feat(helpers): newline --- test/helpers.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/helpers.spec.ts b/test/helpers.spec.ts index 1331fff35b1..0c453acdb80 100644 --- a/test/helpers.spec.ts +++ b/test/helpers.spec.ts @@ -205,6 +205,7 @@ describe('helpers', () => { faker.helpers.weightedArrayElement(testArray) ).to.throw(); }); + it('should not throw with a frozen array', () => { const testArray: [string, number][] = [ ['ice', 7], From 0c483da962cdda303056a2df0babd6d85da03f15 Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Sun, 11 Dec 2022 20:37:55 +0700 Subject: [PATCH 11/29] feat(helpers): reverse weight and value --- src/modules/helpers/index.ts | 16 +++++------ test/helpers.spec.ts | 54 ++++++++++++++++++------------------ 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts index 4585d6ccc79..c08be63e1c8 100644 --- a/src/modules/helpers/index.ts +++ b/src/modules/helpers/index.ts @@ -446,20 +446,20 @@ export class HelpersModule { } /** - * Returns a weighted random element from the given array. Each element of the array should be an array with two elements: the first is the value, the second is a weight, which can be an integer or a float. + * Returns a weighted random element from the given array. Each element of the array should be an array with two elements: the first is a weight which can be an integer or a float, the second is the value. * For example, if there are two elements A and B, with weights 1 and 2 respectively, then the probability of picking A is 1/3 and the probability of picking B is 2/3. * * @template T The type of the entries to pick from. * @param array Array to pick the value from. * * @example - * faker.helpers.weightedArrayElement([['sunny', 5], ['rainy', 4], ['snowy', 1]]) // 'sunny', 50% of the time, 'rainy' 40% of the time, 'snowy' 10% of the time + * faker.helpers.weightedArrayElement([[5, 'sunny'], [4, 'rainy'], [1, 'snowy']]) // 'sunny', 50% of the time, 'rainy' 40% of the time, 'snowy' 10% of the time * * @since 8.0.0 */ weightedArrayElement( - array: ReadonlyArray<[T, number]> = [] as unknown as ReadonlyArray< - [T, number] + array: ReadonlyArray<[number, T]> = [] as unknown as ReadonlyArray< + [number, T] > ): T { if (array.length === 0) { @@ -472,22 +472,22 @@ export class HelpersModule { 'weightedArrayElement expects an array of [value, weight] pairs' ); } - if (!array.every((elt) => typeof elt[1] === 'number' && elt[1] > 0)) { + if (!array.every((elt) => typeof elt[0] === 'number' && elt[0] > 0)) { throw new Error( 'weightedArrayElement expects an array of [value, weight] pairs where weight is a positive number' ); } - const total = array.reduce((acc, [, weight]) => acc + weight, 0); + const total = array.reduce((acc, [weight]) => acc + weight, 0); const random = this.faker.number.float({ min: 0, max: total }); let current = 0; - for (const [value, weight] of array) { + for (const [weight, value] of array) { current += weight; if (random < current) { return value; } } // In case of rounding errors, return the last element - return array[array.length - 1][0]; + return array[array.length - 1][1]; } /** diff --git a/test/helpers.spec.ts b/test/helpers.spec.ts index 0c453acdb80..d9e39b05a04 100644 --- a/test/helpers.spec.ts +++ b/test/helpers.spec.ts @@ -61,9 +61,9 @@ describe('helpers', () => { t.describe('weightedArrayElement', (t) => { t.it('with array', [ - ['sunny', 5], - ['rainy', 4], - ['snowy', 1], + [5, 'sunny'], + [4, 'rainy'], + [1, 'snowy'], ]); }); @@ -150,33 +150,33 @@ describe('helpers', () => { describe('weightedArrayElement', () => { it('should return a weighted random element in the array', () => { - const testArray: [string, number][] = [ - ['hello', 10], - ['to', 5], - ['you', 3], - ['my', 2], - ['friend', 1], + const testArray: [number, string][] = [ + [10, 'hello'], + [5, 'to'], + [3, 'you'], + [2, 'my'], + [1, 'friend'], ]; const actual = faker.helpers.weightedArrayElement(testArray); - expect(testArray.map((a) => a[0])).toContain(actual); + expect(testArray.map((a) => a[1])).toContain(actual); }); it('should return a weighted random element in the array using floats', () => { - const testArray: [string, number][] = [ - ['hello', 0.1], - ['to', 0.05], - ['you', 0.03], - ['my', 0.02], - ['friend', 0.01], + const testArray: [number, string][] = [ + [0.1, 'hello'], + [0.05, 'to'], + [0.03, 'you'], + [0.02, 'my'], + [0.01, 'friend'], ]; const actual = faker.helpers.weightedArrayElement(testArray); - expect(testArray.map((a) => a[0])).toContain(actual); + expect(testArray.map((a) => a[1])).toContain(actual); }); it('should return the only element in the array when there is only 1', () => { - const testArray: [string, number][] = [['hello', 10]]; + const testArray: [number, string][] = [[10, 'hello']]; const actual = faker.helpers.weightedArrayElement(testArray); expect(actual).toBe('hello'); @@ -187,9 +187,9 @@ describe('helpers', () => { }); it('should throw if any weight is zero', () => { - const testArray: [string, number][] = [ - ['hello', 0], - ['to', 5], + const testArray: [number, string][] = [ + [0, 'hello'], + [5, 'to'], ]; expect(() => faker.helpers.weightedArrayElement(testArray) @@ -197,9 +197,9 @@ describe('helpers', () => { }); it('should throw if any weight is negative', () => { - const testArray: [string, number][] = [ - ['hello', -1], - ['to', 5], + const testArray: [number, string][] = [ + [-1, 'hello'], + [5, 'to'], ]; expect(() => faker.helpers.weightedArrayElement(testArray) @@ -207,9 +207,9 @@ describe('helpers', () => { }); it('should not throw with a frozen array', () => { - const testArray: [string, number][] = [ - ['ice', 7], - ['snow', 3], + const testArray: [number, string][] = [ + [7, 'ice'], + [3, 'snow'], ]; const frozenArray = Object.freeze(testArray); expect(() => From eae262ea796c8a16ebe0fbfbcc867c86fd2779e9 Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Sun, 11 Dec 2022 20:39:05 +0700 Subject: [PATCH 12/29] feat(helpers): use FakerError --- src/modules/helpers/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts index c08be63e1c8..1cebe02ca42 100644 --- a/src/modules/helpers/index.ts +++ b/src/modules/helpers/index.ts @@ -463,17 +463,17 @@ export class HelpersModule { > ): T { if (array.length === 0) { - throw new Error( + throw new FakerError( 'weightedArrayElement expects an array with at least one element' ); } if (!array.every((elt) => elt.length >= 2)) { - throw new Error( + throw new FakerError( 'weightedArrayElement expects an array of [value, weight] pairs' ); } if (!array.every((elt) => typeof elt[0] === 'number' && elt[0] > 0)) { - throw new Error( + throw new FakerError( 'weightedArrayElement expects an array of [value, weight] pairs where weight is a positive number' ); } From 3265564045e9f2d786bd56d5569c73e68557123f Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Sun, 11 Dec 2022 23:48:01 +0700 Subject: [PATCH 13/29] feat(helpers): add small precision --- src/modules/helpers/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts index 1cebe02ca42..37adaa80014 100644 --- a/src/modules/helpers/index.ts +++ b/src/modules/helpers/index.ts @@ -478,7 +478,11 @@ export class HelpersModule { ); } const total = array.reduce((acc, [weight]) => acc + weight, 0); - const random = this.faker.number.float({ min: 0, max: total }); + const random = this.faker.number.float({ + min: 0, + max: total, + precision: 1e-9, + }); let current = 0; for (const [weight, value] of array) { current += weight; From 83eb928b7eb163eb599bf6b32667d170b3e97cb2 Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Mon, 12 Dec 2022 01:00:58 +0700 Subject: [PATCH 14/29] Update src/modules/helpers/index.ts Co-authored-by: Shinigami --- src/modules/helpers/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts index 37adaa80014..85996b909e5 100644 --- a/src/modules/helpers/index.ts +++ b/src/modules/helpers/index.ts @@ -458,9 +458,7 @@ export class HelpersModule { * @since 8.0.0 */ weightedArrayElement( - array: ReadonlyArray<[number, T]> = [] as unknown as ReadonlyArray< - [number, T] - > + array: ReadonlyArray<[number, T]> ): T { if (array.length === 0) { throw new FakerError( From 7d8c152f044b71cefb8d3398edd6e274bb71c705 Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Mon, 12 Dec 2022 01:01:03 +0700 Subject: [PATCH 15/29] Update src/modules/helpers/index.ts Co-authored-by: Shinigami --- src/modules/helpers/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts index 85996b909e5..ac69591157f 100644 --- a/src/modules/helpers/index.ts +++ b/src/modules/helpers/index.ts @@ -457,7 +457,7 @@ export class HelpersModule { * * @since 8.0.0 */ - weightedArrayElement( + weightedArrayElement( array: ReadonlyArray<[number, T]> ): T { if (array.length === 0) { From 68601c58633b9e857ffbe5706f3ff78f16d143b8 Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Mon, 12 Dec 2022 14:18:37 +0700 Subject: [PATCH 16/29] feat(helpers): improve docs and tests --- src/modules/helpers/index.ts | 4 ++-- test/helpers.spec.ts | 18 +++++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts index ac69591157f..7000d70f1f2 100644 --- a/src/modules/helpers/index.ts +++ b/src/modules/helpers/index.ts @@ -467,12 +467,12 @@ export class HelpersModule { } if (!array.every((elt) => elt.length >= 2)) { throw new FakerError( - 'weightedArrayElement expects an array of [value, weight] pairs' + 'weightedArrayElement expects an array of [weight, value] pairs' ); } if (!array.every((elt) => typeof elt[0] === 'number' && elt[0] > 0)) { throw new FakerError( - 'weightedArrayElement expects an array of [value, weight] pairs where weight is a positive number' + 'weightedArrayElement expects an array of [weight, value] pairs where weight is a positive number' ); } const total = array.reduce((acc, [weight]) => acc + weight, 0); diff --git a/test/helpers.spec.ts b/test/helpers.spec.ts index d9e39b05a04..356099159d0 100644 --- a/test/helpers.spec.ts +++ b/test/helpers.spec.ts @@ -183,7 +183,11 @@ describe('helpers', () => { }); it('should throw if the array is empty', () => { - expect(() => faker.helpers.weightedArrayElement([])).to.throw(); + expect(() => faker.helpers.weightedArrayElement([])).toThrowError( + new FakerError( + 'weightedArrayElement expects an array with at least one element' + ) + ); }); it('should throw if any weight is zero', () => { @@ -193,7 +197,11 @@ describe('helpers', () => { ]; expect(() => faker.helpers.weightedArrayElement(testArray) - ).to.throw(); + ).toThrowError( + new FakerError( + 'weightedArrayElement expects an array of [weight, value] pairs where weight is a positive number' + ) + ); }); it('should throw if any weight is negative', () => { @@ -203,7 +211,11 @@ describe('helpers', () => { ]; expect(() => faker.helpers.weightedArrayElement(testArray) - ).to.throw(); + ).toThrowError( + new FakerError( + 'weightedArrayElement expects an array of [weight, value] pairs where weight is a positive number' + ) + ); }); it('should not throw with a frozen array', () => { From 3a18f73770392c676560e7580bc3e8eaf2f9b386 Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Mon, 12 Dec 2022 14:23:52 +0700 Subject: [PATCH 17/29] feat(helpers): format --- src/modules/helpers/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts index 7000d70f1f2..4003fb28e79 100644 --- a/src/modules/helpers/index.ts +++ b/src/modules/helpers/index.ts @@ -457,9 +457,7 @@ export class HelpersModule { * * @since 8.0.0 */ - weightedArrayElement( - array: ReadonlyArray<[number, T]> - ): T { + weightedArrayElement(array: ReadonlyArray<[number, T]>): T { if (array.length === 0) { throw new FakerError( 'weightedArrayElement expects an array with at least one element' From 61d54619f21a3974b2aa213af5283d8bfb6b2b8c Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Tue, 13 Dec 2022 10:40:39 +0700 Subject: [PATCH 18/29] feat(helpers): one more snapshot test --- test/__snapshots__/helpers.spec.ts.snap | 6 ++++++ test/helpers.spec.ts | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/test/__snapshots__/helpers.spec.ts.snap b/test/__snapshots__/helpers.spec.ts.snap index 5eb6b346d8b..1d688327b93 100644 --- a/test/__snapshots__/helpers.spec.ts.snap +++ b/test/__snapshots__/helpers.spec.ts.snap @@ -173,6 +173,8 @@ exports[`helpers > 42 > uniqueArray > with array 1`] = ` exports[`helpers > 42 > weightedArrayElement > with array 1`] = `"sunny"`; +exports[`helpers > 42 > weightedArrayElement > with array with percentages 1`] = `"sunny"`; + exports[`helpers > 1211 > arrayElement > noArgs 1`] = `"c"`; exports[`helpers > 1211 > arrayElement > with array 1`] = `"!"`; @@ -360,6 +362,8 @@ exports[`helpers > 1211 > uniqueArray > with array 1`] = ` exports[`helpers > 1211 > weightedArrayElement > with array 1`] = `"snowy"`; +exports[`helpers > 1211 > weightedArrayElement > with array with percentages 1`] = `"snowy"`; + exports[`helpers > 1337 > arrayElement > noArgs 1`] = `"a"`; exports[`helpers > 1337 > arrayElement > with array 1`] = `"l"`; @@ -529,3 +533,5 @@ exports[`helpers > 1337 > uniqueArray > with array 1`] = ` `; exports[`helpers > 1337 > weightedArrayElement > with array 1`] = `"sunny"`; + +exports[`helpers > 1337 > weightedArrayElement > with array with percentages 1`] = `"sunny"`; diff --git a/test/helpers.spec.ts b/test/helpers.spec.ts index 356099159d0..4ec14758213 100644 --- a/test/helpers.spec.ts +++ b/test/helpers.spec.ts @@ -65,6 +65,12 @@ describe('helpers', () => { [4, 'rainy'], [1, 'snowy'], ]); + + t.it('with array with percentages', [ + [0.5, 'sunny'], + [0.4, 'rainy'], + [0.1, 'snowy'], + ]); }); t.describe('arrayElements', (t) => { From c4a13fa2e5204f7ed6b5e86d3db2734ca04fb716 Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Sat, 24 Dec 2022 16:14:42 +0700 Subject: [PATCH 19/29] feat(helpers): switch to {weight, value} objects --- src/modules/helpers/index.ts | 22 +++++++------ test/helpers.spec.ts | 64 ++++++++++++++++++------------------ 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts index 4003fb28e79..bcfb55b2c75 100644 --- a/src/modules/helpers/index.ts +++ b/src/modules/helpers/index.ts @@ -446,48 +446,50 @@ export class HelpersModule { } /** - * Returns a weighted random element from the given array. Each element of the array should be an array with two elements: the first is a weight which can be an integer or a float, the second is the value. + * Returns a weighted random element from the given array. Each element of the array should be an object with two keys: weight which can be an integer or a float, and value. * For example, if there are two elements A and B, with weights 1 and 2 respectively, then the probability of picking A is 1/3 and the probability of picking B is 2/3. * * @template T The type of the entries to pick from. * @param array Array to pick the value from. * * @example - * faker.helpers.weightedArrayElement([[5, 'sunny'], [4, 'rainy'], [1, 'snowy']]) // 'sunny', 50% of the time, 'rainy' 40% of the time, 'snowy' 10% of the time + * faker.helpers.weightedArrayElement([{ weight: 5, value: 'sunny'}, { weight: 4, value: 'rainy'}, { weight: 1, value: 'snowy'}]) // 'sunny', 50% of the time, 'rainy' 40% of the time, 'snowy' 10% of the time * * @since 8.0.0 */ - weightedArrayElement(array: ReadonlyArray<[number, T]>): T { + weightedArrayElement( + array: ReadonlyArray<{ weight: number; value: T }> + ): T { if (array.length === 0) { throw new FakerError( 'weightedArrayElement expects an array with at least one element' ); } - if (!array.every((elt) => elt.length >= 2)) { + if (!array.every((elt) => elt.value)) { throw new FakerError( - 'weightedArrayElement expects an array of [weight, value] pairs' + 'weightedArrayElement expects an array of {weight, value} objects' ); } - if (!array.every((elt) => typeof elt[0] === 'number' && elt[0] > 0)) { + if (!array.every((elt) => elt.weight > 0)) { throw new FakerError( - 'weightedArrayElement expects an array of [weight, value] pairs where weight is a positive number' + 'weightedArrayElement expects an array of {weight, value} objects where weight is a positive number' ); } - const total = array.reduce((acc, [weight]) => acc + weight, 0); + const total = array.reduce((acc, { weight }) => acc + weight, 0); const random = this.faker.number.float({ min: 0, max: total, precision: 1e-9, }); let current = 0; - for (const [weight, value] of array) { + for (const { weight, value } of array) { current += weight; if (random < current) { return value; } } // In case of rounding errors, return the last element - return array[array.length - 1][1]; + return array[array.length - 1].value; } /** diff --git a/test/helpers.spec.ts b/test/helpers.spec.ts index 4ec14758213..5a212a241b1 100644 --- a/test/helpers.spec.ts +++ b/test/helpers.spec.ts @@ -61,15 +61,15 @@ describe('helpers', () => { t.describe('weightedArrayElement', (t) => { t.it('with array', [ - [5, 'sunny'], - [4, 'rainy'], - [1, 'snowy'], + { weight: 5, value: 'sunny' }, + { weight: 4, value: 'rainy' }, + { weight: 1, value: 'snowy' }, ]); t.it('with array with percentages', [ - [0.5, 'sunny'], - [0.4, 'rainy'], - [0.1, 'snowy'], + { weight: 0.5, value: 'sunny' }, + { weight: 0.4, value: 'rainy' }, + { weight: 0.1, value: 'snowy' }, ]); }); @@ -156,33 +156,33 @@ describe('helpers', () => { describe('weightedArrayElement', () => { it('should return a weighted random element in the array', () => { - const testArray: [number, string][] = [ - [10, 'hello'], - [5, 'to'], - [3, 'you'], - [2, 'my'], - [1, 'friend'], + const testArray = [ + { weight: 10, value: 'hello' }, + { weight: 5, value: 'to' }, + { weight: 3, value: 'you' }, + { weight: 2, value: 'my' }, + { weight: 1, value: 'friend' }, ]; const actual = faker.helpers.weightedArrayElement(testArray); - expect(testArray.map((a) => a[1])).toContain(actual); + expect(testArray.map((a) => a.value)).toContain(actual); }); it('should return a weighted random element in the array using floats', () => { - const testArray: [number, string][] = [ - [0.1, 'hello'], - [0.05, 'to'], - [0.03, 'you'], - [0.02, 'my'], - [0.01, 'friend'], + const testArray = [ + { weight: 0.1, value: 'hello' }, + { weight: 0.05, value: 'to' }, + { weight: 0.03, value: 'you' }, + { weight: 0.02, value: 'my' }, + { weight: 0.01, value: 'friend' }, ]; const actual = faker.helpers.weightedArrayElement(testArray); - expect(testArray.map((a) => a[1])).toContain(actual); + expect(testArray.map((a) => a.value)).toContain(actual); }); it('should return the only element in the array when there is only 1', () => { - const testArray: [number, string][] = [[10, 'hello']]; + const testArray = [{ weight: 10, value: 'hello' }]; const actual = faker.helpers.weightedArrayElement(testArray); expect(actual).toBe('hello'); @@ -197,37 +197,37 @@ describe('helpers', () => { }); it('should throw if any weight is zero', () => { - const testArray: [number, string][] = [ - [0, 'hello'], - [5, 'to'], + const testArray = [ + { weight: 0, value: 'hello' }, + { weight: 5, value: 'to' }, ]; expect(() => faker.helpers.weightedArrayElement(testArray) ).toThrowError( new FakerError( - 'weightedArrayElement expects an array of [weight, value] pairs where weight is a positive number' + 'weightedArrayElement expects an array of {weight, value} objects where weight is a positive number' ) ); }); it('should throw if any weight is negative', () => { - const testArray: [number, string][] = [ - [-1, 'hello'], - [5, 'to'], + const testArray = [ + { weight: -1, value: 'hello' }, + { weight: 5, value: 'to' }, ]; expect(() => faker.helpers.weightedArrayElement(testArray) ).toThrowError( new FakerError( - 'weightedArrayElement expects an array of [weight, value] pairs where weight is a positive number' + 'weightedArrayElement expects an array of {weight, value} objects where weight is a positive number' ) ); }); it('should not throw with a frozen array', () => { - const testArray: [number, string][] = [ - [7, 'ice'], - [3, 'snow'], + const testArray = [ + { weight: 7, value: 'ice' }, + { weight: 3, value: 'snow' }, ]; const frozenArray = Object.freeze(testArray); expect(() => From 14c32de027b6566c7cd50fed9152edee667fed67 Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Mon, 26 Dec 2022 09:11:00 +0700 Subject: [PATCH 20/29] feat(helpers): improve docs --- src/modules/helpers/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts index 2b59a3ee51d..92f2b259bed 100644 --- a/src/modules/helpers/index.ts +++ b/src/modules/helpers/index.ts @@ -446,8 +446,10 @@ export class HelpersModule { } /** - * Returns a weighted random element from the given array. Each element of the array should be an object with two keys: weight which can be an integer or a float, and value. - * For example, if there are two elements A and B, with weights 1 and 2 respectively, then the probability of picking A is 1/3 and the probability of picking B is 2/3. + * Returns a weighted random element from the given array. Each element of the array should be an object with two keys `weight` and `value`. + * Each `weight` key should be a number representing the probability of selecting the value, relative to the sum of the weights. Weights can be any positive float or integer. + * Each `value` key should be the corresponding value. + * For example, if there are two values A and B, with weights 1 and 2 respectively, then the probability of picking A is 1/3 and the probability of picking B is 2/3. * * @template T The type of the entries to pick from. * @param array Array to pick the value from. From f50868b8fd7c6576eea5f7ee8d72af0e526f9af2 Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Mon, 26 Dec 2022 21:30:45 +0700 Subject: [PATCH 21/29] feat(helpers): remove unnecessary check --- src/modules/helpers/index.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts index eff45c8ad8a..15c3870e70d 100644 --- a/src/modules/helpers/index.ts +++ b/src/modules/helpers/index.ts @@ -467,11 +467,6 @@ export class HelpersModule { 'weightedArrayElement expects an array with at least one element' ); } - if (!array.every((elt) => elt.value)) { - throw new FakerError( - 'weightedArrayElement expects an array of {weight, value} objects' - ); - } if (!array.every((elt) => elt.weight > 0)) { throw new FakerError( 'weightedArrayElement expects an array of {weight, value} objects where weight is a positive number' From 9f43631084c89c7191d1e9364b7b78d247aecd1f Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Mon, 26 Dec 2022 22:12:46 +0700 Subject: [PATCH 22/29] feat(helpers): add one more test for falsey values --- test/helpers.spec.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/helpers.spec.ts b/test/helpers.spec.ts index b41774400b5..eff4dd3e008 100644 --- a/test/helpers.spec.ts +++ b/test/helpers.spec.ts @@ -201,6 +201,12 @@ describe('helpers', () => { ); }); + it('should allow falsey values', () => { + const testArray = [{ weight: 1, value: false }]; + const actual = faker.helpers.weightedArrayElement(testArray); + expect(actual).toBe(false); + }); + it('should throw if any weight is zero', () => { const testArray = [ { weight: 0, value: 'hello' }, From 5cb686c2dcc91e2c67c9962aad7587577f9379b3 Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Mon, 26 Dec 2022 23:02:41 +0700 Subject: [PATCH 23/29] Update src/modules/helpers/index.ts Co-authored-by: Shinigami --- src/modules/helpers/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts index 15c3870e70d..c73d181fa7b 100644 --- a/src/modules/helpers/index.ts +++ b/src/modules/helpers/index.ts @@ -455,7 +455,7 @@ export class HelpersModule { * @param array Array to pick the value from. * * @example - * faker.helpers.weightedArrayElement([{ weight: 5, value: 'sunny'}, { weight: 4, value: 'rainy'}, { weight: 1, value: 'snowy'}]) // 'sunny', 50% of the time, 'rainy' 40% of the time, 'snowy' 10% of the time + * faker.helpers.weightedArrayElement([{ weight: 5, value: 'sunny' }, { weight: 4, value: 'rainy' }, { weight: 1, value: 'snowy' }]) // 'sunny', 50% of the time, 'rainy' 40% of the time, 'snowy' 10% of the time * * @since 8.0.0 */ From 7149ed45213917f0adfffa433070273eff0d2816 Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Mon, 26 Dec 2022 23:02:52 +0700 Subject: [PATCH 24/29] Update src/modules/helpers/index.ts Co-authored-by: Shinigami --- src/modules/helpers/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts index c73d181fa7b..fc99516ca2a 100644 --- a/src/modules/helpers/index.ts +++ b/src/modules/helpers/index.ts @@ -469,7 +469,7 @@ export class HelpersModule { } if (!array.every((elt) => elt.weight > 0)) { throw new FakerError( - 'weightedArrayElement expects an array of {weight, value} objects where weight is a positive number' + 'weightedArrayElement expects an array of { weight, value } objects where weight is a positive number' ); } const total = array.reduce((acc, { weight }) => acc + weight, 0); From 01503a24e2e526ca154179113fd0404f86e7334b Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Mon, 26 Dec 2022 23:02:59 +0700 Subject: [PATCH 25/29] Update test/helpers.spec.ts Co-authored-by: Shinigami --- test/helpers.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/helpers.spec.ts b/test/helpers.spec.ts index eff4dd3e008..ed24c3725cb 100644 --- a/test/helpers.spec.ts +++ b/test/helpers.spec.ts @@ -216,7 +216,7 @@ describe('helpers', () => { faker.helpers.weightedArrayElement(testArray) ).toThrowError( new FakerError( - 'weightedArrayElement expects an array of {weight, value} objects where weight is a positive number' + 'weightedArrayElement expects an array of { weight, value } objects where weight is a positive number' ) ); }); From 309e2af39fcddbbfd6a444ee291884a62e6e689f Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Mon, 26 Dec 2022 23:03:05 +0700 Subject: [PATCH 26/29] Update test/helpers.spec.ts Co-authored-by: Shinigami --- test/helpers.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/helpers.spec.ts b/test/helpers.spec.ts index ed24c3725cb..ac39eead22c 100644 --- a/test/helpers.spec.ts +++ b/test/helpers.spec.ts @@ -230,7 +230,7 @@ describe('helpers', () => { faker.helpers.weightedArrayElement(testArray) ).toThrowError( new FakerError( - 'weightedArrayElement expects an array of {weight, value} objects where weight is a positive number' + 'weightedArrayElement expects an array of { weight, value } objects where weight is a positive number' ) ); }); From 3cc43ca990c5209bb2dfcc3b293210abeb33eb62 Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Thu, 29 Dec 2022 22:32:18 +0700 Subject: [PATCH 27/29] Update src/modules/helpers/index.ts Co-authored-by: Shinigami --- src/modules/helpers/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts index fc99516ca2a..ad2e5ba43a2 100644 --- a/src/modules/helpers/index.ts +++ b/src/modules/helpers/index.ts @@ -447,8 +447,10 @@ export class HelpersModule { /** * Returns a weighted random element from the given array. Each element of the array should be an object with two keys `weight` and `value`. - * Each `weight` key should be a number representing the probability of selecting the value, relative to the sum of the weights. Weights can be any positive float or integer. - * Each `value` key should be the corresponding value. + * + * - Each `weight` key should be a number representing the probability of selecting the value, relative to the sum of the weights. Weights can be any positive float or integer. + * - Each `value` key should be the corresponding value. + * * For example, if there are two values A and B, with weights 1 and 2 respectively, then the probability of picking A is 1/3 and the probability of picking B is 2/3. * * @template T The type of the entries to pick from. From 5f535e3c884264a48790c16b6a4228b8d138972a Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Fri, 30 Dec 2022 09:59:05 +0700 Subject: [PATCH 28/29] feat(helpers): delete a stray space --- src/modules/helpers/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts index ad2e5ba43a2..d2635ff980f 100644 --- a/src/modules/helpers/index.ts +++ b/src/modules/helpers/index.ts @@ -450,7 +450,7 @@ export class HelpersModule { * * - Each `weight` key should be a number representing the probability of selecting the value, relative to the sum of the weights. Weights can be any positive float or integer. * - Each `value` key should be the corresponding value. - * + * * For example, if there are two values A and B, with weights 1 and 2 respectively, then the probability of picking A is 1/3 and the probability of picking B is 2/3. * * @template T The type of the entries to pick from. From 3164b38011b767294283177d570711924b145e1e Mon Sep 17 00:00:00 2001 From: Matt Mayer Date: Sun, 1 Jan 2023 10:45:22 +0700 Subject: [PATCH 29/29] feat(helpers): add blank lines to satisfy lint --- src/modules/helpers/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts index e054105f76d..7c5ee9972cd 100644 --- a/src/modules/helpers/index.ts +++ b/src/modules/helpers/index.ts @@ -483,11 +483,13 @@ export class HelpersModule { 'weightedArrayElement expects an array with at least one element' ); } + if (!array.every((elt) => elt.weight > 0)) { throw new FakerError( 'weightedArrayElement expects an array of { weight, value } objects where weight is a positive number' ); } + const total = array.reduce((acc, { weight }) => acc + weight, 0); const random = this.faker.number.float({ min: 0, @@ -501,6 +503,7 @@ export class HelpersModule { return value; } } + // In case of rounding errors, return the last element return array[array.length - 1].value; }