JavaScript Generate Your Own Random Number Without Math Random

A Custom Code for Finding Pseudo-generated Number

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 assigning this.seed a value, which is Date.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 in dateIntArr. We then add the same indexes values of this.seed & dateIntArr. If the sum is greater than 9 we consider the remainder after %10. We assign the resulting array to this.seed.
  • Lastly, shuffle this.seed array to the left by one, and return the 0 index value, which will be anything from 0 to 9. The shuffled this.seed is now the new seed value to be used in the next generate call.

Reasoning

  • Reversing the arrays this.seed & dateIntArr is not necessary, but then we’ll need to return this.seed[this.seed.length] instead of this.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