No, it isn't.
What you're effectively doing (via a cast of binary data to a floating point value) is calculating floor(max * rand() / (RAND_MAX + 1.0))
(with RAND_MAX
equal to 252−1). This will always result in a skewed distribution unless max
is a factor of RAND_MAX+1
, as explained here.
This is quite easy to demonstrate:
function random_number(max) {
let buffer=new ArrayBuffer(8);
let ints=new Int8Array(buffer);
window.crypto.getRandomValues(ints);
ints[7]=64-1;
ints[6]|=0xf0;
let float=new DataView(buffer).getFloat64(0,true)-1;
return Math.floor(float*Math.floor(max+1));
}
function check_skew() {
var m = Math.floor(Math.pow(2,52) * 2 / 3);
var o = [0,0];
var ns = 100000;
for (i=0; i<ns; i++) o[random_number(m)&1]++; o;
console.log("Out of "+ns+" random numbers, "+o[0]*100/ns+
"% were even and "+o[1]*100/ns+"% were odd.");
}
<button onclick="check_skew()">Click this button a few times and check the results</button>
The correct way to obtain a random integer over a specified range is to start with a uniform random number whose bit length is at least as long as that of max
. Discard the higher bits, and return the result if it is less than or equal to max
. Otherwise repeat the process.
Something like this, perhaps:
function rand_int(max) {
// Returns a uniform random integer from 0 to max (inclusive)
var mask = 1;
var crypto = window.crypto;
max = Math.floor(max);
if (!crypto) throw "window.crypto undefined";
if (max < 1) throw "max value too small";
if (max > 0xffffffff) throw "max value too large";
// Generate binary mask (all 1)
while (mask < max) mask = (mask << 1) | 1;
// Now generate random values until one is within range
var r = new Int32Array(1);
do {
crypto.getRandomValues(r);
r[0] &= mask;
} while (r[0] > max);
return r[0];
}