Trying to generate a number using MAX_SAFE_INTEGER I noticed something strange, I’m sure it has to do with the way numbers are stored in javascript, but I don’t understand what exactly it is.
Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) // Always returns an odd number
Math.floor(Math.random() * (Number.MAX_SAFE_INTEGER - 1)) // Returns an odd number 75% of the time
Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER) // Has a 50/50 chance to return odd or even
How can this behavior be explained and what would be the largest integer you can use in Math.floor to get a 50/50 ratio?
class RandomNumberCounter {
constructor() {
this.evenCount = 0;
this.oddCount = 0;
}
generateNumbersAndCount() {
for (let i = 0; i < 10000; i++) {
const randomNumber = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
if (randomNumber % 2 === 0) {
this.evenCount++;
} else {
this.oddCount++;
}
}
}
printCounts() {
console.log("Number of even numbers:", this.evenCount);
console.log("Number of odd numbers:", this.oddCount);
}
}
const randomNumberCounter = new RandomNumberCounter();
randomNumberCounter.generateNumbersAndCount();
randomNumberCounter.printCounts();
3
Answers
@pointy highlighted the solution well. I’m just leaving here a working code that brings an equal ratio of even and odd numbers.
The larger the boundary value of random number generation, the higher the probability of the result being even. If you double this value, the ratio will remain equal after many runs.
So here the emphasis remains on fractions due to JavaScript’s floating-point number representation.
Think about what binary floating point representation means, when you’re dealing with a value between 0 (inclusive) and 1 (exclusive). The integer part is always 0 (an even value), and the fractional part is represented by a sum of inverses of powers of 2: 1/2, 1/4, 1/8, 1/16, etc. Each 1 bit in the mantissa represents the presence of one of those inverse powers of two in the value.
Thus, any random mantissa is going to be the inverse of an even value, because all the denominators are even. When you divide that even sum into an odd value (
Number.MAX_SAFE_INTEGER
), you have to get an odd result, because you can’t multiply an even number by an even number and get an odd result.First, you should multiply by 253 (
Number.MAX_SAFE_INTEGER + 1
) to get all 53 bits from aMath.random
implementation that uses the full double precision. 253−1 probably doesn’t hurt much if it ever produces any different results at all, but I don’t feel like doing the necessary floating-point analysis – it’s much easier to just pick the solution that’s obviously correct.But then what’s the issue? Well, your original code works fine on Firefox and Safari! It’s just that V8 (i.e. Chrome and derivatives) uses 52 bits instead of 53.
(Firefox, Safari)
(Chrome)
(The reason that you can store 53 bits accurately with a significand with 52 bits of storage is that the integer part is implicitly a 1 that can be scaled to the right place by the exponent, same as why
Number.MAX_SAFE_INTEGER
is what it is.)Looking at the relevant part of V8’s implementation, I assume the only reason it does this is for performance – by fixing the exponent to make the range [1, 2), it can insert the random bits directly into the double instead of having to perform a multiplication.
So to answer your question,
At most 252, but I wouldn’t count on
Math.random
having more than 32 bits of randomness unless you’re only targeting one engine (V8 changed to 52 in 2015, for example), or even on it being good enough randomness for a particular purpose – none of this stuff is in the spec.You might want to consider implementing a known PRNG in JavaScript and seeding it with strong randomness from
crypto.getRandomValues
.