- Stage 1
- Authors: WhosyVox and Tab Atkins-Bittner
- Champions: Tab Atkins-Bittner
- Spec Text: currently this README
Historically, JS has offered a very simple API for randomness: a Math.random()
function that returns a random value from a uniform distribution in the range [0,1), with no further details. This is a perfectly adequate primitive, but most actual applications end up having to wrap this into other functions to do something useful, such as obtaining random integers in a particular range, or sampling a normal distribution. While it's not hard to write many of these utility functions, doing so correctly can be non-trivial: off-by-one errors are easy to hit even in simple "simulate a die roll" cases.
This proposal aims to add a handful of new simple uniform-distribution random number methods. (At the request of the committee, further use-cases are being offloaded to separate proposals, notably Random Collection Functions and Random Distributions.)
This proposal builds on the (now Stage 2) Seeded Random proposal, which adds a new Random
namespace object, both to host the Random.Seeded
class itself and to host the Random.Seeded
random methods, so you can call them with a randomly-seeded PRNG created by the User Agent rather than always having to create a Random.Seeded
yourself.
There is a very large family of functions we could potentially include. This proposal is intentionally pared down to the basic set of commonly-written random number functions: random numbers in a range other than 0-1, random integers, random bigints, and random bytes, all drawn from a uniform distribution:
Random.random()
- generates a number in 0-1Random.number(a,b)
- generates a number in a-bRandom.range(...)
- generates a random value that would be generated by the sameIterator.range(...)
Random.int(a,b)
- generates an integer in a-bRandom.bigint(a,b)
- generates a bigint in a-bRandom.bytes(n)
- generates a UInt8Array filled with N random bytesRandom.fillBytes(buf)
- fillsbuf
with random bytes
Returns a random Number
in the range [0, 1)
(that is, including 0, but excluding 1),
identical to Math.random()
,
but with a better suggested algorithm.
If options
is passed
and either provides a valid step
,
or a truthy excludeMin
,
instead act exactly like Random.number(0, 1, options)
.
(Because the range is half-open,
excludeMax
doesn't do anything.)
Note
Issue 19 discusses the exact planned algorithm, using a single 64-bit chunk of randomness to get 2^53 possible values, uniformly spaced. (Unless it needs to act like Random.number(), in which case it's a little different.)
If step
is omitted,
returns a random Number
in the range (lo, hi)
(that is, not containing either lo
or hi
)
with a uniform distribution.
If there are no floats between lo
and hi
,
returns lo
unless the excludeMin
option is true;
otherwise, returns hi
unless the excludeMax
option is true;
otherwise, throws a {{RangeError}}.
Note
Issue 19 discusses the exact planned algorithm, using 2 64-bit chunks of randomness to get up to 2^54 possible values, uniformly spaced.
If step
is passed (directly, or as the step
option)
returns a random Number
of the form lo + N*step
,
in the range [lo, hi]
(that is, potentially containing lo
or hi
).
If the excludeMin
or excludeMax
options are passed,
it avoids returning lo
or hi
, respectively;
if this results in there being no possible values to return,
throws a {{RangeError}}.
Specifically:
- Let
epsilon
be a small value related tostep
, chosen to matchIterator.range
issue #64). - Let
minN
be 0 if theexcludeMin
option is false, or 1 if it's true. - Let
maxN
be the largest integer such thatlo + maxN*step
is less than or equal tohi
, if theexcludeMax
option is false, or less thanhi
, if it's true. - If the
excludeMax
option is false, andlo + maxN*step
is not withinepsilon
ofhi
, butlo + (maxN+1)*step
is (even if it's greater thanhi
), setmaxN
tomaxN+1
. - If
minN
is larger thanmaxN
, throw a RangeError. - Let
N
be a random integer betweenminN
andmaxN
, inclusive. - If
N
ismaxN
, theexcludeMax
option is false, andlo + maxN*step
is withinepsilon
ofhi
, returnhi
. Otherwise, returnlo + N*step
.
Note
This step
/epsilon
behavior is taken directly from CSS's random()
function.
It's also being proposed for Iterator.range()
.
This is an accompaniment to Iterator.range()
,
and will live either in this proposal or the Iterator.range
proposal, whichever advances last.
The arguments to Random.range()
exactly match those of Iterator.range()
,
and are interpreted identically.
The function returns one of the values in the range, chosen uniformly at random.
If the equivalent range would be infinite,
instead throws a RangeError.
Note
This is just a convenience function for what I expect will be a commonly-desired need.
It's sugar for some equivalent Random.number()
with a step
argument.
Returns a random integer Number in the range [lo, hi]
(that is, containing lo
and hi
),
with a uniform distribution.
If excludeMin
or excludeMax
options are passed and true,
it excludes lo
and hi
.
If the range thus contains no possible values,
throws a RangeError.
If step
is passed (directly, or as the step
option),
returns a random integer Number of the form lo + N*step
in the range [lo, hi]
.
(This might not be capable of returning the hi
value,
depending on the chosen hi
and step
.)
If excludeMin
or excludeMax
options are passed and true,
it excludes lo
and hi
.
If the range thus contains no possible values,
throws a RangeError.
Note
Issue 19 discusses what algorithm to use. Current plan uses 2 64-bit chunks if the lo-hi range contains less than 2^63 values. If the range is larger, the algorithm uses approximately N+1 64-bit chunks, where N is the number of 64-bit chunks it takes to represent the range in the first place (with an ignorable chance of rejection, requiring another set of chunks). Either way, every int in the range is possible and has a uniform chance, unlike Random.number(lo, hi, {step:1})
for large ranges. (For .int()
, if the range leaves the safe integer range, the values have non-uniform chances but are relative to the number of integers they represent, so the statistics are as close to uniform as possible.)
Identical to Random.int()
, except it returns BigInts instead.
Returns a Uint8Array
of length n
,
filled with random bytes with a uniform distribution.
Fills the passed TypedArray
or ArrayBuffer
with random bytes with a uniform distribution.
If start
and/or end
are passed,
only fills between those positions,
identical to TypedArray.prototype.fill()
.
Note
Note that "random bytes" produces a uniform distribution of values for the integer types, like Uint8Array
, Int32Array
, etc.
It does not do the same for the float types like Float64Array
.
Those types do not have any straightforward definition of "uniform" over their range of possible values.
If you need something smarter, you'll need to write it yourself to meet your exact use-cases.
All of the above functions will also be defined on the Random.Seeded
class as methods, with identical signatures and behavior. That is, Random.number(...)
and new Random.Seeded(...).number(...)
will both work.
Precise generation algorithms will be defined for the Random.Seeded
methods, to ensure reproducibility. It's recommended that the Random
versions use the same algorithm, but not strictly required; doing so just lets you use an internal Random.Seeded
object and avoid implementing the same function twice.
Why do some functions use an open range and others use a closed range?
The open/closed-ness of the ranges expressed in the algorithms is somewhat incidental. They should all be thought of as closed ranges, that include both endpoints. This allows us to offer an identical set of options across the functions (excludeMin
and excludeMax
).
Being open/closed/half-open is extremely important and visible for most random int use-cases - if simulating a d6, for example, if Random.int(1,6)
uses an open or half-open range it'll give extremely incorrect statistics.
This is not true for almost all random numbers. Most of the time, a Random.number()
range will cover at least one full exponent regime of floats (the range between two consecutive powers of 2), meaning you'll have at least 2^52 possible return values (four quadrillion!). (The planned algorithm maxes out at 2^54, ~18 quadrillion, possible return values.) Even if it doesn't cover a full regime, any realistic scenario is almost certain to cover a large fraction of one, still guaranteeing an extremely large number of possible values, almost certainly in the billions to trillions.
This means it is extremely unlikely for any particular value to be returned, such as the actual min or max value. Except in weird corner cases (ranges where the two endpoints are very close together), you simply can't ever depend on seeing those values in the first place, so whether they're actually possible to see or not is irrelevant. The fact that excludeMin
and excludeMax
don't actually do anything most of the time in Random.number()
is simply undetectable, most of the time.
On a slightly different topic, sometimes most values in a range are perfectly fine, but some cause problems. In general we can't automatically handle this, and you'll just have to do rejection sampling on your own. For example, if you're generating Random.number(1, 10)
but have to avoid the pure integers, that's on you. That sort of thing is fairly rare, anyway.
What's not rare is the endpoints being special in some way. For example, 1 / Random.number(0, 1)
is fine for almost every possible value, except for 0 itself, which'll result in Infinity. You'll only trigger that issue less than 1 quadrillionth of the time - too rare to ever depend on it happening, but not so rare that it's impossible to see at scale. By omitting the endpoints by default in Random.number()
, we avoid an entire class of extremely-rare-but-possible bugs like this. And if the author does specify the excludeMin
/excludeMax
options, we make sure the endpoints don't show up even when the range would otherwise be empty. (This argument doesn't apply to random ints, because if an endpoint is problematic you can just... use the next int over, instead. "The next float over" is much more difficult to express.)
Note that Random.random()
ignores all of that and just has a half-open range. This is because [0-1)
is just such a common, canonical range for this exact use-case that it's hard to justify violating it. Also, there's a super nice, cheap algorithm for generating numbers in this range, and it naturally generates the half-open range.
- Python's
random
module- random float, with any min/max bounds (and any step)
- random ints, with any min/max bounds (and any step)
- random bytes
- random selection (or N selections with replacement) from a list, with optional weights
- random sampling (or N samples, without replacement) from a list, with optional counts
- randomly shuffle an array
- sample various random distributions: binomal, triangular, beta, exponential, gamma, normal, log-normal, von Mises, Pareto, Weibull
- .Net's
Random
class- random ints, with any min/max bounds
- random floats, with any min/max bounds
- random bytes
- randomly shuffle an array
- Haskell's
RandomGen
interface- random u8/u16/u32/u64, either across the full range or between 0 and max
generate two RNGs from the starting RNG(seeded-random use-case)- random from any type with a range (like all the numeric types, enums, etc), or tuples of such
- random bytes
- random floats between 0 and 1
- Ruby's
Random
class- random floats between 0 and max
- random ints between 0 and max
- random bytes
- Common Lisp's
(random n)
function- random ints between 0 and max
- random floats between 0 and max
- Java's
Random
class- random doubles, defaulting to [0,1) but can be given any min/max bounds
- random ints (or longs), with any min/max bounds
- random bools
- random bytes
- sample Gaussian distribution
- JS
genTest
library- random ints (within certain classes)
- random characters
- random "strings" (relatively short but random lengths, random characters)
- random bools
- random selection (N, with replacement) from a list
- custom random generators
- 2024-04: Initial Stage 0 proposal authored.
- 2025-05: Proposal was accepted for Stage 1, somewhat stripped down (TODO: link to notes)