Skip to content

Commit 2b35193

Browse files
committed
Generalise functionality to shuffle things in //base
1 parent 8d2c180 commit 2b35193

File tree

2 files changed

+102
-0
lines changed

2 files changed

+102
-0
lines changed

javascript/base/shuffle.js

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2020 Las Venturas Playground. All rights reserved.
2+
// Use of this source code is governed by the MIT license, a copy of which can
3+
// be found in the LICENSE file.
4+
5+
import { random } from 'base/random.js';
6+
7+
// Shuffles the given |value| with the Fisher-Yates algorithm. Any iterable value is supported,
8+
// although recomposition is limited to arrays (default), strings, Set and Map. The given |value|
9+
// will not be modified, although each of its values will not be cloned.
10+
//
11+
// https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
12+
export function shuffle(value) {
13+
if (!isIterable(value))
14+
throw new Error('Only iterable values can be shuffled.');
15+
16+
const values = [ ...value ];
17+
let counter = values.length;
18+
19+
while (counter > 0) {
20+
const index = random(counter--);
21+
const temp = values[counter];
22+
23+
values[counter] = values[index];
24+
values[index] = temp;
25+
}
26+
27+
// Specializations for returning shuffled data in particular data types.
28+
switch (Object.prototype.toString.call(value)) {
29+
case '[object Map]':
30+
return new Map(values);
31+
32+
case '[object Set]':
33+
return new Set(values);
34+
35+
case '[object String]':
36+
return values.join('');
37+
}
38+
39+
// Default to returning the |values| as an array.
40+
return values;
41+
}
42+
43+
// Returns whether the given |value| is iterable.
44+
function isIterable(value) {
45+
return value !== null && typeof value[Symbol.iterator] === 'function';
46+
}

javascript/base/shuffle.test.js

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright 2020 Las Venturas Playground. All rights reserved.
2+
// Use of this source code is governed by the MIT license, a copy of which can
3+
// be found in the LICENSE file.
4+
5+
import { shuffle } from 'base/shuffle.js';
6+
7+
describe('shuffle', it => {
8+
it('should be able to shuffle iterables', assert => {
9+
assert.throws(() => shuffle());
10+
assert.throws(() => shuffle(3.14));
11+
assert.throws(() => shuffle({ yo: 1 }));
12+
assert.throws(() => shuffle(null));
13+
14+
// Arrays
15+
const array = shuffle([ 1, 2, 3, 4, 5 ]);
16+
17+
assert.isTrue(Array.isArray(array));
18+
assert.equal(array.length, 5);
19+
assert.deepEqual(array.sort(), [ 1, 2, 3, 4, 5 ]);
20+
21+
// Maps
22+
const map = shuffle(new Map([ [ 'a', 1 ], [ 'c', 3 ], [ 'b', 2 ] ]));
23+
const sortedMap = [ ...map ].sort((lhs, rhs) => lhs[1] > rhs[1] ? 1 : -1);
24+
25+
assert.instanceOf(map, Map);
26+
assert.equal(map.size, 3);
27+
28+
assert.deepEqual(sortedMap, [ [ 'a', 1 ], [ 'b', 2 ], [ 'c', 3 ] ]);
29+
30+
// Sets
31+
const set = shuffle(new Set([ 'aap', 'noot', 'mies' ]));
32+
33+
assert.instanceOf(set, Set);
34+
assert.equal(set.size, 3);
35+
assert.deepEqual([ ...set ].sort(), [ 'aap', 'mies', 'noot' ]);
36+
37+
// Strings
38+
const string = shuffle('banana');
39+
40+
assert.typeOf(string, 'string');
41+
assert.equal(string.length, 6);
42+
assert.deepEqual([ ...'banana' ].sort(), [ ...string ].sort());
43+
44+
// Distribution
45+
//
46+
// The letters in the string 'banana' can be arranged in 60 different ways, because both the
47+
// 'A' and 'B' occur multiple times. Given 1000 attempts, we should see at least 50 of those
48+
// ways, or there's something seriously wrong with the PRNG.
49+
const distribution = new Set();
50+
51+
for (let iteration = 0; iteration < 1000; ++iteration)
52+
distribution.add(shuffle('banana'));
53+
54+
assert.isAboveOrEqual(distribution.size, 50);
55+
});
56+
});

0 commit comments

Comments
 (0)