Get number of digits before decimal point

29,911

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()
Share:
29,911
fasadat
Author by

fasadat

Updated on July 05, 2022

Comments

  • fasadat
    fasadat almost 2 years

    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.

  • Nahum
    Nahum over 10 years
    this answer is just wrong because decimal can be WAYYY bigger than int
  • Selman Genç
    Selman Genç over 10 years
    or you could use long :)
  • Kevin Brechbühl
    Kevin Brechbühl over 10 years
    You are right thanks. It's better to use Math.Truncate. I've updated my answer.
  • hsun324
    hsun324 over 10 years
    The Log functions in the Math class only handle doubles.
  • Stephan Bauer
    Stephan Bauer over 10 years
    I think that's the best solution so far. But I would add Math.Abs to handle negative values as well. And maybe check whether dis 0)
  • fasadat
    fasadat over 10 years
    if the value is larger than max value of double, it again raise exception!
  • fasadat
    fasadat over 10 years
    @Selman22 decimal d = 99999999999999999.7m; double i = ((double)d).ToString().Length; it return wrong result!
  • Selman Genç
    Selman Genç over 10 years
    @fasadat check my answer then.convert it to string and don't worry about value range!
  • astef
    astef over 10 years
    Oops, forgot about zero )
  • Fedor
    Fedor over 10 years
    For anyone who is going to call this method million times and concerned about performance: even with all this calls to Math methods, it's still at least 2 times faster than "truncate" solution, however I would prefer Stephan's answer briefness and clarity
  • Stephan Bauer
    Stephan Bauer over 10 years
    @Selman22 Maybe you could please tell us how truncating could help in the code astef provided instead of acting childish?!
  • Fedor
    Fedor over 10 years
    @Selman22 I've measured it in comparison with Stephan's solution, it's quite precisely, I know what I'm talking about.
  • Taemyr
    Taemyr over 10 years
    I think it might be better to test if absolute value is less than 1, rather than just for zero. It's an edge case, but one might not want 0.0001 to return as -4 digits before the decimal point.
  • Dubu
    Dubu over 10 years
    @astef You should also include Maths.Abs() in the test: return Math.Abs(d) < 10 ? 1 : ...
  • Micros
    Micros over 10 years
    This solution will count the minus sign for negative numbers. That's why @stephan-bauer 's answer is better.
  • Luaan
    Luaan over 10 years
    You should also use ToString(CultureInfo.InvariantCulture), or you can get some pretty crazy results.
  • usr
    usr over 10 years
    How can you be so sure that this solution will work for all possible values? Even ones that are huge or that are a tiny fraction above or below a power of ten?
  • CodesInChaos
    CodesInChaos over 10 years
    "Note, that this solution is valid for all decimal values" --- wrong since the conversion to double is lossy. For example GetNumberOfDigits(999999999999999) should return 15 but returns 16 with your code (at least on my computer, doubles aren't consistent across computers).
  • CodesInChaos
    CodesInChaos over 10 years
    @ThomasW. Those don't fit into a decimal in the first place.
  • CodesInChaos
    CodesInChaos over 10 years
    Even long is not big enough to represent all decimals.
  • CodesInChaos
    CodesInChaos over 10 years
    wrong since the conversion to double is lossy. For example GetNumberOfDigits(999999999999999) should return 15 but returns 16 with your code (at least on my computer, doubles aren't consistent across computers).
  • Soner Gönül
    Soner Gönül over 10 years
    @CodesInChaos Exactly. I think there is no healthy conversation for this case. astef's way is the closest right answer so far (but not exactly right)..
  • Thomas Weller
    Thomas Weller over 10 years
    @CodesInChaos: Of course that true. I apologize for not reading the question and tags clearly.
  • CodesInChaos
    CodesInChaos over 10 years
    Astef's solution is one of the hopeless approaches. Nothing that starts with converting to int, long or double will work. Some of the string based answers work, but they should use InvariantCulture, and the division based answers work as well.
  • Soner Gönül
    Soner Gönül over 10 years
    @CodesInChaos On which case my answer doesn't work? It is because only I'm using long? Yeah it can't hold every possible decimal values. I'm aware of that but is there also another reason? What do you think?
  • Jeppe Stig Nielsen
    Jeppe Stig Nielsen over 10 years
    @CodesInChaos Certainly correct. Since a double has a precision of approx. 15 to 17 significant decimal digits, something like 9999999999999999993728621.07m will have to be rounded (up or down, depending on where the nearest multiple of the relevant power of two is, but in some (~50%) cases it will surely matter).
  • TecBrat
    TecBrat over 10 years
    I think I like the answer by @Vinay Wadhwa better than my own. :-) (unless casting to int causes problems with large numbers)
  • Jack Aidley
    Jack Aidley over 10 years
    You could probably do this faster by comparing to 1 rather than doing a floor each time.
  • Brian Moths
    Brian Moths over 10 years
    Also you can have a test value that you multiply by ten instead of doing a division.
  • Markus
    Markus over 10 years
    @JackAidley: thanks for your input - I did a short "benchmark" with both solutions (Floor vs. > 1m). I'm surprised that Floor is faster (10k runs, 25ms for Floor, 32ms for > 1m).
  • Jack Aidley
    Jack Aidley over 10 years
    Fair enough. I wonder whether that is because of the reducing complexity of the decimal each pass?
  • Stephan
    Stephan over 10 years
    With value like "0.1", this solution won't work with OP comment, it will return 1 when the OP want the result to be 0.
  • Fedor
    Fedor over 10 years
    float number is not decimal. Decimal type in .NET is significantly larger than int, see other similiar answers and comments to them
  • Fedor
    Fedor over 10 years
    Did you read this answer before posting yours?
  • aradil
    aradil over 10 years
    Does the 4 #s here have any significance? In the documentation, 5 appears to be used for showing all numbers and 2 #s says it will round the least significant digit (although when I tested it in LINQPad, it didn't do that).
  • Fedor
    Fedor over 10 years
    Good solution, although TC stated that value like 0.456m should be treated as zero-digits, while your solution returns 1 for it.
  • Fedor
    Fedor over 10 years
    Little performance note: this solution is about 2 times slower than the original Stephan's solution which uses ToString()
  • Sebastien H.
    Sebastien H. over 10 years
    then add an if statement on "value.split('.')[0]" and check it's value if it's equals to 0..
  • Phil Perry
    Phil Perry over 10 years
    So long as Log10() is well-behaved for your data type (that it can handle any decimal number thrown at it), @Kubuxu's formula is the correct way. No need for snide remarks or cultural dependencies on how a number gets formatted. If Log10() can't handle a range of decimal numbers, that would be a problem. Other posted solutions involving repeated division by 10 are doing the same thing, in a roundabout (and slower) manner.
  • CodesInChaos
    CodesInChaos over 10 years
    @PhilPerry As I posted on other, equivalent answers, Log10 only works on double, not decimal. There are values decimal can represent, but where this function gives the wrong result.
  • Stephan Bauer
    Stephan Bauer over 10 years
    I just wanted to avoid thousand separators. But it's a quick'n'dirty and not absolutely reliable solution. I'm sure there are way better approaches ;-)
  • John Odom
    John Odom over 10 years
    I was going to add to my answer a recursive version but it seems like you already have one. +1.
  • user505255
    user505255 over 10 years
    +1, this is exactly how I would have done it. I suppose it's not the most efficient way, though.
  • sidgeon smythe
    sidgeon smythe over 10 years
    I don't know C#, but I'd guess you don't need the decimal.Floor in the first line of your function, to take absolute value (that is, you can just write decimal d = dec < 0 ? decimal.Negate(dec) : dec;, without wrapping the RHS in a decimal.Floor). Also, I don't think you need the repeated Floor either: try making your loop just while (d >= 10) { cnt++; d = d / 10; }. How would this compare against your benchmarks?
  • sidgeon smythe
    sidgeon smythe over 10 years
    Ah now I reread the earlier comments and see why you have the decimal.Floor in the first line... but the simpler loop is worth trying out.
  • Fedor
    Fedor over 10 years
    What if delimeter will be not . but ,?
  • Khaled.K
    Khaled.K over 10 years
    @Fyodor the decimal point is ., , is not a decimal point but can appear when you try decimal.ToString("C"); which is currency representation
  • Fedor
    Fedor over 10 years
    Don't forget about other cultures :) I'm saying that decimal delimeter is depends on culture under which thread is running. It's not always ., so just ToString() can return "3,14" instead of "3.14"
  • Sebastien H.
    Sebastien H. over 10 years
    i know it's not the best solution, but i will spare you lines of math code, and might be even more performant
  • Gray
    Gray over 10 years
    @user505255 It is indeed likely to be less efficient than doing the same operation with a while loop, what with the overhead involved with recursion... It is a pretty clean example that lends itself to recursion, though. In my opinion it is probably more readable than the accepted answer, but I imagine the easiest one for developers to understand at a glance would simply be a toString()-based solution... but I suppose that has its own problems.
  • Mr Lister
    Mr Lister over 10 years
    So, 0 has 0 digits? That's interesting.
  • Gray
    Gray over 10 years
    @MrLister The OP actually says he thinks that is the correct answer (see his comments on the question itself). We could modify the function to not do that pretty easily though. It let's you have a distinction between .54 and 1.54 as far as number of digits goes. Otherwise, you'd treat them the same.
  • Mr Lister
    Mr Lister over 10 years
    I read the OP's remark as .1 having no digits before the decimal point, but maybe he was including 0. Oh well.
  • Gray
    Gray over 10 years
    OP wasn't exactly explicit - I'll give you that. But it would be very frustrating to me if a program returned a different result for .1 and 0.1 since they are equal. It all depends on what this would actually be used for. If it was to teach kids about decimals, you may have a different answer than if you were writing a program for a satellite that accounts for special relativity.
  • user207421
    user207421 over 10 years
    This doesn't work correctly for 0 < n < 1. The conditional is actually unnecessary. The part after the : is all you need. Math.Floor() is also redundant. Zero can be handled as a special case. Values > 999999999999997 won't work correctly.
  • astef
    astef over 10 years
    @EJP Math.Floor and explicit cast to int are not the same for negative values (Log10(x) + 1 is negative for very small x).
  • astef
    astef over 10 years
    @EJP About 0..1 values - you're right, answer need to be updated due to OP's new comments. And still some of the big parameters lead to a wrong results as it stated in an update text
  • Vinay W
    Vinay W over 10 years
    thats why it is the JAVA solution.
  • Fedor
    Fedor over 10 years
    Yeah, but OP explicitly specified decimal tag in question. Furthermore, as far as I know, where are some kind of Decimal type in java too.
  • CodesInChaos
    CodesInChaos about 10 years
    Breaks on cultures with a different decimal separator. Either use InvariantCulture to convert to string (preferred), or query the current culture for the decimal separator.
  • CodesInChaos
    CodesInChaos about 10 years
    doesn't support negative inputs
  • Khaled.K
    Khaled.K about 10 years
    is it normal to get random (-1)s without any comment ?
  • Beytan Kurt
    Beytan Kurt almost 5 years
    Answer should be static int GetNumberOfDigits(decimal d) { decimal abs = Math.Abs(d); return abs < 1 ? 0 : (int)(Math.Log10(decimal.ToDouble(abs)) + 1); } since GetNumberOfDigits(0.5m) should return 1 instead of 0.
  • c-sharp-and-swiftui-devni
    c-sharp-and-swiftui-devni over 4 years
    This does not work these only gets the total length, not before the decimal point, which was the Op's original request.
  • c-sharp-and-swiftui-devni
    c-sharp-and-swiftui-devni over 4 years
    Perfect this worked one of the others here didn't this was a simple and nice extension method thanks