Find the number of decimal places in decimal value regardless of culture

I am wondering if there is a short and accurate way to output the number of decimal places in a decimal value (like int) that will be safe to use in different cultural data?

For example:
19.0 should return 1,
27.5999 should return 4,
12.19 must return 2,
and etc.

I wrote a query that divided the string by period to find decimals:

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

But it occurs to me that this will only work in regions that use ".". as a decimal separator and therefore very fragile in different systems.

+71
decimal c # cultureinfo
Nov 20
source share
14 answers

I used Joe way to solve this problem :)

 decimal argument = 123.456m; int count = BitConverter.GetBytes(decimal.GetBits(argument)[3])[2]; 
+142
Nov 21 '12 at 12:55
source share

Since none of the answers was good enough for the magic number "-0.01f" converted to decimal number. ie: GetDecimal((decimal)-0.01f);
I can only assume that a colossal fart virus attacked everyone 3 years ago :)
Here is what seems to be a working implementation of this evil and monstrous problem, a very difficult task of counting decimal places after a period - no lines, no cultures, no need to count bits, no need to read math forums. just simple 3rd grade math.

 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
May 13 '15 at 3:27
source share

I will probably use the solution in @fixagon's answer .

However, although there is no way in the Decimal structure to get the number of decimal places, you can call Decimal.GetBits to retrieve the binary representation, then use an integer value and scale to calculate the number of decimal places.

This will probably be faster than formatting as a string, although you will have to process a huge amount of decimal places to notice the difference.

I will leave the implementation as an exercise.

+18
Nov 20
source share

One of the best solutions for finding the number of digits after a decimal point is shown in the burn_LEGION message.

Here I use the details from the STSdb ​​forum article: The number of digits after the decimal point .

At MSDN, we can read the following explanation:

"A decimal number is a floating point value that consists of a sign, a numerical value, where each digit in the value is in the range from 0 to 9, and a scaling factor that indicates the position of the floating decimal point that separates the integral and fractional parts of the numerical value . "

As well as:

“The binary representation of a decimal value consists of a 1-bit character, a 96-bit integer and a scaling factor used to separate a 96-bit integer and indicate which part it is a decimal fraction. The scaling factor is implicitly equal to the number 10 raised to an exponent in the range from 0 to 28. "

At the internal level, the decimal value is represented by four integer values.

Decimal internal representation

There is a publicly available GetBits function for retrieving an internal view. The function returns an array int []:

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

The fourth element of the returned array contains a scale factor and a sign. And as MSDN says that the scaling factor is implicitly equal to 10, it increases to an exponent from 0 to 28. This is exactly what we need.

Thus, based on all the above studies, we can build our method:

 private const int SIGN_MASK = ~Int32.MinValue; public static int GetDigits4(decimal value) { return (Decimal.GetBits(value)[3] & SIGN_MASK) >> 16; } 

Here, the SIGN_MASK character is used to ignore the character. After the logical one, we also shifted the result from 16 bits to the right to get the actual scale factor. This value finally indicates the number of digits after the decimal point.

Note that here MSDN also says that the scaling factor also stores any trailing zeros in decimal. Trailing zeros do not affect the decimal value in arithmetic or comparative operations. However, trailing zeros can be detected by the ToString method if the appropriate format string is used.

These solutions look like the best, but wait, there are more. Using access to private methods in C # , we can use expressions to create direct access to the flags field and avoid building an int array:

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

This compiled code is assigned to the GetDigits field. Note that the function takes the decimal value as ref, so the actual copying is not performed - only a reference to the value. Using the GetDigits function of DecimalHelper is very simple:

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

This is the fastest way to get the number of digits after the decimal point for decimal values.

+15
Jul 03 '14 at 8:39
source share

you can use InvariantCulture

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

another possibility would be to do something like this:

 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
Nov 20 '12 at 16:35
source share

Relying on the internal representation of decimal places is not cool.

How about this:

  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
Sep 29 '15 at 7:08
source share

Most people here don't seem to know that the decimal value considers trailing zeros significant for storage and printing. A.

Thus, 0.1 m, 0.10 m and 0.100 m can be compared as equal, they are saved in different ways (as a value / scale of 1/1, 10/2 and 100/3, respectively) and will be printed as 0.1 , 0.10 and 0.100, respectively, ToString() .

Thus, solutions that report "too high an accuracy" actually report the correct accuracy in decimal terms.

In addition, math-based solutions (e.g., multiplication by powers of 10) are likely to be very slow (the decimal value is ~ 40x slower than double for arithmetic, and you don't want to mix in a floating point either because it is likely to introduce inaccuracy). Similarly, dropping to int or long as a truncation means is error prone ( decimal has a much wider range than any of them - it is based on a 96-bit integer value).

Although not elegant as such, the following is likely to be one of the fastest ways to obtain accuracy (if it is defined as “decimal places excluding trailing zeros”):

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

An invariant culture guarantees a. like a decimal point, trailing zeros are truncated, and then it’s just a matter of how many positions are left after the decimal point (if they don’t even exist).

Edit: change return type to int

+6
Aug 6 '15 at 8:32
source share

And in another way, use the SqlDecimal type, which has a scale property with a digit count to the right of the decimal. Enter the decimal value in SqlDecimal, and then access the scale.

 ((SqlDecimal)(decimal)yourValue).Scale 
+5
Mar 07 '17 at 20:20
source share

Yesterday I wrote a short little technique that also returns the number of decimal places without having to rely on any string partitions or cultures that are perfect:

 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
Nov 21
source share

To date, almost all of these solutions allocate GC memory, which is largely a C # method, but far from ideal in performance-critical environments. (Those that do not allocate usage cycles, and also do not account for trailing zeros.)

Thus, to avoid GC allocation, you can simply access the scale bits in an insecure context. This may seem fragile, but according to Microsoft's reference source , the decimal structure is Sequential and even contains a comment so as not to change the order of the fields:

  // 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; 

As you can see, the first int here is the flags field. From the documentation and, as already mentioned in other comments, we know that only bits from 16-24 encode the scale and that we need to avoid the 31st bit that encodes the character. Since int is 4 bytes in size, we can safely do this:

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

This should be the most productive solution since there is no GC allocation of a byte array or ToString conversions. I tested it on .Net 4.x and .Net 3.5 in Unity 2019.1. If there are any versions where this does not help, please let me know.

Edit:

Thanks to @Zastai for reminding us of the possibility of using an explicit layout of the structure to practically achieve the same pointer logic outside of unsafe code:

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

To solve the original problem, you can remove all fields except Value and Scale but it might be useful for someone to have all of them.

+2
Feb 04 '19 at 13:55
source share

You can try:

 int priceDecimalPlaces = price.ToString(System.Globalization.CultureInfo.InvariantCulture) .Split('.')[1].Length; 
+1
Nov 20 '12 at 16:42
source share

I use the following mechanism in my 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; } 

decimal input = 3.376; var instring = input.ToString ();

call GetDecimalLength (instring)

+1
Feb 10 '14 at 6:06
source share

I suggest using this method:

  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
Feb 13 '17 at 11:10
source share

With recursion, you can:

 private int GetDecimals(decimal n, int decimals = 0) { return n % 1 != 0 ? GetDecimals(n * 10, decimals + 1) : decimals; } 
0
Dec 18 '17 at 16:02
source share



All Articles