Shuffle / Sample SizeLoading saved progress…

Shuffle / Sample Size

shuffle returns a new array holding a random reordering of its input, and sampleSize returns n of its elements picked at random with no repeats. They are the array-randomizing helpers from lodash (_.shuffle and _.sampleSize). Here you build both on the Fisher-Yates shuffle — the standard, unbiased way to permute an array — and, importantly, on a random source you pass in, so the output can be tested.

Package both as methods on one object, sampleSizeShuffle. Each takes an injectable rng (defaulting to Math.random) that returns a float in [0, 1). In real use rng is Math.random; in a test you pass a fixed generator so the "random" result becomes a known value you can assert. Neither function may mutate its input.

Signature

shuffle(array, rng = Math.random)        // a new, randomly-ordered copy of array
sampleSize(array, n, rng = Math.random)  // n random elements, no repeats, random order
// rng() returns a float in [0, 1); export sampleSizeShuffle = { shuffle, sampleSize }

Examples

const rng = () => 0; // a fake random source that always returns 0

sampleSizeShuffle.shuffle([1, 2, 3, 4], rng); // [2, 3, 4, 1]  (fixed by this rng)
sampleSizeShuffle.shuffle([]);                // []
sampleSizeShuffle.shuffle([7]);               // [7]
const arr = ['a', 'b', 'c', 'd', 'e'];

sampleSizeShuffle.sampleSize(arr, 2);  // 2 distinct elements, e.g. ['d', 'a']
sampleSizeShuffle.sampleSize(arr, 99); // all 5, shuffled (n clamped to the length)
sampleSizeShuffle.sampleSize(arr, 0);  // []

Notes

  • Package both on one object — export sampleSizeShuffle = { shuffle, sampleSize }, not two loose functions.
  • Inject the randomness — each function takes rng (default Math.random) returning a float in [0, 1). Tests pass a fixed rng to make the output predictable; do not call Math.random directly inside.
  • Never mutate the input — return a new array; the array passed in must read exactly the same after the call, and the result must be a different reference.
  • Use Fisher-Yates — copy, then for i from the last index down to 1, pick j = Math.floor(rng() * (i + 1)) and swap slots i and j. Avoid array.sort(() => rng() - 0.5); it is biased and not deterministic to test.
  • Clamp n in sampleSize — an n above the length returns a full-length sample; an n of 0 or less returns []. Draw without replacement, so no element appears twice.
  • Do not use the built-ins — build the behavior yourself; do not call _.shuffle or _.sampleSize.

FAQ

Why not just shuffle with array.sort(() => Math.random() - 0.5)?
A random comparator is not a consistent ordering, so the sort produces a biased, non-uniform permutation — some orderings come up far more often than others. Fisher-Yates gives each of the n! orderings an equal chance.
How do you test a shuffle when the output is random?
Inject the random source. Both functions take an rng parameter that defaults to Math.random; a test passes a scripted or seeded generator to get a known, asserted output, while production still uses Math.random.
What is the time complexity of shuffle and sampleSize?
shuffle is O(n) — one pass with a constant-time swap per element. sampleSize copies in O(n) and then does only k = min(n, length) swaps, returning a k-element array.
Loading editor…