117 votes

Trouver le nombre de décimales d'une valeur décimale indépendamment de la culture

Je me demande s'il existe un moyen concis et précis d'extraire le nombre de décimales d'une valeur décimale (sous la forme d'un int) qui puisse être utilisé en toute sécurité dans différentes cultures ?

Par exemple :
19.0 devrait retourner 1,
27.5999 devrait retourner 4,
19.12 devrait revenir 2,
etc.

J'ai écrit une requête qui fait une séparation de chaîne sur un point pour trouver les décimales :

int priceDecimalPlaces = price.ToString().Split('.').Count() > 1 
                  ? price.ToString().Split('.').ToList().ElementAt(1).Length 
                  : 0;

Mais il me semble que cette méthode ne fonctionnera que dans les régions qui utilisent le "." comme séparateur décimal et qu'elle est donc très fragile dans les différents systèmes.

200voto

burning_LEGION Points 7262

J'ai utilisé La méthode de Joe pour résoudre ce problème :)

decimal argument = 123.456m;
int count = BitConverter.GetBytes(decimal.GetBits(argument)[3])[2];

44voto

G.Y Points 2631

Puisqu'aucune des réponses fournies n'était assez bonne pour le nombre magique "-0.01f" converti en décimal c'est-à-dire : GetDecimal((decimal)-0.01f);
Je ne peux que supposer qu'un colossal virus de l'esprit a attaqué tout le monde il y a 3 ans :)
Voici ce qui semble être une implémentation fonctionnelle de ce problème maléfique et monstrueux, le problème très compliqué du comptage des décimales après le point - pas de chaînes, pas de cultures, pas besoin de compter les bits et pas besoin de lire des forums de mathématiques juste de simples mathématiques de 3ème année.

public static class MathDecimals
{
    public static int GetDecimalPlaces(decimal n)
    {
        n = Math.Abs(n); //make sure it is positive.
        n -= (int)n;     //remove the integer part of the number.
        var decimalPlaces = 0;
        while (n > 0)
        {
            decimalPlaces++;
            n *= 10;
            n -= (int)n;
        }
        return decimalPlaces;
    }
}

private static void Main(string[] args)
{
    Console.WriteLine(1/3m); //this is 0.3333333333333333333333333333
    Console.WriteLine(1/3f); //this is 0.3333333

    Console.WriteLine(MathDecimals.GetDecimalPlaces(0.0m));                  //0
    Console.WriteLine(MathDecimals.GetDecimalPlaces(1/3m));                  //28
    Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)(1 / 3f)));     //7
    Console.WriteLine(MathDecimals.GetDecimalPlaces(-1.123m));               //3
    Console.WriteLine(MathDecimals.GetDecimalPlaces(43.12345m));             //5
    Console.WriteLine(MathDecimals.GetDecimalPlaces(0));                     //0
    Console.WriteLine(MathDecimals.GetDecimalPlaces(0.01m));                 //2
    Console.WriteLine(MathDecimals.GetDecimalPlaces(-0.001m));               //3
    Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)-0.00000001f)); //8
    Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)0.0001234f));   //7
    Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)0.01f));        //2
    Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)-0.01f));       //2
}

26voto

Joe Points 60749

J'utiliserais probablement la solution dans La réponse de @fixagon .

Cependant, bien que la structure Decimal ne dispose pas d'une méthode pour obtenir le nombre de décimales, vous pourriez appeler Decimal.GetBits pour extraire la représentation binaire, puis utiliser la valeur entière et l'échelle pour calculer le nombre de décimales.

Cette méthode serait probablement plus rapide que le formatage en tant que chaîne de caractères, bien que vous deviez traiter un très grand nombre de décimales pour remarquer la différence.

Je laisse la mise en œuvre comme un exercice.

20voto

Kris Dimitrov Points 66

L'une des meilleures solutions pour trouver le nombre de chiffres après la virgule est présentée dans le tableau suivant Le message de burning_LEGION .

Ici, j'utilise des parties d'un article du forum STSdb : Nombre de chiffres après le point décimal .

Dans MSDN, nous pouvons lire l'explication suivante :

" Un nombre décimal est une valeur à virgule flottante qui se compose d'un signe, d'une valeur numérique où chaque chiffre de la valeur va de 0 à 9, et un facteur d'échelle qui indique la position d'un point décimal flottant qui sépare les parties intégrales et fractionnaires de la valeur numérique. "

Et aussi :

"La représentation binaire d'une valeur décimale se compose d'un signe de 1 bit, d'un nombre entier de 96 bits et d'un facteur d'échelle utilisé pour diviser l'entier de 96 bits et spécifier quelle partie est une fraction décimale. et préciser quelle partie de celui-ci est une fraction décimale. Le facteur d'échelle est implicitement le nombre 10, élevé à un exposant allant de 0 à 28."

Au niveau interne, la valeur décimale est représentée par quatre valeurs entières.

Decimal internal representation

Il existe une fonction GetBits accessible au public pour obtenir la représentation interne. La fonction renvoie un tableau d'int[] :

[__DynamicallyInvokable] 
public static int[] GetBits(decimal d)
{
    return new int[] { d.lo, d.mid, d.hi, d.flags };
}

Le quatrième élément du tableau retourné contient un facteur d'échelle et un signe. Et comme le dit le MSDN, le facteur d'échelle est implicitement le nombre 10, élevé à un exposant allant de 0 à 28. C'est exactement ce dont nous avons besoin.

Ainsi, sur la base de toutes les investigations ci-dessus, nous pouvons construire notre méthode :

private const int SIGN_MASK = ~Int32.MinValue;

public static int GetDigits4(decimal value)
{
    return (Decimal.GetBits(value)[3] & SIGN_MASK) >> 16;
}

Ici, un SIGN_MASK est utilisé pour ignorer le signe. Après les logiques et nous avons également décalé le résultat de 16 bits vers la droite pour recevoir le facteur d'échelle réel. Cette valeur, enfin, indique le nombre de chiffres après le point décimal.

Notez qu'ici, MSDN indique également que le facteur d'échelle préserve également les zéros de fin dans un nombre décimal. Les zéros de queue n'affectent pas la valeur d'un nombre décimal dans les opérations arithmétiques ou de comparaison. Cependant, les zéros de queue peuvent être révélés par la méthode ToString si une chaîne de format appropriée est appliquée.

Ces solutions semblent être les meilleures, mais attendez, il y en a d'autres. Par Accès aux méthodes privées en C# nous pouvons utiliser des expressions pour construire un accès direct au champ flags et éviter de construire le tableau int :

public delegate int GetDigitsDelegate(ref Decimal value);

public class DecimalHelper
{
    public static readonly DecimalHelper Instance = new DecimalHelper();

    public readonly GetDigitsDelegate GetDigits;
    public readonly Expression<GetDigitsDelegate> GetDigitsLambda;

    public DecimalHelper()
    {
        GetDigitsLambda = CreateGetDigitsMethod();
        GetDigits = GetDigitsLambda.Compile();
    }

    private Expression<GetDigitsDelegate> CreateGetDigitsMethod()
    {
        var value = Expression.Parameter(typeof(Decimal).MakeByRefType(), "value");

        var digits = Expression.RightShift(
            Expression.And(Expression.Field(value, "flags"), Expression.Constant(~Int32.MinValue, typeof(int))), 
            Expression.Constant(16, typeof(int)));

        //return (value.flags & ~Int32.MinValue) >> 16

        return Expression.Lambda<GetDigitsDelegate>(digits, value);
    }
}

Ce code compilé est affecté au champ GetDigits. Notez que la fonction reçoit la valeur décimale en tant que ref, donc aucune copie réelle n'est effectuée - seulement une référence à la valeur. L'utilisation de la fonction GetDigits de DecimalHelper est simple :

decimal value = 3.14159m;
int digits = DecimalHelper.Instance.GetDigits(ref value);

Il s'agit de la méthode la plus rapide pour obtenir le nombre de chiffres après la virgule pour les valeurs décimales.

20voto

Clement Points 587

S'appuyer sur la représentation interne des décimales n'est pas cool.

Que dites-vous de ça ?

    int CountDecimalDigits(decimal n)
    {
        return n.ToString(System.Globalization.CultureInfo.InvariantCulture)
                //.TrimEnd('0') uncomment if you don't want to count trailing zeroes
                .SkipWhile(c => c != '.')
                .Skip(1)
                .Count();
    }

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