Random Integer in a Range
Off-by-one bugs in random integer helpers are the silent kind: tests pass, distributions look right, but the maximum value never appears. This snippet covers the inclusive `[min, max]` form most people actually want, the unbiased version using `crypto.getRandomValues`, and a helper for picking a uniform random element from an array. Use it for sampling, dice rolls, jitter, and quick demos.
195 views
6
function randomInt(min, max) {
if (max < min) [min, max] = [max, min];
const lo = Math.ceil(min);
const hi = Math.floor(max);
return Math.floor(Math.random() * (hi - lo + 1)) + lo;
}
for (let i = 0; i < 5; i++) console.log(randomInt(1, 6));The classic Math.floor(Math.random() * (hi - lo + 1)) + lo is inclusive on both ends; the + 1 is the part beginners often forget, which silently drops the upper bound from the distribution. Math.ceil(min) and Math.floor(max) normalise non-integer inputs so a caller passing 1.5, 6.5 still draws from {2, 3, 4, 5, 6}. The swap guards against min > max so the function does not silently return only min. Use this for any non-cryptographic case (UI demos, retry jitter, sampling).
function cryptoRandomInt(min, max) {
if (max < min) [min, max] = [max, min];
const lo = Math.ceil(min);
const hi = Math.floor(max);
const range = hi - lo + 1;
if (range <= 0) throw new Error('empty range');
const max32 = 0x100000000; // 2^32
const cap = max32 - (max32 % range);
const buf = new Uint32Array(1);
let n;
do {
crypto.getRandomValues(buf);
n = buf[0];
} while (n >= cap);
return lo + (n % range);
}
for (let i = 0; i < 5; i++) console.log(cryptoRandomInt(1, 6));Math.random() is fast but not cryptographically uniform: a % range shortcut introduces tiny bias when 2^32 is not a multiple of range. Rejection sampling fixes that by drawing fresh 32-bit integers until one falls inside the largest exact multiple of range (cap), then taking the remainder. The loop iterates more than once with vanishingly small probability, so throughput is essentially identical to a single draw. Use this whenever the value is security-relevant (CSRF tokens, nonces, password resets, raffle picks); use the cheaper randomInt everywhere else.
function randomChoice(arr) {
if (!arr.length) return undefined;
return arr[Math.floor(Math.random() * arr.length)];
}
function sample(arr, n) {
const out = [];
const indices = arr.map((_, i) => i);
for (let i = 0; i < Math.min(n, arr.length); i++) {
const j = i + Math.floor(Math.random() * (indices.length - i));
[indices[i], indices[j]] = [indices[j], indices[i]];
out.push(arr[indices[i]]);
}
return out;
}
console.log(randomChoice(['a', 'b', 'c']));
console.log(sample(['a', 'b', 'c', 'd', 'e'], 3));randomChoice is a one-liner that benefits from an explicit empty-array guard so callers do not silently get undefined from arr[NaN]. sample returns n distinct items via a partial Fisher-Yates shuffle on an index array, which avoids mutating the input and runs in O(n) instead of the naive O(n^2) reject-on-collision approach. Operating on indices keeps the helper non-destructive even when the input is large or shared. For weighted sampling, swap the index pick for a cumulative-weight binary search; the structure stays the same.
