rwaldron / proposal-math-extensions

ES Math Extensions
61 stars 7 forks source link

Add Math.randomInteger #8

Open Jamesernator opened 8 years ago

Jamesernator commented 8 years ago

Generating random integers is a rather common procedure so it would be nice to have given that most people have probably used the suggested function from MDN.

Obviously this wouldn't be suitable for generating cryptographic-ally secure random integers as its limited by Math.random but it would be useful for non-secure use cases.

The proposed procedure would be as follows (based of the MDN code):

Math.randomInteger(max)

  1. Return `Math.randomInteger(0, max)

Math.randomInteger(min, max)

  1. If either argument is not an integer in the range Math.MIN_SAFE_INTEGER to Math.MAX_SAFE_INTEGER throw a RangeError
  2. If max <= min then throw a RangeError
  3. Let randomInteger be Math.floor(Math.random() * (max - min)) + min
  4. Return randomInteger

Questions:

fuchsia commented 8 years ago

Javascript needs to improve its random number support. I was going to propose Math.randomGaussian() (return a normally distributed random number with mean 0 and standard deviation of 1). But I'd held back because I was queasy about having it hanging off Math.

I'm even more queasy about adding random integer generation to Math. However your algorithm never returns max (since Math.random() always returns a number LESS than 1). That makes me reconsider. (Perhaps Math.random.uniformInteger(), Math.random.gaussian() etc...?)

I also I think you should remove the RangeError and sort the numbers. Something like this:

Math.randomInteger(n1,n2 = 0)

  1. let a = ToNumber( n1), b = ToNumber( n2 )
  2. If a or b is non-finite return NaN
  3. let min = min( a, b )
  4. let range = abs( a - b ) + 1
  5. return floor( Math.random() * range + min )

(Almost certainly I've made some silly mistake, too.)

fuchsia commented 8 years ago

I've written the above as inclusive because that's what I expect when I see min-max; anything else would be counterintuitive to me.

However, I expect the one argument form to be exclusive; I expect randomInteger(N) to return an uniform integer in the range [0,N). One of my main uses cases for random integers is picking from lists, e.g. array[ randomInteger( array.length ) ], an inclusive range from would be a handicap in that case.

So maybe the half-open range you wrote is right. Or maybe it's a foot-gun. (Where are the smart people?)

Another justification for supporting random integers is that PRNGs work in terms of integers and I presume recovering it from a float is less efficient than transforming an integer to an integer.

Jamesernator commented 8 years ago

Mixed behaviour would be the most un-intuitive, e.g.

array[Math.randomInteger(array.length)] would correctly select a random element of the array but

array[Math.randomInteger(array.length/2, array.length)] could give array[array.length] which is bad

Also I was thinking about the probabilities (I know Javascript won't have any guarantees but its roughly true) for choosing a random number from a range e.g. if I use Math.randomInteger(N) with exclusive then each number has a 1/N chance of being selected, similarly Math.randomInteger(A, B) will give each number a 1/(B-A) chance of being selected which seems more intuitive e.g. take for example a coin flip

if (Math.randomInteger(2)) {
    ~50% chance
} ...

That could be solved by making the default value one instead of zero but then you'd need to do

if (Math.randomInteger(2) === 1) {
    ~50% chance
} ...

Python has both randint and randrange where randint(a, b) is just randrange(a, b+1) so adding two functions is another possible solution.

I do like the idea of having the functions hang off Math.random so we could have:

Math.random.range
Math.random.integer
// ...etc if other's are deemed useful enough
micnic commented 7 years ago

If this gets accepted please use the name randomInt to be consistent with parseInt, Int8Array and other similar names.

ghost commented 7 years ago

@fuchsia I have asked es-discuss if they think Array.prototype.random would be a useful addition to the language as an easy way to pick from a list. So far the responses have been:

I believe it's too specialized to be a part of Array interface.

I feel in this particular case, a more general solution is much more useful than just a "pick some random item in an array".

I agree that having a random integer function would be more suitable. Picking a random element from an array is a fairly uncommon thing...

What are some other uses for a random integer?

Berkmann18 commented 5 years ago

What about something like:

Math.randomInt = (min, max) => Math.floor(Math.random() * (max - min + 1) + min)
//Or
Math.randomInt = (min, max) => (Math.random() * (max - min + 1) + min) | 0;

Math.randomFloat = (min, max) => Math.random() * (max - min + 1) + min;

That way, it allows coders to easily generate random floats or integers while being able to specify the minimum and maximum number it can output.

Or maybe something closer to how Python, Matlab and Processing (Java) did with a function definition as such:

Math.randomInt = (max, min = 0) => ...
Andrew-Cottrell commented 4 years ago

For functions that take two arguments, which together specify a half-open interval, I have found it useful to name the parameters inclusive and exclusive without implication that one is less than or greater than the other. This makes these functions easier to predict and makes it easier for calling code to avoid off-by-one errors. For example

Math.randomInt = function (inclusive, exclusive) {
    // This implementation is only an example and not a suggested specification
    var delta = exclusive - inclusive;
    if (delta && Number.isInteger(inclusive) && Number.isInteger(exclusive)) {
        return inclusive + Math.trunc(delta * Math.random());
    }
    throw new RangeError("Invalid arguments (" + inclusive + ", " + exclusive + ")");
};

Which enables (in pseudo-code)

   0 <= Math.randomInt(   0,  length ) < length; // the primary use case
-128 <= Math.randomInt( -128,   +128 ) <   +128; // a signed octet
+180 >= Math.randomInt( +180,   -180 ) >   -180; // a longitude in integer degrees
 100 >= Math.randomInt(  100,      0 ) >      0; // a non-zero integer percent
  13 >= Math.randomInt(   13,      0 ) >      0; // "pick a card, any card..."
  10 >= Math.randomInt(   10,      0 ) >      0; // "think of a number between 1 and 10"
   6 >= Math.randomInt(    6,      0 ) >      0; // "roll one 6-sided die"

This approach complements

Math.inInterval = function (number, inclusive, exclusive) {
    if (inclusive < exclusive) {
        return inclusive <= number && number < exclusive;
    }
    return exclusive < number && number <= inclusive;
};

Which ensures the aesthetically pleasing identity

Math.inInterval( Math.randomInt( inclusive, exclusive ), inclusive, exclusive ) === true