Util functions to generate random numbers with upper bound or with both upper and lower bounds:
var waver = (function() {
return { between: function(min, max) {
return Math.floor( Math.random() * ( max - min + 1 ) + min );
}
, until: function(max) {
return Math.floor( Math.random() * ( max + 1 ) );
}
};})();
1 Answer 1
As already indicated in the comments, the naming of the functions is weird. Both waver
and until
are not well chosen.
The functions you've provided look very much as the example in the JavaScript documentation for the function Math.random():
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min) + min); // The maximum is exclusive and the minimum is inclusive
}
However, you can see that the + 1
you did isn't there, that's if the max
value is inclusive. However, you've specified "until" as indicator, which would mean that the intent is to be exclusive.
The problem with using floating point values and operations is that they are always limited by a specific amount of precision. That in turn will mean that the function is ever so slightly biased. Usually this amount of bias can be safely ignored, but now always.
Instead it may be a better idea to use and rejection sampling using integers. Additionally I'd rather use a cryptographically secure random number generator that provides many security properties over Random
.
function getRandomInt(max) {
if (max <= 0 || !Number.isInteger(max)) {
throw new Error("The max value should be a positive integer.");
}
const bitLength = Math.ceil(Math.log2(max));
const byteLength = Math.ceil(bitLength / 8);
const rejectionLimit = Math.pow(2, bitLength) - max;
const buffer = new Uint8Array(byteLength);
while (true) {
crypto.getRandomValues(buffer);
let randomValue = 0;
for (let i = 0; i < byteLength; ++i) {
randomValue |= (buffer[i] << (8 * i));
}
// Zero out the bits that are out of range, based on bitLength
randomValue &= (Math.pow(2, bitLength) - 1);
// Reject the sample if it's larger than the max value.
if (randomValue >= rejectionLimit) {
continue;
}
return randomValue % max;
}
}
To use this with a minimum value as well:
function getRandomIntInRange(min, max) {
if (!Number.isInteger(min) || !Number.isInteger(max) || max <= min) {
throw new Error("Both min and max should be integers with max > min.");
}
const range = max - min;
const randomInt = getRandomInt(range);
return randomInt + min;
}
Beware that this is untested code. I'm pretty sure it works, but please test it before actual deployment.
-
\$\begingroup\$ Warning
crypto
is only available when running a secure context. Also one would usecrypto
not because of any bias inMath.random
(it is a rather good PRNG on the major browsers), but because random can be determined if one has knowledge of a set/sequence of results, which makes it insecure (not bias). \$\endgroup\$Blindman67– Blindman672023年08月20日 22:35:56 +00:00Commented Aug 20, 2023 at 22:35 -
\$\begingroup\$ @Blindman67 Any secure code should be run in a secure context, i.e. over TLS :) I've changed the wording w.r.t. the CSPRNG, I didn't want to indicate that this has anything to do with using floating point, just that it is something that I would change as well. \$\endgroup\$Maarten Bodewes– Maarten Bodewes2023年08月20日 23:19:18 +00:00Commented Aug 20, 2023 at 23:19
-
2\$\begingroup\$ Javascript uses double-precision floating-point values, with a precision of roughly 15 to 17 digits. That's a lot. I don't think this "bias" will be a problem in normal applications, but it is good to be aware of it. \$\endgroup\$KIKO Software– KIKO Software2023年08月21日 07:17:34 +00:00Commented Aug 21, 2023 at 7:17
-
\$\begingroup\$ @KIKOSoftware Yeah, you're right. Of course there are other reasons to use a cryptographic random instead of a normal random, e.g. you cannot calculate the state and find other random values before or after the random produced. You'll quickly find that
getRandomValues
will only return integer based arrays, so then you'd have to produce a double precision float from that , and in that case you might as well use rejection sampling. \$\endgroup\$Maarten Bodewes– Maarten Bodewes2023年08月21日 22:05:27 +00:00Commented Aug 21, 2023 at 22:05
waver.until(10)
would then return 10 possible values, instead of 11, but as I said, user275698 might have intended to return 11 possible values. \$\endgroup\$