Get number of digits before decimal point

I have a variable of decimal type and I want to check the number of digits before decimal point in it. What should I do? For example, 467.45 should return 3.


Solution 1:

Solution without converting to string (which can be dangerous in case of exotic cultures):

static int GetNumberOfDigits(decimal d)
{
    decimal abs = Math.Abs(d);

    return abs < 1 ? 0 : (int)(Math.Log10(decimal.ToDouble(abs)) + 1);
}

Note, that this solution is valid for all decimal values

UPDATE

In fact this solution does not work with some big values, for example: 999999999999998, 999999999999999, 9999999999999939...

Obviously, the mathematical operations with double are not accurate enough for this task.

While searching wrong values I tend to use string-based alternatives proposed in this topic. As for me, that is the evidence that they are more reliable and easy-to-use (but be aware of cultures). Loop-based solutions can be faster though.

Thanks to commentators, shame on me, lesson to you.

Solution 2:

Instead of converting to string, you can also divide the number by 10 until it equals 0. Interesting is, that the mathematical operations on decimals are much slower than converting the decimal to a string and returning the length (see benchmarks below).
This solution does not use the Math-methods that take a double as input; so all operations are done on decimals and no casting is involved.

using System;

public class Test
{
    public static void Main()
    {
        decimal dec = -12345678912345678912345678912.456m;
        int digits = GetDigits(dec);
        Console.WriteLine(digits.ToString());
    }

    static int GetDigits(decimal dec)
    {
        decimal d = decimal.Floor(dec < 0 ? decimal.Negate(dec) : dec);
        // As stated in the comments of the question, 
        // 0.xyz should return 0, therefore a special case
        if (d == 0m)
            return 0;
        int cnt = 1;
        while ((d = decimal.Floor(d / 10m)) != 0m)
            cnt++;
        return cnt;
    }
}

Output is 29. To run this sample, visit this link.


Side note: some benchmarks show surprising results (10k runs):

  • while ((d = decimal.Floor(d / 10m)) != 0m): 25ms
  • while ((d = d / 10m) > 1m): 32ms
  • ToString with Math-double-operations: 3ms
  • ToString with decimal-operations: 3ms
  • BigInt (see answer of @Heinzi): 2ms

Also using random numbers instead of always the same value (to avoid possible caching of the decimal to string conversion) showed that the string-based methods are much faster.

Solution 3:

I would try this:

Math.Truncate(467.45).ToString().Length

If you want to be sure not having some weird results for different cultures and with negative decimals, you better use this:

var myDecimal = 467.45m;
Math.Truncate(Math.Abs(myDecimal)).ToString(CultureInfo.InvariantCulture).Length

Solution 4:

I would prefer the following instead of casting to int to ensure that you can also handle big numbers (e.g. decimal.MaxValue):

Math.Truncate ( Math.Abs ( decValue ) ).ToString( "####" ).Length

Solution 5:

decimal d = 467.45M;
int i = (int)d;
Console.WriteLine(i.ToString(CultureInfo.InvariantCulture).Length); //3

As a method;

public static int GetDigitsLength(decimal d)
{
  int i = int(d);
  return i.ToString(CultureInfo.InvariantCulture).Length;
}

Note: Of course you should check first your decimals full part is bigger than Int32.MaxValue or not. Because if it is, you get an OverflowException.

Is such a case, using long instead of int can a better approach. However even a long (System.Int64) is not big enough to hold every possible decimal value.

As Rawling mentioned, your full part can hold the thousands separator and my code will be broken in such a case. Because in this way, it totally ignores my number contains NumberFormatInfo.NumberGroupSeparator or not.

That's why getting numbers only is a better approach. Like;

i.ToString().Where(c => Char.IsDigit(c)).ToArray()