How to generate a random double number in the inclusive [0,1] range?

Viewed 949

The following code generates a double number in the range [0,1) which means that 1 is exclusive.

var random = new Random();
random.NextDouble();

I am looking for some smart way to generate a random double number in the range [0,1]. It means that 1 is inclusive. I know that the probability of generating 0 or 1 is really low, but imagine that I want to implement a correct mathematical function that requires from me the inclusive limits. How can I do it?

The question is: What is the correct way of generating random in the range [0,1]. If there is no such way, I would love to learn it also.

6 Answers

If you want uniform distribution, it‘s harder than it seams. Look how NextDouble is implemented.

There’re ways to produce uniformly distributed numbers in arbitrary intervals, an easy one is selectively discarding some of the generated values. Here’s how I would do that for your problem.

/// <summary>Utility function to generate random 64-bit numbers</summary>
static ulong nextUlong( Random rand )
{
    Span<byte> buffer = stackalloc byte[ 8 ];
    rand.NextBytes( buffer );
    return BitConverter.ToUInt64( buffer );
}

/// <summary>Generate a random number in [ 0 .. +1 ] interval, inclusive.</summary>
public static double nextDoubleInclusive( Random rand )
{
    // We need uniformly distributed integer in [ 0 .. 2^53 ]
    // The interval contains ( 2^53 + 1 ) distinct values.

    // The complete range of ulong is [ 0 .. 2^64 - 1 ], 2^64 distinct values.
    // 2^64 / ( 2^53 + 1 ) is about 2047.99, here's why
    // https://www.wolframalpha.com/input/?i=2%5E64+%2F+%28+2%5E53+%2B+1+%29

    const ulong discardThreshold = 2047ul * ( ( 1ul << 53 ) + 1 );

    ulong src;
    do
    {
        src = nextUlong( rand );
    }
    while( src >= discardThreshold );
    // Got uniformly distributed value in [ 0 .. discardThreshold ) interval
    // Dividing by 2047 gets us a uniformly distributed value in [ 0 ..  2^53 ]
    src /= 2047;

    // Produce the result
    return src * ( 1.0 / ( 1ul << 53 ) );
}

After taking a shower, I have conceived of a potential solution based on my understanding of how a random floating point generator works. My solution makes three assumptions, which I believe to be reasonable, however I can not verify if these assumptions are correct or not. Because of this, the following code is purely academic in nature, and I would not recommend its use in practice. The assumptions are as follows:

  1. The distribution of random.NextDouble() is uniform
  2. The difference between any two adjacent numbers in the range produced by random.NextDouble() is a constant epsilon e
  3. The maximum value generated by random.NextDouble() is equal to 1 - e

Provided that those three assumptions are correct, the following code generates random doubles in the range [0, 1].

// For the sake of brevity, we'll omit the finer details of reusing a single instance of Random
var random = new Random();

double RandomDoubleInclusive() {
    double d = 0.0;
    int i = 0;

    do {
        d = random.NextDouble();
        i = random.Next(2);
    } while (i == 1 && d > 0)
    
    return d + i;
}

This is somewhat difficult to conceptualize, but the essence is somewhat like the below coin-flipping explanation, except instead of a starting value of 0.5, you start at 1, and if at any point the sum exceeds 1, you restart the entire process.

From an engineering standpoint, this code is a blatant pessimization with little practical advantage. However, mathematically, provided that the original assumptions are correct, the result will be as mathematically sound as the original implementation.

Below is the original commentary on the nature of random floating point values and how they're generated.

Original Reply:

Your question carries with it a single critical erroneous assumption: Your use of the word "Correct". We are working with floating point numbers. We abandoned correctness long ago.

What follows is my crude understanding of how a random number generator produces a random floating point value.

You have a coin, a sum starting at zero, and a value starting at one half (0.5).

  1. Flip the coin.
  2. If heads, add the value to the sum.
  3. Half the value.
  4. Repeat 23 times.

You have just generated a random number. Here are some properties of the number (for reference, 2^23 is 8,388,608, and 2^(-23) is the inverse of that, or approximately 0.0000001192):

  • The number is one of 2^23 possible values
  • The lowest value is 0
  • The highest value is 1 - 2^(-23);
  • The smallest difference between any two potential values is 2^(-23)
  • The values are evenly distributed across the range of potential values
  • The odds of getting any one value are completely uniform across the range
  • Those last two points are true regardless of how many times you flip the coin
  • The process for generating the number was really really easy

That last point is the kicker. It means if you can generate raw entropy (i.e. perfectly uniform random bits), you can generate an arbitrarily precise number in a very useful range with complete uniformity. Those are fantastic properties to have. The only caveat is that it doesn't generate the number 1.

The reason that caveat is seen as acceptable is because every other aspect of the generation is so damned good. If you're trying to get a high precision random value between 0 and 1, chances are you don't actually care about landing on 1 any more than you care about landing on 0.38719, or any other random number in that range.

While there are methods for getting 1 included in your range (which others have stated already), they're all going to cost you in either speed or uniformity. I'm just here to tell you that it might not actually be worth the tradeoff.

Usually, knowing that NextDouble() has a finite range, we multiply the value to suit the range we need.

For this reason it is common to create your own wrapper to produce the next business value when the built in logic does not meet your requirements.

For this particular example, why not just post process the result, when zero get the value from Next(0,2)

public static double NextInclude1(this Random rand = null)
{
    rand = rand ?? new Random();
    var result = rand.NextDouble();
    if (result == 0) result = rand.Next(0,2);
    return result;
}

You can implement your own bias for 0 or 1 as a result by varying the comparison to zero, if you do that though you are likely to create an exclusion range, so after the comparison you may need to return the next NextDouble()

public static double NextInclude1(this Random rand = null)
{
    rand = rand ?? new Random();
    var result = rand.NextDouble();
    if (result < 0.2) 
        result = rand.Next(0,2);
    else
        result = rand.NextDouble();
    return result;
}

This particular example results in an overall bias for 0, it's up to you to determine the specific parameters that you would accept, overall NextDouble() is your base level tool for most of your custom Random needs.

The Random.Next method returns an integer value in the range [0..Int32.MaxValue) (the exclusive range-end is denoted by the right parenthesis). So if you want to make the value 1.0 a possible result of the NextDouble method (source code), you could do this:

/// <summary>Returns a random floating-point number that is greater than or equal to 0.0,
/// and less than or equal to 1.0.</summary>
public static double NextDoubleInclusive(this Random random)
{
    return (random.Next() * (1.0 / (Int32.MaxValue - 1)));
}

This fiddle verifies that the expression (Int32.MaxValue - 1) * (1.0 / (Int32.MaxValue - 1)) evaluates to 1.0.

This definitely works, You can check the distribution here https://dotnetfiddle.net/SMMOrM

Random random = new Random();
double result = (int)(random.NextDouble() * 10) > 4
    ? random.NextDouble()
    : 1 - random.NextDouble();

Update

Agree with Snoot, this version would return 0 and 1 twice less often as other values

Easy, you can do this

var random = new Random();
var myRandom = 1 - Math.Abs(random.NextDouble() - random.NextDouble());

update

Sorry, this won't generate normal distribution of results, where they will tend to be higher ones, close to 1.

Related