Math.random()
быстро и подходит для многих целей, но это не подходит, если вам нужны криптографически безопасные значения (это небезопасно) или если вам нужны целые числа из абсолютно равномерного несмещенного распределения (подход умножения, используемый в других ответах выдает определенные значения чуть чаще, чем другие).
В таких случаях мы можем использовать crypto.getRandomValues()
для генерации безопасных целых чисел и отклонения любых сгенерированных значений, которые мы не можем равномерно отобразить в целевой диапазон. Это будет медленнее, но не должно быть значительным, если вы не генерируете очень большое количество значений.
Чтобы прояснить проблему смещения распределения, рассмотрим случай, когда мы хотим сгенерировать значение от 1 до 5, но у нас есть генератор случайных чисел, который выдает значения от 1 до 16 (4-битное значение). Мы хотим иметь одинаковое количество сгенерированных значений, сопоставляемых с каждым выходным значением, но 16 не делится поровну на 5: остается остаток от 1. Поэтому нам нужно отклонить 1 из возможных сгенерированных значений и продолжить только тогда, когда мы получим одно из 15 меньших значений, которые могут быть равномерно отображены в нашем целевом диапазоне. Наше поведение может выглядеть следующим образом:
Generate a 4-bit integer in the range 1-16.
If we generated 1, 6, or 11 then output 1.
If we generated 2, 7, or 12 then output 2.
If we generated 3, 8, or 13 then output 3.
If we generated 4, 9, or 14 then output 4.
If we generated 5, 10, or 15 then output 5.
If we generated 16 then reject it and try again.
В следующем коде используется аналогичная логика, но вместо этого генерируется 32-разрядное целое число, поскольку это самый большой общий размер целого числа, который может быть представлен стандартным типом JavaScript number
. (Это можно изменить, чтобы использовать BigInt
s, если вам нужен больший диапазон.) Независимо от выбранного диапазона, доля сгенерированных значений, которые отклоняются, всегда будет меньше 0,5, поэтому ожидаемое количество отклонений всегда будет меньше чем 1,0 и обычно близко к 0,0; вам не нужно беспокоиться о том, что оно зациклится навсегда.
const randomInteger = (min, max) => {
const range = max - min;
const maxGeneratedValue = 0xFFFFFFFF;
const possibleResultValues = range + 1;
const possibleGeneratedValues = maxGeneratedValue + 1;
const remainder = possibleGeneratedValues % possibleResultValues;
const maxUnbiased = maxGeneratedValue - remainder;
if (!Number.isInteger(min) || !Number.isInteger(max) ||
max > Number.MAX_SAFE_INTEGER || min < Number.MIN_SAFE_INTEGER) {
throw new Error('Arguments must be safe integers.');
} else if (range > maxGeneratedValue) {
throw new Error(`Range of ${range} (from ${min} to ${max}) > ${maxGeneratedValue}.`);
} else if (max < min) {
throw new Error(`max (${max}) must be >= min (${min}).`);
} else if (min === max) {
return min;
}
let generated;
do {
generated = crypto.getRandomValues(new Uint32Array(1))[0];
} while (generated > maxUnbiased);
return min + (generated % possibleResultValues);
};
console.log(randomInteger(-8, 8)); // -2
console.log(randomInteger(0, 0)); // 0
console.log(randomInteger(0, 0xFFFFFFFF)); // 944450079
console.log(randomInteger(-1, 0xFFFFFFFF));
// Error: Range of 4294967296 covering -1 to 4294967295 is > 4294967295.
console.log(new Array(12).fill().map(n => randomInteger(8, 12)));
// [11, 8, 8, 11, 10, 8, 8, 12, 12, 12, 9, 9]