Write a truly inclusive random method for javascript

Viewed 7326

Javascript's MATH object has a random method that returns from the set [0,1) 0 inclusive, 1 exclusive. Is there a way to return a truly random method that includes 1.

e.g.

var rand = MATH.random()*2;

if(rand > 1)
{
   rand = MATH.floor(rand);
}

return rand; 

While this always returns a number from the set [0,1] it is not truly random.

10 Answers

This will return [0,1] inclusive:

if(MATH.random() == 0)
    return 1;
else
    return MATH.random();

Explanation: If the first call to random() returns 0, return 1. Otherwise, call random again, which will be [0,1). Therefore, it will return [0,1] all inclusive.

To put it bluntly, what you're trying to do doesn't make sense.

Remember that under a continuous probability distribution, the probability of getting a specific value is infinitesimal, so mathematically speaking you will never see the exact value of 1.

Of course, in the world of computers, the distribution of an RNG isn't truly continuous, so it's "possible" that you'll encounter a specific value (as silly as that sounds), but the probability will be so vanishingly small that in practice you will never observe it.

To make the point a bit more clearly: if you did manage to write such a function in terms of double-precision floats, the probability of getting exactly 1 would be approximately 2-64. If you called the function 1 million times per second, you would have to wait around 600,000 years before you got a 1.

The Math.random function returns a random number between 0 and 1, where 0 is inclusive and 1 is exclusive. This means that the only way to properly distribute the random numbers as integers in an interval is to use an exclusive upper limit.

To specify an inclusive upper limit, you just add one to it to make it exclusive in the calculation. This will distribute the random numbers correctly between 7 and 12, inclusive:

var min = 7;
var max = 12;
var rnd = min + Math.floor(Math.random() * (max - min + 1));

You want it to include 1?

return 1 - Math.random();

However, I think this is one of those questions which hints at other problems. Why do you need to include 1? There's probably a better way to do it.

This should work properly.

function random_inclusive () {
    while (true) {
        var value = Math.random() + (Math.random() < 0.5? 0: 1);
        if (value <= 1) {
            return value;
        }
    }
}

What we do here is generate additional single random bit to expand PRNG range to [0, 2). Then we simply discard values in (1, 2) and retry until our value hits in [0, 1].

Notice that this method calls Math.random() 4 times on average.

Alternatively, we can speed up things twice by the cost of 1 bit of precision:

var value = Math.random() * 2;

For those who want to test, here is the way. Just assume that Math.random() only has 1 bit of precision and generates either 0 or 0.5. So on the output we should have uniform distribution among values 0, 0.5, and 1.

function random_inclusive_test (samples) {
    var hits = {};
    for (var i=0; i<samples; i++) {
        while (true) {
            var value =
                (Math.random() < 0.5? 0: 0.5) +
                (Math.random() < 0.5? 0: 1);
            if (value <= 1) {
                break;
            }
        }
        if (!hits[value]) {
            hits[value] = 1;
        }
        else {
            hits[value]++;
        }
    }
    console.log(hits);
}
random_inclusive_test(300000);

The solution I found was to use trigonometric equations.

Because sine oscillates from Math.sin(0) = 0 and Math.sin(90) = 1. This repeats until 360 degrees which is equal to 0. However, 360 is still not highly precise so use radians which is 2 * Math.PI. You only need to take the absolute value to get rid of the negative values.

So,

double angle = 2 * Math.PI * Math.random() 
double inclusive = Math.abs(Math.sin(angle)) // Math.cos(angle) also works.

If you do need generating [0-1] inclusive, it can be achieved via specifying a precision, like when generating an integer number with an inclusive limit, and divide it back afterwards.
Because we are in the world of computers, I suggest a using a precision which is a power of 2 so the division at the end will not actually touch the digits.
The upper limit is not specified other than by Number itself being a 64-bit double-precision number, which means 53 significant digits. But the generator itself is implementation-dependent, and in reality it may or may not be capable of generating every single floating point number between 0 and 1.

To actually see 0 and 1 generated, typically a far lower precision is needed, like 20-something bits. This time-limited (stops after 30 seconds) snippet succeeds for me up to 25-bits, sometimes even 26. The default is 23, that's really expected to finish fast.

const precision=1<<parseInt(prompt("Bits (23)?") || "23");
console.log("hex:",precision.toString(16),"dec:",precision);
const limit=precision+1;
function next0to1() {
  return Math.floor(Math.random()*limit)/precision;
}

let start=Date.now();
let stop=start+30000;
let count=0;
while(Date.now()<stop){
  count++;
  if(next0to1()===0){
    console.log("0",count,Date.now()-start);
    break;
  }
}
while(Date.now()<stop){
  count++;
  if(next0to1()===1){
    console.log("1",count,Date.now()-start);
    break;
  }
}
console.log("attempts:",count);

Related