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(helpers): add new faker.helpers.weightedArrayElement #1654

Merged
merged 36 commits into from
Jan 2, 2023
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
65d5802
feat: add new faker.helpers.weightedArrayElement function
matthewmayer Dec 11, 2022
10d5985
Update test/helpers.spec.ts
Dec 11, 2022
5c4292f
Update test/helpers.spec.ts
Dec 11, 2022
93da621
Update test/helpers.spec.ts
Dec 11, 2022
897c3fb
Update src/modules/helpers/index.ts
Dec 11, 2022
d3c4dec
feat(helpers): allow longer tuples
matthewmayer Dec 11, 2022
33dea71
feat(helpers): allow floats
matthewmayer Dec 11, 2022
690c871
feat(helpers): allow float weights
matthewmayer Dec 11, 2022
929cb9a
feat(helpers): Object.freeze test
matthewmayer Dec 11, 2022
48da306
feat(helpers): newline
matthewmayer Dec 11, 2022
0c483da
feat(helpers): reverse weight and value
matthewmayer Dec 11, 2022
eae262e
feat(helpers): use FakerError
matthewmayer Dec 11, 2022
3265564
feat(helpers): add small precision
matthewmayer Dec 11, 2022
83eb928
Update src/modules/helpers/index.ts
Dec 11, 2022
7d8c152
Update src/modules/helpers/index.ts
Dec 11, 2022
68601c5
feat(helpers): improve docs and tests
matthewmayer Dec 12, 2022
3a18f73
feat(helpers): format
matthewmayer Dec 12, 2022
0c8a489
Merge branch 'next' into chore/weightedArrayElement
ST-DDT Dec 12, 2022
61d5461
feat(helpers): one more snapshot test
matthewmayer Dec 13, 2022
c4a13fa
feat(helpers): switch to {weight, value} objects
matthewmayer Dec 24, 2022
1640fbd
Merge branch 'next' into chore/weightedArrayElement
Dec 24, 2022
14c32de
feat(helpers): improve docs
matthewmayer Dec 26, 2022
c8efa28
Merge branch 'next' into chore/weightedArrayElement
ST-DDT Dec 26, 2022
f50868b
feat(helpers): remove unnecessary check
matthewmayer Dec 26, 2022
9f43631
feat(helpers): add one more test for falsey values
matthewmayer Dec 26, 2022
5cb686c
Update src/modules/helpers/index.ts
Dec 26, 2022
7149ed4
Update src/modules/helpers/index.ts
Dec 26, 2022
01503a2
Update test/helpers.spec.ts
Dec 26, 2022
309e2af
Update test/helpers.spec.ts
Dec 26, 2022
3cc43ca
Update src/modules/helpers/index.ts
Dec 29, 2022
5f535e3
feat(helpers): delete a stray space
matthewmayer Dec 30, 2022
292400e
Merge branch 'next' into chore/weightedArrayElement
ST-DDT Dec 30, 2022
3c94fcc
Merge branch 'next' into chore/weightedArrayElement
ST-DDT Dec 31, 2022
ce272b3
Merge branch 'next' into chore/weightedArrayElement
ST-DDT Dec 31, 2022
3164b38
feat(helpers): add blank lines to satisfy lint
matthewmayer Jan 1, 2023
c0ba03b
Merge branch 'next' into chore/weightedArrayElement
ST-DDT Jan 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions src/modules/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,55 @@ export class HelpersModule {
return array[index];
}

/**
* 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.
*
* @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
*
* @since 8.0.0
*/
weightedArrayElement<T>(
array: ReadonlyArray<{ weight: number; value: T }>
ST-DDT marked this conversation as resolved.
Show resolved Hide resolved
): T {
if (array.length === 0) {
throw new FakerError(
'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,
max: total,
precision: 1e-9,
});
let current = 0;
for (const { weight, value } of array) {
current += weight;
if (random < current) {
ST-DDT marked this conversation as resolved.
Show resolved Hide resolved
return value;
}
}

// In case of rounding errors, return the last element
return array[array.length - 1].value;
Shinigami92 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Returns a subset with random elements of the given array in random order.
*
Expand Down
12 changes: 12 additions & 0 deletions test/__snapshots__/helpers.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ 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`] = `"!"`;
Expand Down Expand Up @@ -368,6 +372,10 @@ 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"`;
Expand Down Expand Up @@ -541,3 +549,7 @@ exports[`helpers > 1337 > uniqueArray > with array 1`] = `
"d",
]
`;

exports[`helpers > 1337 > weightedArrayElement > with array 1`] = `"sunny"`;

exports[`helpers > 1337 > weightedArrayElement > with array with percentages 1`] = `"sunny"`;
102 changes: 102 additions & 0 deletions test/helpers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,20 @@ describe('helpers', () => {
t.it('noArgs').it('with array', 'Hello World!'.split(''));
});

t.describe('weightedArrayElement', (t) => {
t.it('with array', [
{ weight: 5, value: 'sunny' },
{ weight: 4, value: 'rainy' },
{ weight: 1, value: 'snowy' },
]);

t.it('with array with percentages', [
{ weight: 0.5, value: 'sunny' },
{ weight: 0.4, value: 'rainy' },
{ weight: 0.1, value: 'snowy' },
]);
});
ST-DDT marked this conversation as resolved.
Show resolved Hide resolved

t.describe('arrayElements', (t) => {
t.it('noArgs')
.it('with array', 'Hello World!'.split(''))
Expand Down Expand Up @@ -145,6 +159,94 @@ describe('helpers', () => {
});
});

describe('weightedArrayElement', () => {
it('should return a weighted random element in the array', () => {
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.value)).toContain(actual);
});

it('should return a weighted random element in the array using floats', () => {
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.value)).toContain(actual);
});

it('should return the only element in the array when there is only 1', () => {
const testArray = [{ weight: 10, value: 'hello' }];
const actual = faker.helpers.weightedArrayElement(testArray);

expect(actual).toBe('hello');
});

it('should throw if the array is empty', () => {
expect(() => faker.helpers.weightedArrayElement([])).toThrowError(
new FakerError(
'weightedArrayElement expects an array with at least one element'
)
);
});

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' },
{ weight: 5, value: 'to' },
];
expect(() =>
faker.helpers.weightedArrayElement(testArray)
).toThrowError(
new FakerError(
'weightedArrayElement expects an array of { weight, value } objects where weight is a positive number'
)
);
});

it('should throw if any weight is negative', () => {
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 } objects where weight is a positive number'
)
);
});

it('should not throw with a frozen array', () => {
const testArray = [
{ weight: 7, value: 'ice' },
{ weight: 3, value: 'snow' },
];
const frozenArray = Object.freeze(testArray);
expect(() =>
faker.helpers.weightedArrayElement(frozenArray)
).to.not.throw();
});
ST-DDT marked this conversation as resolved.
Show resolved Hide resolved
});
Shinigami92 marked this conversation as resolved.
Show resolved Hide resolved

describe('arrayElements', () => {
it('should return a subset with random elements in the array', () => {
const testArray = ['hello', 'to', 'you', 'my', 'friend'];
Expand Down