77 votes

Générer un décimal aléatoire en C#

Comment puis-je obtenir un System.Decimal aléatoire ? System.Random ne le supporte pas directement.

59voto

Jon Skeet Points 692016

EDIT : Suppression de l'ancienne version

Cette version est similaire à celle de Daniel, mais donne la gamme complète. Elle introduit également une nouvelle méthode d'extension pour obtenir une valeur aléatoire "n'importe quel nombre entier", ce qui me semble pratique.

Notez que la distribution des décimales ici n'est pas uniforme .

/// <summary>
/// Returns an Int32 with a random value across the entire range of
/// possible values.
/// </summary>
public static int NextInt32(this Random rng)
{
     int firstBits = rng.Next(0, 1 << 4) << 28;
     int lastBits = rng.Next(0, 1 << 28);
     return firstBits | lastBits;
}

public static decimal NextDecimal(this Random rng)
{
     byte scale = (byte) rng.Next(29);
     bool sign = rng.Next(2) == 1;
     return new decimal(rng.NextInt32(), 
                        rng.NextInt32(),
                        rng.NextInt32(),
                        sign,
                        scale);
}

12voto

Rasmus Faber Points 24195

On attendrait normalement d'un générateur de nombres aléatoires qu'il ne se contente pas de générer des nombres aléatoires, mais que ces nombres soient générés de manière uniforme et aléatoire.

Il existe deux définitions de l'expression "uniformément aléatoire" : discret uniformément aléatoire y continu uniformément aléatoire .

L'expression "aléatoire uniforme discret" s'applique à un générateur de nombres aléatoires qui a un nombre fini de résultats différents possibles. Par exemple, générer un nombre entier entre 1 et 10. On s'attendrait alors à ce que la probabilité d'obtenir 4 soit la même que celle d'obtenir 7.

Le terme "aléatoire uniforme et continu" a un sens lorsque le générateur de nombres aléatoires génère des nombres dans une plage. Par exemple, un générateur qui génère un nombre réel entre 0 et 1. On s'attendrait alors à ce que la probabilité d'obtenir un nombre entre 0 et 0,5 soit la même que celle d'obtenir un nombre entre 0,5 et 1.

Lorsqu'un générateur de nombres aléatoires génère des nombres à virgule flottante (ce qui est en fait ce qu'est un System.Decimal - il s'agit simplement d'une virgule flottante en base 10), on peut discuter de la définition correcte de l'aléatoire uniforme :

D'une part, comme le nombre à virgule flottante est représenté par un nombre fixe de bits dans un ordinateur, il est évident qu'il existe un nombre fini de résultats possibles. On pourrait donc soutenir que la distribution appropriée est une distribution continue discrète, chaque nombre représentable ayant la même probabilité. C'est en fait ce que Jon Skeet's y John Leidegren la mise en œuvre le fait.

D'un autre côté, on pourrait dire que puisqu'un nombre à virgule flottante est censé être une approximation d'un nombre réel, il serait préférable d'essayer d'approcher le comportement d'un générateur de nombres aléatoires continu - même si le RNG réel est en fait discret. C'est le comportement que l'on obtient avec Random.NextDouble(), où - même s'il y a approximativement autant de nombres représentables dans la plage 0.00001-0.00002 que dans la plage 0.8-0.9, vous avez mille fois plus de chances d'obtenir un nombre dans la seconde plage - comme on peut s'y attendre.

Ainsi, une implémentation correcte de Random.NextDecimal() devrait probablement être uniformément distribuée de manière continue.

Voici une variation simple de la réponse de Jon Skeet qui est uniformément distribuée entre 0 et 1 (je réutilise sa méthode d'extension NextInt32()) :

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

Vous pouvez également discuter de la manière d'obtenir une distribution uniforme sur toute la gamme des décimales. Il existe probablement un moyen plus simple de le faire, mais cette légère modification de la méthode de calcul de la distribution uniforme est la suivante La réponse de John Leidegren devrait produire une distribution relativement uniforme :

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);
}

Fondamentalement, nous veillons à ce que les valeurs d'échelle soient choisies proportionnellement à la taille de la plage correspondante.

Cela signifie que nous devrions obtenir une échelle de 0 dans 90 % des cas - puisque cette plage contient 90 % de la plage possible - une échelle de 1 dans 9 % des cas, etc.

Il y a encore quelques problèmes avec l'implémentation, puisqu'elle ne prend pas en compte le fait que certains nombres ont des représentations multiples - mais elle devrait être beaucoup plus proche d'une distribution uniforme que les autres implémentations.

6voto

Danil Points 1084

Voici une implémentation de Decimal random with Range qui fonctionne bien pour moi.

public static decimal NextDecimal(this Random rnd, decimal from, decimal to)
{
    byte fromScale = new System.Data.SqlTypes.SqlDecimal(from).Scale;
    byte toScale = new System.Data.SqlTypes.SqlDecimal(to).Scale;

    byte scale = (byte)(fromScale + toScale);
    if (scale > 28)
        scale = 28;

    decimal r = new decimal(rnd.Next(), rnd.Next(), rnd.Next(), false, scale);
    if (Math.Sign(from) == Math.Sign(to) || from == 0 || to == 0)
        return decimal.Remainder(r, to - from) + from;

    bool getFromNegativeRange = (double)from + rnd.NextDouble() * ((double)to - (double)from) < 0;
    return getFromNegativeRange ? decimal.Remainder(r, -from) + from : decimal.Remainder(r, to);
}

5voto

John Leidegren Points 21951

J'aime la deuxième approche de Jon Skeet, voici une troisième alternative. Ne tenez pas compte de mon astuce de sous-classement, ceci devrait être écrit comme une méthode d'extension. La méthode protégée Sample() est exposée par la méthode publique NextDouble() (qui en interne, lorsque vous spécifiez une plage personnalisée, est utilisée pour générer les chiffres).

public static decimal NextDecimal(this Random 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;
    var s = (byte)(29 * r.NextDouble());
    return new Decimal(a, b, c, n, s);
}

2voto

Daniel Ballinger Points 4883

J'ai été un peu perplexe à ce sujet. C'est le mieux que j'ai pu trouver :

public class DecimalRandom : Random
    {
        public override decimal NextDecimal()
        {
            //The low 32 bits of a 96-bit integer. 
            int lo = this.Next(int.MinValue, int.MaxValue);
            //The middle 32 bits of a 96-bit integer. 
            int mid = this.Next(int.MinValue, int.MaxValue);
            //The high 32 bits of a 96-bit integer. 
            int hi = this.Next(int.MinValue, int.MaxValue);
            //The sign of the number; 1 is negative, 0 is positive. 
            bool isNegative = (this.Next(2) == 0);
            //A power of 10 ranging from 0 to 28. 
            byte scale = Convert.ToByte(this.Next(29));

            Decimal randomDecimal = new Decimal(lo, mid, hi, isNegative, scale);

            return randomDecimal;
        }
    }

Edit : Comme indiqué dans les commentaires lo, mid et hi ne peuvent jamais contenir int.MaxValue donc la gamme complète des décimales n'est pas possible.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X