Repository: faker-js/faker (TypeScript)

Issue:
Uniform distribution in helpers.arrayElements results
### Clear and concise description of the problem

I want faker.helpers.arrayElements to always return each element with the same probability.
However, in certain situations, some indexes are picked far more often than others.

For example, when sampling 10 elements from an array with 1000 elements, indexes ending in 9 are are picked ~20 times more often than indexes ending in 1. And when sampling 9 or fewer elements, indexes ending in 1 seem to be never picked at all.

### Suggested solution

The root cause of the problem is that arrayElements picks array indices using `faker.number.float()` [here](https://github.com/faker-js/faker/blob/5f5be203c0c38a485dd4837ba16281e12de5c4c6/src/modules/helpers/index.ts#L598) with the default precision of 0.01
This works fine with an array of 100 elements, but as the length grows, anomalies begin to appear.

The simplest solution would be to replace `this.faker.number.float({ max: 0.99 })` with `Math.random()`. This would, however, break some deterministic test cases.

### Alternative

We could also use something like `this.faker.number.float({ max: 0.999999999, precision: 0.000000001 })` but I'm not sure what the best number of digits is.
The precision could also be conceivably derived from the length of the given array.

If I get some suggestions from maintainers, I may be able to submit a PR.

### Additional context

Code to reproduce issue:

```
const { faker } = require('@faker-js/faker/locale/en')

const arrayLength = 1000
const ids = Array(arrayLength)

for (let i = 0; i < arrayLength; i++) {
    ids[i] = i
}

const countByMod = new Map()

for (let i = 0; i < 1000; i++) {
    for (const id of faker.helpers.arrayElements(ids, 10)) {
        const mod = id % 10
        const count = countByMod.get(mod) ?? 0
        countByMod.set(mod, count + 1)
    }
}

console.log('nines:', countByMod.get(9), 'ones:', countByMod.get(1))
```

Some outputs:
```
nines: 2793 ones: 116
nines: 2760 ones: 126
nines: 2755 ones: 107
```

Both should be much closer to 1000, which is the case when the same code is run on a fixed version of arrayElements:

```
nines: 990 ones: 1098
nines: 1008 ones: 1013
nines: 1023 ones: 943
```


Discussion:
> The simplest solution would be to replace `faker.number.float({ max: 0.99 })` with `Math.random()`. This would, however, break some deterministic test cases.

As you found already out, this is a no-go 🙅

---

Is this also affecting other parts of faker and we need to change something underlying? Or is this only the precision passed inside the `arrayElement` function?

If possible we should define a test to cover the probability with a max delta-error :thinking: 
> Or is this only the precision passed inside the `arrayElement` function?

The problem can be fixed by passing in a different precision from `arrayElements`, doesn't seem to affect anything else.
The float imprecision should already be covered by #1675

- #1675 

~~And the uneven distribution should already be fixed in v8.0.0-alpha.0 because helpers.arrayElement -> number.int -> mersenne without any precision limitations.
Or am I missing something?~~
We should probably rewrite this line to make it more readable.

https://github.com/faker-js/faker/blob/5f5be203c0c38a485dd4837ba16281e12de5c4c6/src/modules/helpers/index.ts#L598
Note i deliberately kept the precision of 0.01 to avoid too much churn on the snapshot tests when i implemented https://github.com/faker-js/faker/pull/1675, but yes after that is merged, that line should be rewritten to take advantage of the higher precision floats now available.
@mjomble I originally wanted wanted to let you write a fix for this, but then spend a lot of time trying to understand the issue/code, that I ended up writing the PR myself. Sorry about that.

Will be fixed in #1770.

---

While working on this issue I also found https://github.com/faker-js/faker/issues/1771

- #1771

If you would like to contribute that, then please a comment there.


> @mjomble I originally wanted wanted to let you write a fix for this, but then spend a lot of time trying to understand the issue/code, that I ended up writing the PR myself. Sorry about that.

Totally fine by me 😄 Thanks for the fix!



Patch:
diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts
index 8d10ced9639..8492a096ee4 100644
--- a/src/modules/helpers/index.ts
+++ b/src/modules/helpers/index.ts
@@ -594,8 +594,9 @@ export class HelpersModule {
     let temp: T;
     let index: number;
 
+    // Shuffle the last `count` elements of the array
     while (i-- > min) {
-      index = Math.floor((i + 1) * this.faker.number.float({ max: 0.99 }));
+      index = this.faker.number.int(i);
       temp = arrayCopy[index];
       arrayCopy[index] = arrayCopy[i];
       arrayCopy[i] = temp;


Test patch:
diff --git a/test/__snapshots__/helpers.spec.ts.snap b/test/__snapshots__/helpers.spec.ts.snap
index c5714ea91a3..ca5ae30a031 100644
--- a/test/__snapshots__/helpers.spec.ts.snap
+++ b/test/__snapshots__/helpers.spec.ts.snap
@@ -195,15 +195,15 @@ exports[`helpers > 1211 > arrayElements > noArgs 1`] = `
 
 exports[`helpers > 1211 > arrayElements > with array 1`] = `
 [
-  "r",
+  "d",
   "o",
-  "l",
+  "r",
   "!",
   "l",
-  "d",
-  "W",
   "H",
+  "W",
   "e",
+  "l",
   "o",
   "l",
   " ",
@@ -213,7 +213,7 @@ exports[`helpers > 1211 > arrayElements > with array 1`] = `
 exports[`helpers > 1211 > arrayElements > with array and count 1`] = `
 [
   "r",
-  "o",
+  " ",
   "!",
 ]
 `;
diff --git a/test/helpers.spec.ts b/test/helpers.spec.ts
index 6ef7c374eff..0e75e10fa80 100644
--- a/test/helpers.spec.ts
+++ b/test/helpers.spec.ts
@@ -292,6 +292,45 @@ describe('helpers', () => {
 
           expect(result).toHaveLength(0);
         });
+
+        it('should return the only element in the array when there is only 1', () => {
+          const testArray = ['hello'];
+          const actual = faker.helpers.arrayElements(testArray);
+          expect(actual).toEqual(testArray);
+        });
+
+        it('should return each element with a somewhat equal distribution with 2 elements', () => {
+          const input = Array.from({ length: 2 }, (_, i) => i);
+          const occurrences = Array.from({ length: 2 }, () => 0);
+
+          for (let i = 0; i < 1000; i++) {
+            const [result] = faker.helpers.arrayElements(input, 1);
+            occurrences[result]++;
+          }
+
+          for (const occurrence of occurrences) {
+            expect(occurrence).toBeGreaterThanOrEqual(400);
+            expect(occurrence).toBeLessThanOrEqual(600);
+          }
+        });
+
+        it.each([10, 100, 1000])(
+          'should return each element with a somewhat equal distribution with %s elements',
+          (length) => {
+            const input = Array.from({ length }, (_, i) => i % 10);
+            const occurrences = Array.from({ length: 10 }, () => 0);
+
+            for (let i = 0; i < 1000; i++) {
+              const [result] = faker.helpers.arrayElements(input, 1);
+              occurrences[result]++;
+            }
+
+            for (const occurrence of occurrences) {
+              expect(occurrence).toBeGreaterThanOrEqual(70);
+              expect(occurrence).toBeLessThanOrEqual(130);
+            }
+          }
+        );
       });
 
       describe('slugify()', () => {