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.
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 }
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); // []
sampleSizeShuffle = { shuffle, sampleSize }, not two loose functions.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.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.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._.shuffle or _.sampleSize.