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) qui sera sûr à utiliser dans différentes informations de culture?
Par exemple:
19.0 doit renvoyer 1,
27.5999 doit renvoyer 4,
19.12 doit renvoyer 2,
etc.
J'ai écrit une requête qui a fait une division de chaîne sur un point 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 dans différents systèmes.
c#
decimal
cultureinfo
Jesse Carter
la source
la source
19.0
de retour1
est un détail d'implémentation concernant le stockage interne de la valeur19.0
. Le fait est qu'il est parfaitement légitime que le programme stocke ceci sous la forme190×10⁻¹
ou1900×10⁻²
ou19000×10⁻³
. Tous ces éléments sont égaux. Le fait qu'il utilise la première représentation lorsqu'on lui attribue une valeur de19.0M
et que celle-ci soit exposée lors de l'utilisationToString
sans spécificateur de format est juste une coïncidence et une chose heureuse. Sauf que ce n'est pas heureux quand les gens comptent sur l'exposant dans les cas où ils ne devraient pas.19M
de manière fiable19.0M
de19.00M
, vous devrez créer une nouvelle classe qui regroupe la valeur sous-jacente comme une propriété et le nombre de décimales comme une autre propriété.Réponses:
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];
la source
decimal
garde le nombre de chiffres après la virgule, c'est pourquoi vous trouvez ce "problème", vous devez convertir décimal en double et en décimal à nouveau pour corriger: BitConverter.GetBytes (decimal.GetBits ((decimal) (double) argument) [3]) [ 2];Étant donné qu'aucune des réponses fournies n'était assez bonne pour le nombre magique "-0.01f" converti en décimal .. ie:
GetDecimal((decimal)-0.01f);
je ne peux que supposer qu'un virus colossal a attaqué tout le monde il y a 3 ans :)
Voici ce qui semble fonctionner mise en œuvre de ce problème diabolique 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 de 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 }
la source
n = n % 1; if (n < 0) n = -n;
car une valeur plus grande queint.MaxValue
provoquera unOverflowException
, par exemple2147483648.12345
.J'utiliserais probablement la solution dans la 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.
Ce serait probablement plus rapide que le formatage sous forme de chaîne, bien que vous deviez traiter énormément de décimales pour remarquer la différence.
Je vais laisser la mise en œuvre comme un exercice.
la source
L'une des meilleures solutions pour trouver le nombre de chiffres après la virgule décimale est indiquée dans le message de burn_LEGION .
Ici, j'utilise des parties 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 qui se compose d'un signe, une valeur numérique où chaque chiffre de la valeur est compris entre 0 et 9, et un facteur d'échelle qui indique la position d'une virgule flottante qui sépare l'intégrale et la fraction parties 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 de mise à l'échelle utilisé pour diviser l'entier de 96 bits et spécifier quelle partie est une fraction décimale. Le facteur de mise à l'é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.
Il existe une fonction GetBits accessible au public pour obtenir la représentation interne. La fonction renvoie 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 renvoyé contient un facteur d'échelle et un signe. Et comme le MSDN le dit, 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 logique 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 la virgule décimale.
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. Cependant, des 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 ressemble à la meilleure, mais attendez, il y en a plus. En accédant aux 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, 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);
C'est la méthode la plus rapide possible pour obtenir le nombre de chiffres après la virgule décimale pour les valeurs décimales.
la source
0.01
et ce0.010
sont des nombres exactement égaux . De plus, l'idée qu'un type de données numérique a une sorte de sémantique «nombre de chiffres utilisés» sur laquelle on peut se fier est complètement erronée (à ne pas confondre avec «nombre de chiffres autorisé». Ne confondez pas la présentation (l'affichage de la valeur d'un nombre dans une base particulière, par exemple, le développement décimal de la valeur indiquée par le développement binaire 111) avec la valeur sous-jacente! Pour rappel, les nombres ne sont pas des chiffres, ni ne sont constitués de chiffres .Se fier à la représentation interne des décimales 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(); }
la source
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); }
la source
.
.La plupart des gens ici ne semblent pas savoir que le nombre décimal considère les zéros de fin comme importants pour le stockage et l'impression.
Ainsi, 0,1 m, 0,10 m et 0,100 m peuvent être comparés comme égaux, ils sont stockés différemment (en tant que valeur / échelle 1/1, 10/2 et 100/3, respectivement), et seront imprimés comme 0,1, 0,10 et 0,100, respectivement , par
ToString()
.En tant que telles, les solutions qui rapportent "une précision trop élevée" rapportent en fait la précision correcte , selon
decimal
les conditions.De plus, les solutions basées sur les mathématiques (comme la multiplication par des puissances de 10) seront probablement très lentes (la décimale est ~ 40x plus lente que le double pour l'arithmétique, et vous ne voulez pas non plus mélanger en virgule flottante car cela risque d'introduire une imprécision. ). De même, la conversion vers
int
oulong
comme moyen de troncature est sujette aux erreurs (decimal
a une plage beaucoup plus grande que l'une ou l'autre - elle est basée sur un entier de 96 bits).Bien que n'étant pas élégant en tant que tel, ce qui suit sera probablement l'un des moyens les plus rapides d'obtenir la précision (lorsqu'il est défini comme "décimales à l'exclusion des zéros de fin"):
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 «.» comme point décimal, les zéros de fin sont coupés, puis il suffit de voir combien de positions restent après le point décimal (s'il y en a même une).
Edit: modification du type de retour en int
la source
Et voici une autre façon, utilisez le type SqlDecimal qui a une propriété d'échelle avec le nombre de chiffres à droite de la décimale. Convertissez votre valeur décimale en SqlDecimal, puis accédez à Scale.
((SqlDecimal)(decimal)yourValue).Scale
la source
GetBytes
afin qu'il alloue le tableau Byte au lieu d'accéder aux octets dans un contexte non sécurisé. Il y a même une note et un code commenté dans le code de référence, indiquant cela et comment ils pourraient le faire à la place. Pourquoi ils ne l'ont pas fait est un mystère pour moi. Je resterais à l'écart de cela et accéderais directement aux bits d'échelle au lieu de cacher le GC Alloc dans ce casting, car ce n'est tout simplement pas très évident ce qu'il fait sous le capot.Jusqu'à présent, presque toutes les solutions répertoriées allouent de la mémoire GC, ce qui est vraiment la façon C # de faire les choses, mais loin d'être idéale dans les environnements critiques en termes de performances. (Ceux qui n'allouent pas de boucles d'utilisation et ne prennent pas non plus en compte les zéros de fin.)
Donc, pour éviter GC Allocs, vous pouvez simplement accéder aux bits d'échelle dans un contexte non sécurisé. Cela peut sembler fragile, mais selon la source de référence de Microsoft , la disposition struct de decimal est séquentielle et contient même un commentaire, pour ne pas changer 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 voir, 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 de 16 à 24 codent l'échelle et que nous devons éviter le 31e bit qui code le signe. Puisque int a 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 ou de conversions ToString. Je l'ai testé avec .Net 4.x et .Net 3.5 dans Unity 2019.1. S'il y a des versions où cela échoue, veuillez me le faire savoir.
Éditer:
Merci à @Zastai de m'avoir rappelé la possibilité d'utiliser une disposition de structure explicite pour réaliser pratiquement la même logique de pointeur en dehors du code unsafe:
[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 d'origine, vous pouvez supprimer tous les champs en plus
Value
etScale
peut-être que cela pourrait être utile pour quelqu'un de tous les avoir.la source
[StructLayout(LayoutKind.Explicit)] public struct DecimalHelper { [FieldOffset(0)] public decimal Value; [FieldOffset(0)] public uint Flags; [FieldOffset(0)] public ushort Reserved; [FieldOffset(2)] public byte Scale; [FieldOffset(3)] public DecimalSign Sign; [FieldOffset(4)] public uint ValuePart1; [FieldOffset(8)] public ulong ValuePart2; }
const decimal Foo = 1.0000000000000000000000000000m;
alors diviser une décimale par cela la remettra à l'échelle la plus basse possible (c'est-à-dire n'incluant plus les zéros décimaux de fin). Je n'ai pas évalué cela pour voir si oui ou non c'est plus rapide que l'approche basée sur les chaînes que j'ai suggérée ailleurs.J'ai écrit une petite méthode concise hier qui renvoie également le nombre de décimales sans avoir à compter sur des divisions 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;
la source
19.0 should return 1
. Cette solution prendra toujours une quantité minimale de 1 décimale et ignorera les zéros de fin. decimal peut avoir ceux-ci car il utilise un facteur d'échelle. Le facteur d'échelle est accessible comme dans les octets 16 à 24 de l'élément avec l'indice 3 dans le tableau obtenu à partir deDecimal.GetBytes()
ou en utilisant la logique de pointeur.J'utilise quelque chose de très similaire à la réponse de Clément:
private int GetSignificantDecimalPlaces(decimal number, bool trimTrailingZeros = true) { string stemp = Convert.ToString(number); if (trimTrailingZeros) stemp = stemp.TrimEnd('0'); return stemp.Length - 1 - stemp.IndexOf( Application.CurrentCulture.NumberFormat.NumberDecimalSeparator); }
N'oubliez pas d'utiliser System.Windows.Forms pour accéder à Application.CurrentCulture
la source
Tu peux essayer:
int priceDecimalPlaces = price.ToString(System.Globalization.CultureInfo.InvariantCulture) .Split('.')[1].Length;
la source
[1]
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 ();
appeler GetDecimalLength (instring)
la source
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; }
la source
19.0 should return 1
. Cette solution ignorera les zéros de fin. decimal peut avoir ceux-ci car il utilise un facteur d'échelle. Le facteur d'échelle est accessible comme dans les octets 16 à 24 de l'élément avec l'index 3 dans leDecimal.GetBytes()
tableau ou en utilisant la logique du pointeur.string number = "123.456789"; // Convert to string int length = number.Substring(number.IndexOf(".") + 1).Length; // 6
la source
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); }
la source
En tant que méthode d'extension décimale qui prend en compte:
public static class DecimalExtensions { public static int GetNumberDecimalPlaces(this decimal source) { var parts = source.ToString(CultureInfo.InvariantCulture).Split('.'); if (parts.Length < 2) return 0; return parts[1].TrimEnd('0').Length; } }
la source