Numbers in Javascript are just 64bit floats and hence are only good for about 15 decimal digits. Taking the modulus of values with ~37 decimal digits will mean the low order bits are all effectively zero and you'll get relatively sparse output. e.g.:
a = Array(100).fill(0)
for (i = 0; i < 10000; i++) {
d = Math.random() * 2**128
a[d % 100] += 1
}
note that the Math.random() * 2**128 is roughly equivalent to generating the hash of a random email. this gives me an a like:
[
409, 0, 0, 0, 408, 0, 0, 0, 408, 0, 0, 0,
398, 0, 0, 0, 420, 0, 0, 0, 434, 0, 0, 0,
356, 0, 0, 0, 401, 0, 0, 0, 398, 0, 0, 0,
423, 0, 0, 0, 346, 0, 0, 0, 397, 0, 0, 0,
406, 0, 0, 0, 378, 0, 0, 0, 429, 0, 0, 0,
410, 0, 0, 0, 421, 0, 0, 0, 358, 0, 0, 0,
389, 0, 0, 0, 363, 0, 0, 0, 398, 0, 0, 0,
398, 0, 0, 0, 426, 0, 0, 0, 396, 0, 0, 0,
430, 0, 0, 0
]
indicating that only values divisible by 4 are possible, and hence 75 of your 100 values will never be used.
As James K. Polk commented, taking the modulus is also slightly biased, but the above is a much bigger issue. I'd also second the suggestion using division as this keeps the high order bits and maintains the entropy, something like:
digest = crypto.createHash('md5').update('example@gmail.com').digest("hex")
Math.floor(parseInt(digest, 16) / 2**128 * 100)
you can use something similar to above loop to see that this gives a uniform distribution of outputs
note that both of the above generate values from 0 to 99, so you probably want to add 1 to the result
another way is to go with Node's BigInt type, something like:
digest = crypto.createHash('md5').update('example@gmail.com').digest()
Number(digest.readBigUInt64BE() / (2n**64n / 100n + 1n))
which avoids converting to strings and back again, but gets to basically the same answer.