Find number of decimal places in decimal value regardless of culture

C#DecimalCultureinfo

C# Problem Overview


I'm wondering if there is a concise and accurate way to pull out the number of decimal places in a decimal value (as an int) that will be safe to use across different culture info?

For example:
19.0 should return 1,
27.5999 should return 4,
19.12 should return 2,
etc.

I wrote a query that did a string split on a period to find decimal places:

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 the '.' as a decimal separator and is therefore very brittle across different systems.

C# Solutions


Solution 1 - C#

I used Joe's way to solve this issue :)

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

Solution 2 - C#

Since none of the answers supplied were good enough for the magic number "-0.01f" converted to decimal.. i.e: GetDecimal((decimal)-0.01f);
I can only assume a colossal mind-fart virus attacked everyone 3 years ago :)
Here is what seems to be a working implementation to this evil and monstrous problem, the very complicated problem of counting the decimal places after the point - no strings, no cultures, no need to count the bits and 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
}

Solution 3 - C#

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

However, while the Decimal struct doesn't have a method to get the number of decimals, you could call http://msdn.microsoft.com/en-us/library/system.decimal.getbits.aspx">Decimal.GetBits</a> to extract the binary representation, then use the integer value and scale to compute the number of decimals.

This would probably be faster than formatting as a string, though you'd have to be processing an awful lot of decimals to notice the difference.

I'll leave the implementation as an exercise.

Solution 4 - C#

One of the best solutions for finding the number of digits after the decimal point is shown in burning_LEGION's post.

Here I am using parts from a STSdb forum article: Number of digits after decimal point.

In MSDN we can read the following explanation:

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

And also:

"The binary representation of a Decimal value consists of a 1-bit sign, a 96-bit integer number, and a scaling factor used to divide the 96-bit integer and specify what portion of it is a decimal fraction. The scaling factor is implicitly the number 10, raised to an exponent ranging from 0 to 28."

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

Decimal internal representation

There is a publicly available GetBits function for getting the internal representation. The function returns an int[] array:

[__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 the MSDN says the scaling factor is implicitly the number 10, raised to an exponent ranging from 0 to 28. This is exactly what we need.

Thus, based on all above investigations we can construct our method:

private const int SIGN_MASK = ~Int32.MinValue;

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

Here a SIGN_MASK is used to ignore the sign. After logical and we have also shifted the result with 16 bits to the right to receive the actual scale factor. This value, finally, indicates the number of digits after the decimal point.

Note that here MSDN also says the scaling factor also preserves any trailing zeros in a Decimal number. Trailing zeros do not affect the value of a Decimal number in arithmetic or comparison operations. However, trailing zeros might be revealed by the ToString method if an appropriate format string is applied.

This solutions looks like the best one, but wait, there is more. By accessing private methods in C# we can use expressions to build a direct access to the flags field and avoid constructing the 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 receives the decimal value as ref, so no actual copying is performed - only a reference to the value. Using the GetDigits function from the DecimalHelper is easy:

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

This is the fastest possible method for getting number of digits after decimal point for decimal values.

Solution 5 - C#

Relying on the internal representation of decimals 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();
    }

Solution 6 - C#

you can use the InvariantCulture

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

another possibility would be to do something like that:

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

Solution 7 - C#

Most people here seem to be unaware that decimal considers trailing zeroes as significant for storage and printing.

So 0.1m, 0.10m and 0.100m may compare as equal, they are stored differently (as value/scale 1/1, 10/2 and 100/3, respectively), and will be printed as 0.1, 0.10 and 0.100, respectively, by ToString().

As such, the solutions that report "too high a precision" are actually reporting the correct precision, on decimal's terms.

In addition, math-based solutions (like multiplying by powers of 10) will likely be very slow (decimal is ~40x slower than double for arithmetic, and you don't want to mix in floating-point either because that's likely to introduce imprecision). Similarly, casting to int or long as a means of truncating is error-prone (decimal has a much greater range than either of those - it's based around a 96-bit integer).

While not elegant as such, the following will likely be one of the fastest way to get the precision (when defined as "decimal places excluding trailing zeroes"):

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

The invariant culture guarantees a '.' as decimal point, trailing zeroes are trimmed, and then it's just a matter of seeing of how many positions remain after the decimal point (if there even is one).

Edit: changed return type to int

Solution 8 - C#

And here's another way, use the type SqlDecimal which has a scale property with the count of the digits right of the decimal. Cast your decimal value to SqlDecimal and then access Scale.

((SqlDecimal)(decimal)yourValue).Scale

Solution 9 - C#

I'm using something very similar to Clement's answer:

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

Remember to use System.Windows.Forms to get access to Application.CurrentCulture

Solution 10 - C#

So far, nearly all of the listed solutions are allocating GC Memory, which is very much the C# way to do things but far from ideal in performance critical environments. (The ones that do not allocate use loops and also don't take trailing zeros into consideration.)

So to avoid GC Allocs, you can just access the scale bits in an unsafe context. That might sound fragile but as per Microsoft's reference source, the struct layout of decimal is Sequential and even has a comment in there, 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 mentioned in other comments here, we know that only the bits from 16-24 encode the scale and that we need to avoid the 31st bit which encodes the sign. Since int is the size of 4 bytes, 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 performant solution since there is no GC alloc of the bytes array or ToString conversions. I've tested it against .Net 4.x and .Net 3.5 in Unity 2019.1. If there are any versions where this does fail, please let me know.

Edit:

Thanks to @Zastai for reminding me about the possibility to use an explicit struct layout 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 could strip away all fields besides Value and Scale but maybe it could be useful for someone to have them all.

Solution 11 - C#

As a decimal extension method that takes into account:

  • Different cultures
  • Whole numbers
  • Negative numbers
  • Trailing set zeros on the decimal place (e.g. 1.2300M will return 2 not 4)
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;
	}
}

Solution 12 - C#

I wrote a concise little method yesterday that also returns the number of decimal places without having to rely on any string splits or cultures which is ideal:

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;

Solution 13 - C#

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)

Solution 14 - C#

Using recursion you can do:

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

Solution 15 - C#

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

Solution 16 - C#

You can try:

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

Solution 17 - C#

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

Solution 18 - C#

I actually performance tested most of the solutions here. Some are fast but not reliable, some are reliable but not fast. With modification of @RooiWillie's answer, I get this which is fast enough and reliable:

public static int GetSignificantDecimalPlaces(decimal number)
{
    if (number % 1 == 0) return 0;
    var numstr = number.ToString(CultureInfo.InvariantCulture).TrimEnd('0');
    return numstr.Length - 1 - numstr.IndexOf('.');
}

> Note: It does not count trailing zeros.

xUnit tests:

[Theory]
[InlineData(0, 0)]
[InlineData(1.0, 0)]
[InlineData(100, 0)]
[InlineData(100.10, 1)]
[InlineData(100.05, 2)]
[InlineData(100.0200, 2)]
[InlineData(0.0000000001, 10)]
[InlineData(-52.12340, 4)]
public void GetSignificantDecimalPlaces(decimal number, int expected)
{
    var actual = GetSignificantDecimalPlaces(number);
    Assert.Equal(expected, actual);
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJesse CarterView Question on Stackoverflow
Solution 1 - C#burning_LEGIONView Answer on Stackoverflow
Solution 2 - C#G.YView Answer on Stackoverflow
Solution 3 - C#JoeView Answer on Stackoverflow
Solution 4 - C#Kristiyan DimitrovView Answer on Stackoverflow
Solution 5 - C#ClementView Answer on Stackoverflow
Solution 6 - C#fixagonView Answer on Stackoverflow
Solution 7 - C#ZastaiView Answer on Stackoverflow
Solution 8 - C#RitchieDView Answer on Stackoverflow
Solution 9 - C#RooiWillieView Answer on Stackoverflow
Solution 10 - C#Martin Tilo SchmitzView Answer on Stackoverflow
Solution 11 - C#bytedevView Answer on Stackoverflow
Solution 12 - C#Jesse CarterView Answer on Stackoverflow
Solution 13 - C#SrikanthView Answer on Stackoverflow
Solution 14 - C#MarsView Answer on Stackoverflow
Solution 15 - C#Eva ChangView Answer on Stackoverflow
Solution 16 - C#NicoRiffView Answer on Stackoverflow
Solution 17 - C#Veysel OzdemirView Answer on Stackoverflow
Solution 18 - C#oradView Answer on Stackoverflow