Table of Contents
Learn how to generate a range of random numbers that fall not only within an upper and lower range you specify, but the frequency that each number appears is fair and balanced! ⚖️
For many situations ranging from coin toss operations to procedural animations, you will want to work with numbers whose values are a bit unpredictable. You will want to work with numbers that are random. Generating a random number is easy. It is built-in to JavaScript and exposed via the infamous Math.random() function. Now, if this function already exists, what are we still doing here? Well, using the function is only part of being able to generate a random number. For most real-life situations, we don't want just any random number. We will want a specific type of random number - one that falls within a range that we define. There are some subtle gotchas with that, and that's what the rest of these sections will explore.
Onwards!
Before I dive into the details, if all you want is the code for generating a random whole number within a set range, use Math.random() with the following formula:
Math.floor(Math.random() * (1 + High - Low)) + Low
The value for High is the largest random number you would like to generate. The value for low is the smallest random number you would like to generate instead. When you run this code, what you will get is a number that randomly falls somewhere between the bounds specified by High and Low.
Here are some examples:
// Random number between 0 and 10 (inclusive)
let foo = Math.floor(Math.random() * 11);
console.log(foo);
// Random number between 0 and 100 (inclusive)
let bar = Math.floor(Math.random() * 101);
console.log(bar);
// Random number between 5 and 25 (inclusive)
let zorb = Math.floor(Math.random() * 21) + 5;
console.log(zorb);
To make things simple, here is a function you can use instead:
function getRandomNumber(low, high) {
let r = Math.floor(Math.random() * (high - low + 1)) + low;
return r;
}
Just call getRandomNumber and pass in the lower and upper bound as arguments:
// Random number between 0 and 10 (inclusive)
let foo = getRandomNumber(0, 10);
console.log(foo);
// Random number between 0 and 100 (inclusive)
let bar = getRandomNumber(0, 100);
console.log(bar);
// Random number between 5 and 25 (inclusive)
let zorb = getRandomNumber(5, 25);
console.log(zorb);
That's all there is to generating a random number that falls within a range that you specify.
In JavaScript, Math.random() returns a number greater than or equal to 0 but less than 1:
Another way of stating that is (0 <= n < 1) where n is the number you are looking for. This inability for Math.random to get really REALLY close to 1 but never quite getting there is largely why getting a random number between a range of numbers is so...inelegant:
Math.floor(Math.random() * (1 + High - Low)) + Low
Looking at our approach a bit further, let's start with the biggest oddity, the addition of the 1. Here is why we include it. We have already stated many times that the Math.random() function will never return a 1 as its value. It will return something close like .9999999, but it won't ever be a 1. We also want a round number that doesn't include decimals. There are several approaches for rounding a number, but we are rounding the output by Math.floor where we round down to the nearest integer. This rounding down is the problem.
If we did not add the 1, because of how Math.floor works, it will never get the maximum possible answer when we multiply the result of High - Low by Math.random(). This probably doesn't make a lot of sense, so let's look at an example where attempt to get a random number without adding the 1.
What we want to do is generate a random number between 10 and 50. Ignoring the 1, this is what the expression will look like:
Math.floor(Math.random() * 40) + 10
The Math.random() * 40 will never return a 40 because that would require Math.random() to return a 1...which it can't. Let's be optimistic and say that Math.random() returns a .9999, and we multiply that value by 40. The value that gets returned will be 39.996. Guess what Math.floor of 39.996 is going to be? It is going to be 39! When this 39 gets added to 10, you get a value of 49. You have no conceivable way of getting an answer of 50 which is the high number in your range of values. The only solution is to add a 1 to the operation:
Math.floor(Math.random() * 41) + 10
Once you do that, you will be able to generate all numbers within the range of your high and low numbers, 10 and 50 respectively, with equal frequency.
Using Math.random() works as advertised. It gets us a random number between 0 and almost 1. What it doesn't give us is a random number that is cryptographically secure. If you are planning on using a random number generator for some security intensive activities like generating a certificate or authenticating someone, Math.random() isn't a secure option. The reason is that a clever attacker can figure out the pattern Math.random() uses and bypass any security you may have that is based on the randomness being truly random.
For those moments when you need random numbers that are cryptographically secure, we have the Crypto.getRandomValues() function. Not to get too detailed here, but this method's internals use some specialized logic to help generate a really random number that an attacker can't easily guess. To use Crypto.getRandomValues() in our apps, take a look at the following cryptoRandom() function that uses it to return a random number between 0 and almost 1:
function cryptoRandom() {
let initial = new Uint32Array(1);
window.crypto.getRandomValues(initial);
return initial[0] / (0xFFFFFFFF + 1);
}
We can use this function in the same situations we would normally have used Math.random() in. To see this for ourselves, if we take our examples from earlier and adapt them to use our cryptoRandom function, here is what this will all look like:
function cryptoRandom() {
let initial = new Uint32Array(1);
window.crypto.getRandomValues(initial);
return initial[0] / (0xFFFFFFFF + 1);
}
function getSecureRandomNumber(low, high) {
let r = Math.floor(cryptoRandom() * (high - low + 1)) + low;
return r;
}
// Random number between 0 and 10 (inclusive)
let foo = getSecureRandomNumber(0, 10);
console.log(foo);
// Random number between 0 and 100 (inclusive)
let bar = getSecureRandomNumber(0, 100);
console.log(bar);
// Random number between 5 and 25 (inclusive)
let zorb = getSecureRandomNumber(5, 25);
console.log(zorb);
Notice that we have a getSecureRandomNumber function that takes a low and high value as its arguments and returns a random number within that range, inclusively.
Now, you may be asking yourself why we don't just generate cryptographically strong/secure random numbers all the time? The reason has to do with performance. The internal gymnastics needed to generate a really REALLY random number takes more time than the meh random numbers generated by Math.random(). Unless you are doing something that truly needs a cryptographically strong random number, Math.random() should be all you need.
One of the more difficult parts of random numbers is wrapping our heads around the idea that a range of numbers get chosen randomly AND nearly equally after enough tries. To help us visualize this, take a look at the handy dandy Random Number Frequency Visualizer:
We are picking a random number between 1 and 5 inclusively. This visualizer plots the frequency that each of these numbers get hit across 1400 runs. Unless something went really off, you will see all of the numbers being hit fairly evenly.
Generating random numbers properly is a challenging problem. It seems really simple, but getting the range of numbers not only correct but also occurring with equal frequency is where the challenges happen. As long as you are aware of the limitations of the various rounding methods, you can replace Math.floor with Math.ceil or, if you are really daring, Math.round.
Just a final word before we wrap up. If you have a question and/or want to be part of a friendly, collaborative community of over 220k other developers like yourself, post on the forums for a quick response!
:: Copyright KIRUPA 2024 //--