We know that there is no true random number. The best we get is pseudo-random number, which comes from a seed value. I was wondering if I could get a random number without using Math.random() function of JavaScript. So I developed an algorithm of my own, which is a few lines of code that works by keeping, updating, and shifting a seed array in the state. This ensures nearly equal distribution of the generated values for larger and larger test set.
Below is the code with explanation, followed by its performance test and comparison with Math.random().
The Code
const getDateIntArr = () => Date.now().toString().split("").reverse().map(d => parseInt(d))
class MyRandom {
constructor() {
this.seed = getDateIntArr();
}
generate() {
const dateIntArr = getDateIntArr();
this.seed = dateIntArr.map((d, index) => {
let newVal = d + this.seed[index];
if (newVal > 9) {
newVal = newVal % 10;
}
return newVal;
});
this.seed.push(this.seed.shift());
return this.seed[0];
}
};
The Code Explained
- We start with creating a class
MyRandom
and assigningthis.seed
a value, which isDate.now()
in reversed array form and converted to integer. (Example:Date.now()
=1654202603378
,this.seed
=[ 8, 7, 3, 3, 0, 6, 2, 0, 2, 4, 5, 6, 1 ]
.) - The
generate
method creates a similar reversed array indateIntArr
. We then add the same indexes values ofthis.seed
&dateIntArr
. If the sum is greater than 9 we consider the remainder after%10
. We assign the resulting array tothis.seed
. - Lastly, shuffle
this.seed
array to the left by one, and return the0
index value, which will be anything from 0 to 9. The shuffledthis.seed
is now the new seed value to be used in the nextgenerate
call.
Reasoning
- Reversing the arrays
this.seed
&dateIntArr
is not necessary, but then we’ll need to returnthis.seed[this.seed.length]
instead ofthis.seed[0]
. (also, we may want to change the array shift to the opposite direction.) - We return the first element because that’s the one most frequently changed in Date.now() after reversing, followed by second, third and so on. After addition to the previous seed and shifting, this should be the most unpredictable number out of the lot.
Time Complexity
The time complexity will be O(1) i.e. constant, because though there are loops used but they won’t be dynamic and only go so far as 13, which is the length of Date.now()
.
Performance Test and Comparison with Math.Random()
A reasonably good (pseudo)random generator needs to generate the numbers within a given range almost equally. If we generate a random number from 1 to 10 (like we did above), the percentage of each value from 1 to 10 should be roughly 10%. That is, on generating a random number, say, a 1000 times, every number should ideally appear a 100 times.
To compare the performance of both, let’s run our own custom generator and Math.random()
for 10, 100, 1000, 10000, and 100000 times, and get the percentage of the occurrences of 0 to 9. The script below gets us the percentage of each occurrence, given the number of times a random number is generated. (Change the value of TOTAL for the number of times you want to run the generators):
const sortObject = o => Object.keys(o).sort().reduce((r, k) => (r[k] = o[k], r), {});
const mathRandomHash = {};
const customRandomHash = {}
const TOTAL = 100;
const rand = new MyRandom();
for (let i = 0; i < TOTAL; i++) {
const customRand = rand.generate();
const mathRand = (Math.floor(Math.random() * 10));
if (!customRandomHash[customRand]) {
customRandomHash[customRand] = 1
}
else {
customRandomHash[customRand] += 1;
}
if (!mathRandomHash[mathRand]) {
mathRandomHash[mathRand] = 1
}
else {
mathRandomHash[mathRand] += 1;
}
}
const convertToPercentage = (hash, total) => {
return Object.keys(hash).forEach(e => {
hash[e] = Math.floor((hash[e] / total) * 100);
})
}
convertToPercentage(mathRandomHash, TOTAL);
convertToPercentage(customRandomHash, TOTAL);
console.log("Math Rand");
console.log(sortObject(mathRandomHash));
console.log("My Rand");
console.log(sortObject(customRandomHash));
10 Times
Math Rand
{ '1': 30, '2': 20, '3': 20, '4': 20, '5': 10 }
My Rand
{ '0': 20, '1': 10, '4': 10, '5': 10, '6': 30, '8': 10, '9': 10 }
100 Times
Math Rand
{
'0': 7,
'1': 10,
'2': 10,
'3': 10,
'4': 9,
'5': 13,
'6': 12,
'7': 10,
'8': 10,
'9': 9
}
My Rand
{
'0': 11,
'1': 7,
'2': 7,
'3': 9,
'4': 10,
'5': 11,
'6': 10,
'7': 11,
'8': 12,
'9': 12
}
1000 Times
Math Rand
{
'0': 9,
'1': 8,
'2': 11,
'3': 11,
'4': 9,
'5': 10,
'6': 10,
'7': 10,
'8': 9,
'9': 10
}
My Rand
{
'0': 9,
'1': 11,
'2': 9,
'3': 11,
'4': 8,
'5': 10,
'6': 9,
'7': 10,
'8': 9,
'9': 10
}
10000 Times
Math Rand
{
'0': 10,
'1': 9,
'2': 9,
'3': 9,
'4': 9,
'5': 9,
'6': 10,
'7': 10,
'8': 10,
'9': 10
}
My Rand
{
'0': 10,
'1': 11,
'2': 10,
'3': 9,
'4': 9,
'5': 9,
'6': 11,
'7': 9,
'8': 9,
'9': 9
}
100000 Times
Math Rand
{
'0': 9,
'1': 9,
'2': 10,
'3': 10,
'4': 9,
'5': 9,
'6': 10,
'7': 10,
'8': 10,
'9': 9
}
My Rand
{
'0': 9,
'1': 10,
'2': 10,
'3': 9,
'4': 9,
'5': 10,
'6': 9,
'7': 9,
'8': 10,
'9': 9
}
From the above data, we can observe that both Math.random()
and our custom random number generator start off with pretty uneven distribution of generated numbers from 1 to 10, when run for merely 10 or 100 times. But as we keep on increasing the number, the distribution gets even. For 100,000 runs, each number from 1 to 10 appears almost 10% of the time.
See also
- SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
- Yup Date Format Validation With Moment JS
- Yup Number Validation: Allow Empty String
- Exactly Same Query Behaving Differently in Mongo Client and Mongoose
- JavaScript Unit Testing JSON Schema Validation
- Reduce JS Size With Constant Strings
- JavaScript SDK