Generating a Random Decimal in C#

2020-01-26 07:05发布

How can I get a random System.Decimal? System.Random doesn't support it directly.

12条回答
唯我独甜
2楼-- · 2020-01-26 07:48

To be honest I don't believe the internal format of the C# decimal works the way many people think. For this reason at least some of the solutions presented here are possibly invalid or may not work consistently. Consider the following 2 numbers and how they are stored in the decimal format:

0.999999999999999m
Sign: 00
96-bit integer: 00 00 00 00 FF 7F C6 A4 7E 8D 03 00
Scale: 0F

and

0.9999999999999999999999999999m
Sign: 00
96-bit integer: 5E CE 4F 20 FF FF FF 0F 61 02 25 3E
Scale: 1C

Take special note of how the scale is different but both values are nearly the same, that is, they are both less than 1 by only a tiny fraction. It appears that it is the scale and the number of digits that have a direct relationship. Unless I'm missing something, this should throw a monkey wrench into most any code that tampers with the 96-bit integer part of a decimal but leaves the scale unchanged.

In experimenting I found that the number 0.9999999999999999999999999999m, which has 28 nines, has the maximum number of nines possible before the decimal will round up to 1.0m.

Further experimenting proved the following code sets the variable "Dec" to the value 0.9999999999999999999999999999m:

double DblH = 0.99999999999999d;
double DblL = 0.99999999999999d;
decimal Dec = (decimal)DblH + (decimal)DblL / 1E14m;

It is from this discovery that I came up with the extensions to the Random class that can be seen in the code below. I believe this code is fully functional and in good working order, but would be glad for other eyes to be checking it for mistakes. I'm not a statistician so I can't say if this code produces a truly uniform distribution of decimals, but if I had to guess I would say it fails perfection but comes extremely close (as in 1 call out of 51 trillion favoring a certain range of numbers).

The first NextDecimal() function should produce values equal to or greater than 0.0m and less than 1.0m. The do/while statement prevents RandH and RandL from exceeding the value 0.99999999999999d by looping until they are below that value. I believe the odds of this loop ever repeating are 1 in 51 trillion (emphasis on the word believe, I don't trust my math). This in turn should prevent the functions from ever rounding the return value up to 1.0m.

The second NextDecimal() function should work the same as the Random.Next() function, only with Decimal values instead of integers. I actually haven't been using this second NextDecimal() function and haven't tested it. Its fairly simple so I think I have it right, but again, I haven't tested it - so you will want to make sure it is working correctly before relying on it.

public static class ExtensionMethods {
    public static decimal NextDecimal(this Random rng) {
        double RandH, RandL;
        do {
            RandH = rng.NextDouble();
            RandL = rng.NextDouble();
        } while((RandH > 0.99999999999999d) || (RandL > 0.99999999999999d));
        return (decimal)RandH + (decimal)RandL / 1E14m;
    }
    public static decimal NextDecimal(this Random rng, decimal minValue, decimal maxValue) {
        return rng.NextDecimal() * (maxValue - minValue) + minValue;
    }
}
查看更多
Bombasti
3楼-- · 2020-01-26 07:49
static decimal GetRandomDecimal()
    {

        int[] DataInts = new int[4];
        byte[] DataBytes = new byte[DataInts.Length * 4];

        // Use cryptographic random number generator to get 16 bytes random data
        RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();

        do
        {
            rng.GetBytes(DataBytes);

            // Convert 16 bytes into 4 ints
            for (int index = 0; index < DataInts.Length; index++)
            {
                DataInts[index] = BitConverter.ToInt32(DataBytes, index * 4);
            }

            // Mask out all bits except sign bit 31 and scale bits 16 to 20 (value 0-31)
            DataInts[3] = DataInts[3] & (unchecked((int)2147483648u | 2031616));

          // Start over if scale > 28 to avoid bias 
        } while (((DataInts[3] & 1835008) == 1835008) && ((DataInts[3] & 196608) != 0));

        return new decimal(DataInts);
    }
    //end
查看更多
贪生不怕死
4楼-- · 2020-01-26 07:51

It is also, through the power of easy stuff, to do:

var rand = new Random();
var item = new decimal(rand.NextDouble());
查看更多
干净又极端
5楼-- · 2020-01-26 07:55

You would normally expect from a random-number-generator that it not only generated random numbers, but that the numbers were uniformly randomly generated.

There are two definitions of uniformly random: discrete uniformly random and continuous uniformly random.

Discretely uniformly random makes sense for a random number generator that has a finite number of different possible outcomes. For example generating an integer between 1 and 10. You would then expect that the probability of getting 4 is the same as getting 7.

Continuously uniformly random makes sense when the random number generator generates numbers in a range. For example a generator that generates a real number between 0 and 1. You would then expect that the probability of getting a number between 0 and 0.5 is the same as getting a number between 0.5 and 1.

When a random number generator generates floating-point numbers (which is basically what a System.Decimal is - it is just floating-point which base 10), it is arguable what the proper definition of uniformly random is:

On one hand, since the floating-point number is being represented by a fixed number of bits in a computer, it is obvious that there are a finite number of possible outcomes. So one could argue that the proper distribution is a discrete continuous distribution with each representable number having the same probability. That is basically what Jon Skeet's and John Leidegren's implementation does.

On the other hand, one might argue that since a floating-point number is supposed to be an approximation to a real number, we would be better off by trying to approximate the behavior of a continuous random number generator - even though are actual RNG is actually discrete. This is the behavior you get from Random.NextDouble(), where - even though there are approximately as many representable numbers in the range 0.00001-0.00002 as there are in the range 0.8-0.9, you are a thousand times more likely to get a number in the second range - as you would expect.

So a proper implementation of a Random.NextDecimal() should probably be continuously uniformly distributed.

Here is a simple variation of Jon Skeet's answer that is uniformly distributed between 0 and 1 (I reuse his NextInt32() extension method):

public static decimal NextDecimal(this Random rng)
{
     return new decimal(rng.NextInt32(), 
                        rng.NextInt32(),
                        rng.Next(0x204FCE5E),
                        false,
                        0);
}

You could also discuss how to get an uniform distribution over the entire range of decimals. There is probably an easier way to do this, but this slight modification of John Leidegren's answer should produce a relatively uniform distribution:

private static int GetDecimalScale(Random r)
{
  for(int i=0;i<=28;i++){
    if(r.NextDouble() >= 0.1)
      return i;
  }
  return 0;
}

public static decimal NextDecimal(this Random r)
{
    var s = GetDecimalScale(r);
    var a = (int)(uint.MaxValue * r.NextDouble());
    var b = (int)(uint.MaxValue * r.NextDouble());
    var c = (int)(uint.MaxValue * r.NextDouble());
    var n = r.NextDouble() >= 0.5;
    return new Decimal(a, b, c, n, s);
}

Basically, we make sure that values of scale are chosen proportionally to the size of the corresponding range.

That means that we should get a scale of 0 90% of the time - since that range contains 90% of the possible range - a scale of 1 9% of the time, etc.

There are still some problems with the implementation, since it does take into account that some numbers have multiple representations - but it should be much closer to a uniform distribution than the other implementations.

查看更多
仙女界的扛把子
6楼-- · 2020-01-26 07:57

here you go... uses the crypt library to generate a couple of random bytes, then convertes them to a decimal value... see MSDN for the decimal constructor

using System.Security.Cryptography;

public static decimal Next(decimal max)
{
    // Create a int array to hold the random values.
    Byte[] randomNumber = new Byte[] { 0,0 };

    RNGCryptoServiceProvider Gen = new RNGCryptoServiceProvider();

    // Fill the array with a random value.
    Gen.GetBytes(randomNumber);

    // convert the bytes to a decimal
    return new decimal(new int[] 
    { 
               0,                   // not used, must be 0
               randomNumber[0] % 29,// must be between 0 and 28
               0,                   // not used, must be 0
               randomNumber[1] % 2  // sign --> 0 == positive, 1 == negative
    } ) % (max+1);
}

revised to use a different decimal constructor to give a better range of numbers

public static decimal Next(decimal max)
{
    // Create a int array to hold the random values.
    Byte[] bytes= new Byte[] { 0,0,0,0 };

    RNGCryptoServiceProvider Gen = new RNGCryptoServiceProvider();

    // Fill the array with a random value.
    Gen.GetBytes(bytes);
    bytes[3] %= 29; // this must be between 0 and 28 (inclusive)
    decimal d = new decimal( (int)bytes[0], (int)bytes[1], (int)bytes[2], false, bytes[3]);

        return d % (max+1);
    }
查看更多
Summer. ? 凉城
7楼-- · 2020-01-26 07:57

Since the OP question is very embracing and just want a random System.Decimal without any restriction, below is a very simple solution that worked for me.

I was not concerned with any type of uniformity or precision of the generated numbers, so others answers here are probably better if you have some restrictions, but this one works fine in simple cases.

Random rnd = new Random();
decimal val;
int decimal_places = 2;
val = Math.Round(new decimal(rnd.NextDouble()), decimal_places);

In my specific case, I was looking for a random decimal to use as a money string, so my complete solution was:

string value;
value = val = Math.Round(new decimal(rnd.NextDouble()) * 1000,2).ToString("0.00", System.Globalization.CultureInfo.InvariantCulture);
查看更多
登录 后发表回答