web-dev-qa-db-fra.com

Trouver le nombre de décimales dans la 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 dans une valeur décimale (en tant qu’int) qu’il sera sans danger d’utiliser dans différentes informations sur la culture?

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

J'ai écrit une requête qui a divisé la chaîne sur une période pour trouver des décimales:

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

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

68
Jesse Carter

J'ai utilisé façon de Joe pour résoudre ce problème :)

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

Etant donné qu'aucune des réponses fournies n'était assez bonne pour que le nombre magique "-0.01f" soit converti en décimal .. c'est-à-dire: GetDecimal((decimal)-0.01f);
Je ne peux que supposer qu'un virus colossal pète-esprit a attaqué tout le monde il y a 3 ans :)
Voici ce qui semble être une mise en œuvre efficace de ce problème monstrueux et monstrueux, le problème très compliqué de compter les décimales après le point - pas de chaînes, pas de cultures, pas besoin de compter les bits et pas besoin de lire les forums de mathématiques .. juste simples mathématiques de 3e 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
}
20
G.Y

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

Cependant, bien que la structure Decimal n'ait pas de méthode pour obtenir le nombre de décimales, vous pouvez 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.

Cela serait probablement plus rapide que le formatage en tant que chaîne, bien que vous deviez traiter beaucoup de nombres décimaux pour constater la différence.

Je laisserai la mise en œuvre comme un exercice.

17
Joe

Une des meilleures solutions pour trouver le nombre de chiffres après la virgule décimale est indiquée dans post de burning_LEGION .

Ici, j'utilise des éléments d'un article du forum STSdb: Nombre de chiffres après la virgule décimale .

Dans MSDN, nous pouvons lire l'explication suivante:

"Un nombre décimal est une valeur à virgule flottante constituée d'un signe, d'une valeur numérique où chaque chiffre de la valeur est compris entre 0 et 9, et d'un facteur d'échelle indiquant la position d'un point décimal flottant séparant les parties intégrale et fractionnaire de la valeur numérique. "

Et aussi:

"La représentation binaire d'une valeur décimale se compose d'un signe à 1 bit, d'un nombre entier à 96 bits et d'un facteur d'échelle utilisé pour diviser l'entier de 96 bits et spécifier quelle partie de celui-ci est un nombre décimal. Le facteur d'échelle est implicitement le nombre 10, élevé à un exposant compris entre 0 et 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 retourne un tableau 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 MSDN, le facteur d’échelle est implicitement le nombre 10, élevé à un exposant compris entre 0 et 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 logique et nous avons également décalé le résultat de 16 bits vers la droite pour recevoir le facteur d'échelle réel. Enfin, cette valeur indique le nombre de chiffres après le point décimal.

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

Cette solution semble être la meilleure, mais attendez, il y a plus. Par en accédant à des méthodes privées en C # , nous pouvons utiliser des expressions pour créer 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, de sorte qu'aucune copie réelle n'est effectuée - uniquement une référence à la valeur. Utiliser la fonction GetDigits de DecimalHelper est simple:

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

C'est la méthode la plus rapide possible pour obtenir le nombre de chiffres après le point décimal pour les valeurs décimales.

15
Kris

vous pouvez utiliser InvariantCulture

string priceSameInAllCultures = price.ToString(System.Globalization.CultureInfo.InvariantCulture);

une autre possibilité serait de faire quelque chose comme ça:

private int GetDecimals(decimal d, int i = 0)
{
    decimal multiplied = (decimal)((double)d * Math.Pow(10, i));
    if (Math.Round(multiplied) == multiplied)
        return i;
    return GetDecimals(d, i+1);
}
11
fixagon

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

Que dis-tu 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();
    }
10
Clement

La plupart des gens ici ne semblent pas savoir que la virgule décimale considère les zéros à la fin comme importants pour le stockage et l'impression.

Ainsi, 0.1m, 0.10m et 0.100m peuvent être comparés de manière égale, ils sont stockés différemment (valeur/échelle 1/1, 10/2 et 100/3, respectivement), et seront imprimés sous les formats 0.1, 0.10 et 0.100, respectivement. , par ToString().

En tant que telles, les solutions signalant une "précision trop élevée" signalent en fait la précision correcte, selon les termes de decimal.

De plus, les solutions basées sur les mathématiques (comme multiplier par des puissances de 10) seront probablement très lentes (la décimale est environ 40 fois plus lente que le double pour l'arithmétique, et vous ne voulez pas mélanger en virgule flottante non plus, car cela risquerait d'introduire de l'imprécision. ) De même, la diffusion vers int ou long comme moyen de troncature est sujette aux erreurs (decimal a une plage beaucoup plus grande que celles-ci - elle est basée sur un code 96 bits entier).

Bien que non élégant en tant que tel, ce qui suit constituera probablement l’un des moyens les plus rapides d’obtenir la précision (lorsque défini comme "décimales excluant les zéros").

public static int PrecisionOf(decimal d) {
  var text = d.ToString(System.Globalization.CultureInfo.InvariantCulture).TrimEnd('0');
  var decpoint = text.IndexOf('.');
  if (decpoint < 0)
    return 0;
  return text.Length - decpoint - 1;
}

La culture invariante garantit un '.' en virgule décimale, les zéros à la fin sont supprimés, puis il suffit de voir combien de positions restent après la virgule (s'il en existe une).

Edit: a changé le type de retour en int

6
Zastai

Et voici une autre façon, utilisez le type SqlDecimal qui a une propriété scale avec le nombre de chiffres à droite de la décimale. Convertissez votre valeur décimale en SqlDecimal, puis accédez à Echelle.

((SqlDecimal)(decimal)yourValue).Scale
5
RitchieD

Hier, j’ai écrit une petite méthode concise qui renvoie également le nombre de décimales sans avoir à s’appuyer sur des fractionnements de chaînes ou des cultures, ce qui est idéal:

public int GetDecimalPlaces(decimal decimalNumber) { // 
try {
    // PRESERVE:BEGIN
        int decimalPlaces = 1;
        decimal powers = 10.0m;
        if (decimalNumber > 0.0m) {
            while ((decimalNumber * powers) % 1 != 0.0m) {
                powers *= 10.0m;
                ++decimalPlaces;
            }
        }
return decimalPlaces;
2
Jesse Carter

Jusqu'à présent, presque toutes les solutions répertoriées allouent de la mémoire GC, ce qui est vraiment la manière de faire C #, mais loin d'être idéale dans les environnements critiques en termes de performances. (Ceux qui n'allouent pas de boucles utilisent et ne tiennent pas compte des zéros de fin.)

Donc, pour éviter les GC Allocs, vous pouvez simplement accéder aux bits de la balance dans un contexte dangereux. Cela peut sembler fragile, mais comme le dit source de référence de Microsoft , la structure de la structure de décimale est séquentielle et contient même un commentaire, pour ne pas modifier l'ordre des champs:

    // NOTE: Do not change the order in which these fields are declared. The
    // native methods in this class rely on this particular order.
    private int flags;
    private int hi;
    private int lo;
    private int mid;

Comme vous pouvez le constater, le premier int ici est le champ flags. D'après la documentation et comme mentionné dans d'autres commentaires ici, nous savons que seuls les bits compris entre 16 et 24 codent la balance et que nous devons éviter le 31e bit qui code le signe. Puisque int est la taille de 4 octets, nous pouvons le faire en toute sécurité:

internal static class DecimalExtensions
{
  public static byte GetScale(this decimal value)
  {
    unsafe
    {
      byte* v = (byte*)&value;
      return v[2];
    }
  }
}

Cela devrait être la solution la plus performante car il n'y a pas d'allocation GC du tableau d'octets ni de conversions ToString. Je l'ai testé contre .Net 4.x et .Net 3.5 dans Unity 2019.1. S'il y a des versions où cela échoue, merci de me le faire savoir.

Edit:

Merci à @Zastai de m'avoir rappelé la possibilité d'utiliser une structure de structure explicite pour réaliser pratiquement la même logique de pointeur en dehors du code peu sûr:

[StructLayout(LayoutKind.Explicit)]
public struct DecimalHelper
{
    const byte k_SignBit = 1 << 7;

    [FieldOffset(0)]
    public decimal Value;

    [FieldOffset(0)]
    public readonly uint Flags;
    [FieldOffset(0)]
    public readonly ushort Reserved;
    [FieldOffset(2)]
    byte m_Scale;
    public byte Scale
    {
        get
        {
            return m_Scale;
        }
        set
        {
            if(value > 28)
                throw new System.ArgumentOutOfRangeException("value", "Scale can't be bigger than 28!")
            m_Scale = value;
        }
    }
    [FieldOffset(3)]
    byte m_SignByte;
    public int Sign
    {
        get
        {
            return m_SignByte > 0 ? -1 : 1;
        }
    }
    public bool Positive
    {
        get
        {
            return (m_SignByte & k_SignBit) > 0 ;
        }
        set
        {
            m_SignByte = value ? (byte)0 : k_SignBit;
        }
    }
    [FieldOffset(4)]
    public uint Hi;
    [FieldOffset(8)]
    public uint Lo;
    [FieldOffset(12)]
    public uint Mid;

    public DecimalHelper(decimal value) : this()
    {
        Value = value;
    }

    public static implicit operator DecimalHelper(decimal value)
    {
        return new DecimalHelper(value);
    }

    public static implicit operator decimal(DecimalHelper value)
    {
        return value.Value;
    }
}

Pour résoudre le problème initial, vous pouvez supprimer tous les champs autres que Value et Scale, mais il serait peut-être utile que quelqu'un les ait tous.

2
Martin Tilo Schmitz

J'utilise le mécanisme suivant dans mon code

  public static int GetDecimalLength(string tempValue)
    {
        int decimalLength = 0;
        if (tempValue.Contains('.') || tempValue.Contains(','))
        {
            char[] separator = new char[] { '.', ',' };
            string[] tempstring = tempValue.Split(separator);

            decimalLength = tempstring[1].Length;
        }
        return decimalLength;
    }

entrée décimale = 3,376; var instring = input.ToString ();

appelez GetDecimalLength (instring)

1
Srikanth
string number = "123.456789"; // Convert to string
int length = number.Substring(number.IndexOf(".") + 1).Length;  // 6
1
Eva Chang

Tu peux essayer:

int priceDecimalPlaces =
        price.ToString(System.Globalization.CultureInfo.InvariantCulture)
              .Split('.')[1].Length;
1
NicoRiff

Je suggère d'utiliser cette méthode:

    public static int GetNumberOfDecimalPlaces(decimal value, int maxNumber)
    {
        if (maxNumber == 0)
            return 0;

        if (maxNumber > 28)
            maxNumber = 28;

        bool isEqual = false;
        int placeCount = maxNumber;
        while (placeCount > 0)
        {
            decimal vl = Math.Round(value, placeCount - 1);
            decimal vh = Math.Round(value, placeCount);
            isEqual = (vl == vh);

            if (isEqual == false)
                break;

            placeCount--;
        }
        return Math.Min(placeCount, maxNumber); 
    }
0
Veysel Özdemir

En utilisant la récursivité, vous pouvez faire:

private int GetDecimals(decimal n, int decimals = 0)  
{  
    return n % 1 != 0 ? GetDecimals(n * 10, decimals + 1) : decimals;  
}
0
Mars