Skip to content

Commit a4896cf

Browse files
✨ feat(shuffled): First draft.
This allows to shuffle iterables while consuming them. Fixes #59.
1 parent e26ca00 commit a4896cf

File tree

6 files changed

+123
-4
lines changed

6 files changed

+123
-4
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ Randomness algorithms for JavaScript.
1212
See [docs](https://aureooms.github.io/js-random).
1313
Parent is [@aureooms/js-algorithms](https://aureooms.github.io/js-algorithms).
1414

15+
> :warning: Depending on your environment, the code may require
16+
> `regeneratorRuntime` to be defined, for instance by importing
17+
> [regenerator-runtime/runtime](https://www.npmjs.com/package/regenerator-runtime).
18+
1519
```js
1620
import {
1721
randint , // randint(i, j) -> [i, j[ \cap ZZ

doc/manual/usage.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Usage
2-
The code needs a ES2015+ polyfill to work, for example
3-
[regenerator-runtime/runtime](https://babeljs.io/docs/usage/polyfill).
2+
3+
> :warning: Depending on your environment, the code may require
4+
> `regeneratorRuntime` to be defined, for instance by importing
5+
> [regenerator-runtime/runtime](https://www.npmjs.com/package/regenerator-runtime).
6+
47
```js
58
require( 'regenerator-runtime/runtime' ) ;
69
// or
@@ -9,7 +12,7 @@ import 'regenerator-runtime/runtime.js' ;
912

1013
Then
1114
```js
12-
const random = require( '@aureooms/js-random' ) ;
15+
const { ... } = require( '@aureooms/js-random' ) ;
1316
// or
14-
import * as random from '@aureooms/js-random' ;
17+
import { ... } from '@aureooms/js-random' ;
1518
```

src/api/shuffled.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import _fisheryates_inside_out from '../kernel/_fisheryates_inside_out.js';
2+
import randint from './randint.js';
3+
4+
/**
5+
* Given an input iterable, constructs an array containing the elements of the
6+
* input shuffled uniformly at random.
7+
*
8+
* @function
9+
* @param {Iterable} iterable The input iterable.
10+
* @return {Array} The constructed array.
11+
*/
12+
const shuffled = _fisheryates_inside_out(randint);
13+
export default shuffled;

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ export {default as random} from './api/random.js';
55
export {default as randrange} from './api/randrange.js';
66
export {default as sample} from './api/sample.js';
77
export {default as shuffle} from './api/shuffle.js';
8+
export {default as shuffled} from './api/shuffled.js';
89
export {default as _choice} from './kernel/_choice.js';
910
export {default as _fisheryates} from './kernel/_fisheryates.js';
11+
export {default as _fisheryates_inside_out} from './kernel/_fisheryates_inside_out.js';
1012
export {default as _randfloat} from './kernel/_randfloat.js';
1113
export {default as _randint} from './kernel/_randint.js';
1214
export {default as _shuffle} from './kernel/_shuffle.js';

src/kernel/_fisheryates_inside_out.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* Shuffle elements of an iterable using an inside-out implementation of the
3+
* Fisher-Yates method.
4+
*
5+
* One can observe that if the input contains n elements, the loop has exactly
6+
* n! possible outcomes: one for the first iteration, two for the second, three
7+
* for the third, etc., the number of outcomes of a loop being the product of
8+
* the number of outcomes for each iteration. Given a perfect randint function,
9+
* each iteration's outcomes are equally likely, and independent of other
10+
* iterations outcomes. The proof below shows that these outcomes are
11+
* distinct.
12+
*
13+
* To see that this method yields the correct result (assume perfect randint):
14+
* 1. Observe that it is correct when the input is empty.
15+
* 2. By induction:
16+
* - Induction hypothesis: assume it is correct when the input consists of
17+
* n elements.
18+
* - We almost insert the (n+1)th element at one of the n+1 possible
19+
* insertion position in the output array. Almost because we move the
20+
* element that is at the insertion position at the end instead of
21+
* shifting the elements right of the insertion position to make room for
22+
* the inserted element.
23+
* - Ideally, since we inserted the last element at one of the n+1
24+
* positions, we would like that the elements inserted earlier form one
25+
* of n! permutations uniformly at random after moving the element under
26+
* the insertion position. This is true because the permutations that we
27+
* obtain after this move are in one-to-one correspondance with the n!
28+
* distinct permutations that can be obtained before the move. These are
29+
* equally likely to be produced by the induction hypothesis.
30+
*
31+
* @param {Function} randint The randint function.
32+
* @return {Function} The sampling function.
33+
*/
34+
const _fisheryates_inside_out = (randint) => {
35+
/**
36+
* Given an input iterable, constructs an array containing the elements of
37+
* the input shuffled uniformly at random.
38+
*
39+
* @param {Iterable} iterable The input iterable.
40+
* @param {Array} [output=[]] The constructed array.
41+
* @return {Array} The constructed array.
42+
*/
43+
const shuffled = (iterable, output = []) => {
44+
let n = 0;
45+
for (const item of iterable) {
46+
const i = randint(-1, n);
47+
if (i === -1) output.push(item);
48+
else {
49+
output.push(output[i]);
50+
output[i] = item;
51+
}
52+
53+
++n;
54+
}
55+
56+
return output;
57+
};
58+
59+
return shuffled;
60+
};
61+
62+
export default _fisheryates_inside_out;

test/src/shuffled.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import test from 'ava';
2+
import {shuffled, _fisheryates_inside_out, randint} from '../../src/index.js';
3+
4+
import {list, range, sorted} from '@aureooms/js-itertools';
5+
import {increasing} from '@aureooms/js-compare';
6+
7+
const macro = (t, _, shuffle, i, j) => {
8+
const input = list(range(i, j));
9+
const output = shuffled(range(i, j));
10+
t.is(output.length, input.length);
11+
t.deepEqual(sorted(increasing, output), sorted(increasing, input));
12+
};
13+
14+
macro.title = (title, shuffle_name, _, i, j) =>
15+
title || `[${n}] shuffled ( ${shuffle_name}, ${i}, ${j} )`;
16+
17+
const n = 100;
18+
19+
const params = [
20+
[0, n],
21+
[20, n],
22+
[0, n - 20],
23+
[10, n - 10],
24+
];
25+
26+
const algorithms = [
27+
['inside-out Fisher-Yates', _fisheryates_inside_out(randint)],
28+
['API', shuffled],
29+
];
30+
31+
for (const [name, algorithm] of algorithms) {
32+
for (const [i, j] of params) {
33+
test(macro, name, algorithm, i, j);
34+
}
35+
}

0 commit comments

Comments
 (0)