In this article, we examine how to count the digits in a number, using C#. Finding the number of digits of an integer is a common problem in informatics. For instance, it’s useful when we need to convert numbers from one numerical system to another, or when we want to understand the order of magnitude of a value.

Throughout the article, we will explore different methods of digit counting, and then we’ll compare their performance, using a benchmark.

To download the source code for this article, you can visit our GitHub repository.

Methods for Counting the Number of Digits in a Number

Usually, when we count digits in a number, we use a series of well-known methods and algorithms:

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!
  • Iterative algorithm
  • Recursive algorithm
  • Using logarithm
  • Converting the number into a string
  • If-chain
  • Bit manipulation
  • Fast count

Let’s see each of these methods in depth.

Iterative Algorithm

We can easily discover the number of digits in a number with an iterative algorithm:

public int GetIterativeCount(int number)
{
    if (number == 0) return 1;

    int count = 0;
    while (number != 0)
    {
        number /= 10;
        count++;
    }

    return count;
}

When we divide an integer by 10, we obtain another integer with one digit less than the original number. For instance, let’s say that number is 123. After the first division, number is 12 and count is 1.

On the second iteration, number / 10 equals 1, and count is incremented to 2.

On the third iteration, number / 10 equals 0, and count becomes 3. At this point, there’s no further iteration, because number is now 0 and the while loop ends.

When the input number is precisely 0, the while loop ends immediately and count is never incremented. So, at the very beginning of the function, we have to test if the input number is 0, and in this case, we return 1.

This function works with both positive and negative integers.

Count the Number of Digits Using Recursion

The recursive algorithm is shorter and more compact, as often happens when using recursion:

public int GetRecursiveCount(int number)
{
    if (Math.Abs(number) < 10) 
    {
        return 1;
    }

    return 1 + GetRecursiveCount(number / 10);
}

We can see that the terminal condition is when the argument is less than 10. In this case, the function returns 1 and the control goes back to the previous call, if any. On the other hand, if the number is greater than 10, the procedure calls itself passing the number divided by 10, and adding 1 to the result.

With this algorithm, we don’t need to manage the case when the number is zero, because zero is less than 10, and the function would correctly return 1. Instead, we have to pay attention to negative numbers: in fact, every negative number is less than 10, so the function would return 1 for all of them! To avoid this error, we test the terminal condition by putting the argument inside Math.Abs(). This makes the function valid with negative numbers, too.

Using Logarithm

The logarithm of a number to a base is the value to which we have to exponentiate the base to obtain the number. For more information about logarithms and their properties, please refer to this Wikipedia article.

From the previous logarithm definition, it follows that Log10(10) = 1, because 101 = 10. In the same way Log10(100) = 2, because 102 = 100, Log10(1000) = 3, and so on. From this pattern, we can see that Log10 of a number is equal to the number of digits minus 1. Using this information, we can quickly compute the digits of a number as 1 + the Log10 of the number.

Because the result of Log10 is computed as a double including the fractional part of the exponent, we need to take the floor of the result:

public int GetLog10Count(int number)
{
    if (number == 0) return 1;

    return 1 + (int)Math.Floor(Math.Log10(Math.Abs(number)));
}

As the logarithm is only defined for numbers strictly greater than zero, we have to manage when the argument is zero or less than zero. If the argument is zero, we immediately return 1, because 0 has one digit. To deal with negative numbers, we use Math.Abs().

Converting the Number Into a String

To count the number of digits of a number, we can convert the number to a string and then get its length:

public int GetStringLengthCount(int number)
{
    return Math.Abs(number).ToString().Length;
}

This solution is quite simple. We still have the negative number problem, but once again we easily manage this with Math.Abs().

Count the Number of Digits Using an If-Chain

This solution is quite naive and kind of brute-force:

public int GetIfChainCount(int number)
{
    number = Math.Abs(number);
    if (number < 10) return 1;
    if (number < 100) return 2;
    if (number < 1_000) return 3;
    if (number < 10_000) return 4;
    if (number < 100_000) return 5;
    if (number < 1_000_000) return 6;
    if (number < 10_000_000) return 7;
    if (number < 100_000_000) return 8;
    if (number < 1_000_000_000) return 9;

    return 10;
}

If the number is less than 10, it has one digit for sure and the function returns 1. If the number is greater than 10, the method goes on and checks if the number is less than 100. In this case, it returns 2, otherwise, it continues comparing the number with 1000, 10000, and so on. To manage negative numbers, we use Math.Abs().

The limit is 10 digits because we are working with Int32 numbers, whose range is from -2,147,483,648 to 2,147,483,647. To know more about integer data types in .NET, please take a look at this article on Microsoft Learn.

This function is not scalable with the data type: if we switch from Int32 to Int64, we have to add additional if conditions to support up to 19 digits. On the other hand, for all the other solutions, changing the argument type to long would be enough.

Bit Manipulation

The next approach gets the leading zeroes of the number in binary form and uses this value to guess the number of digits from an array:

private static readonly int[] GuessDigits = 
    [9, 9, 9, 8, 8, 8, 7, 7, 7, 6, 6, 6, 6, 5, 5, 5, 4, 4, 4, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0];
private static readonly int[] PowersOf10 = 
    [1, 10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000, 100_000_000, 1_000_000_000];

public static int GetBitManipulationCount(int number)
{
    if (number == 0)
    {
        return 1;
    }

    number = Math.Abs(number);
    var leading = BitOperations.LeadingZeroCount((uint)number);
    var digits = GuessDigits[leading];
    if (number >= PowersOf10[digits])
    {
        ++digits;
    }

    return digits;
}

The function BitOperations.LeadingZeroCount() counts the number of leading zeroes of the binary version of its argument. It directly maps a CPU’s intrinsic functions, so it’s very efficient. For more information about it, see this post on Microsoft Learn. We can check out the function implementation here. It’s important to point out that this function is only available from .NET Core 3.0.

Let’s consider the number 12345, stored as an Int32. Its binary representation is 0000_0000_0000_0000_0011_0000_0011_1001, and the number of leading zeroes is 18.

Once we know the number of leading zeroes, we can work out the number of digits. The array GuessDigits is built to map the number of leading zeros of the binary representation with the number of digits of the decimal representation. From GuessDigits[18] we get 4, which isn’t actually the correct number of digits.

If we calculate PowersOf10[4], we obtain 10000. As our number 12345 is greater than 10000, we have to add 1 to the number of digits previously calculated. In the end, we get 5, which is the correct number of digits we were looking for.

The reason for this adjustment is that we can have numbers with both 4 and 5 digits that, in the binary form, have 18 leading zeroes. In fact, the numbers from 8192 (213) to 16383 (214 – 1) have 18 leading zeroes.

This algorithm is very efficient and more easily scalable than the If-Chain one. If we want to scale the algorithm to Int64, we just have to extend the arrays GuessDigits and PowersOf10, and change the type of the argument number from int to long and the type of PowersOf10 from int[] to long[].

Count the Number of Digits Using Fast Count

The next algorithm was proposed by Kendall Willets and appears in this article by Daniel Lemire. The following implementation comes from an internal .NET class:

public static int GetFastCount(int number)
{
    uint value = (uint)Math.Abs(number);

    ReadOnlySpan<long> table =
    [
        4_294_967_296,
        8_589_934_582,
        // Removed for brevity, see our GitHub repo for full code
        42_949_672_960,
    ];

    long tableValue = Unsafe.Add(ref MemoryMarshal.GetReference(table), uint.Log2(value));

    return (int)((value + tableValue) >> 32);
}

The function uses a table-mapped way to calculate the number of digits. The values in the table come from a complex calculation shown in Lemire’s implementation of the algorithm.

Now, let’s compare our solutions using a benchmark.

Benchmarks

In the previous paragraphs, we analyzed different functions that count the digits within a number. So, let’s benchmark them and see how they perform with a selection of numbers of varying lengths.

To measure the performance of the different functions, we use the BenchmarkDotNet library (we can learn more about BenchmarkDotNet by reading our article Introduction to Benchmarking in C# and ASP.NET Core Projects):

[Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)]
public class DigitCounterBenchmark
{
    [Params(-1234567890, -12345, 0, 12345, 1234567890)]
    public int N;

    [Benchmark]
    public int Iterative() => DigitCounter.GetIterativeCount(N);

    [Benchmark]
    public int Recursive() => DigitCounter.GetRecursiveCount(N);

    [Benchmark]
    public int Log10() => DigitCounter.GetLog10Count(N);

    [Benchmark]
    public int String() => DigitCounter.GetStringLengthCount(N);

    [Benchmark]
    public int IfChain() => DigitCounter.GetIfChainCount(N);

    [Benchmark]
    public int BitManipulation() => DigitCounter.GetBitManipulationCount(N);

    [Benchmark]
    public int Fast() => DigitCounter.GetFastCount(N);
}

The functions we want to compare are called by some methods marked with the [Benchmark] attribute. The variable N has the Params attribute, which lists all the values that the benchmark will pass to our functions.

Comparison Between the Different Methods

Let’s run the benchmark and examine the result:

| Method          | N           | Mean       | Error     | StdDev    |
|---------------- |------------ |-----------:|----------:|----------:|
| Log10           | 0           |  0.6764 ns | 0.0101 ns | 0.0090 ns |
| Iterative       | 0           |  0.7023 ns | 0.0499 ns | 0.0442 ns |
| IfChain         | 0           |  1.0113 ns | 0.0050 ns | 0.0041 ns |
| BitManipulation | 0           |  1.0124 ns | 0.0134 ns | 0.0104 ns |
| Recursive       | 0           |  1.0196 ns | 0.0093 ns | 0.0083 ns |
| Fast            | 12345       |  1.0200 ns | 0.0080 ns | 0.0075 ns |
| Fast            | 1234567890  |  1.0233 ns | 0.0113 ns | 0.0106 ns |
| Fast            | 0           |  1.0234 ns | 0.0147 ns | 0.0138 ns |
| Fast            | -1234567890 |  1.0268 ns | 0.0125 ns | 0.0111 ns |
| Fast            | -12345      |  1.0328 ns | 0.0356 ns | 0.0278 ns |
| BitManipulation | 12345       |  1.3539 ns | 0.0069 ns | 0.0061 ns |
| BitManipulation | 1234567890  |  1.3636 ns | 0.0101 ns | 0.0095 ns |
| IfChain         | 12345       |  1.3661 ns | 0.0112 ns | 0.0105 ns |
| BitManipulation | -1234567890 |  1.3661 ns | 0.0159 ns | 0.0141 ns |
| BitManipulation | -12345      |  1.3684 ns | 0.0144 ns | 0.0127 ns |
| IfChain         | -12345      |  1.3740 ns | 0.0202 ns | 0.0198 ns |
| IfChain         | -1234567890 |  2.0613 ns | 0.0259 ns | 0.0242 ns |
| IfChain         | 1234567890  |  2.1672 ns | 0.0157 ns | 0.0147 ns |
| String          | 0           |  2.7083 ns | 0.0186 ns | 0.0165 ns |
| Iterative       | -12345      |  5.2774 ns | 0.0414 ns | 0.0367 ns |
| Iterative       | 12345       |  5.2782 ns | 0.0382 ns | 0.0357 ns |
| Recursive       | 12345       |  7.3150 ns | 0.0320 ns | 0.0283 ns |
| Log10           | 1234567890  |  8.9425 ns | 0.0346 ns | 0.0307 ns |
| Recursive       | -12345      |  8.9541 ns | 0.0455 ns | 0.0404 ns |
| Log10           | 12345       |  9.0266 ns | 0.0406 ns | 0.0380 ns |
| Log10           | -1234567890 |  9.1752 ns | 0.0548 ns | 0.0457 ns |
| Log10           | -12345      |  9.1851 ns | 0.0882 ns | 0.0737 ns |
| String          | 12345       | 12.3657 ns | 0.0386 ns | 0.0342 ns |
| String          | -12345      | 12.7260 ns | 0.2906 ns | 0.3346 ns |
| Iterative       | 1234567890  | 13.4394 ns | 0.0471 ns | 0.0441 ns |
| Iterative       | -1234567890 | 13.7844 ns | 0.3157 ns | 0.2636 ns |
| String          | 1234567890  | 16.8417 ns | 0.0802 ns | 0.0750 ns |
| String          | -1234567890 | 17.1225 ns | 0.0688 ns | 0.0537 ns |
| Recursive       | 1234567890  | 19.5895 ns | 0.0593 ns | 0.0495 ns |
| Recursive       | -1234567890 | 22.0791 ns | 0.1020 ns | 0.0904 ns |

We can see that the absolute winner is Log10 for N = 0. That’s because, in this algorithm, when the argument is 0, we immediately return 1, without doing any further calculation. Zero is also a special case for Iterative, Recursive and BitManipulation.

Immediately after the zero cases, we find out that Fast is the fastest method, followed by BitManipulation and IfChain. At a distance, we have Iterative, Log10, String, and Recursive.

We can see that the fastest methods are the ones that map the input to the solution using a table (Fast and BitManipulation) or via code (IfChain). For algorithms calculating digit count thousands of times, these functions are optimal. In all the other situations, Log10 and Iterative may be a good fit. String along with being slower, also requires additional memory allocation in the form of a string and so is not an optimal choice.

With our benchmarks in hand, let’s examine how these methods scale.

How Methods Run Times Change With the Number of Digits

When discussing algorithms and their performance, it is always good to try to understand how they scale. In Computer Science terms it is good to have an idea of their worst-case complexity (often referred to as Big-O or Big-O notation). The ideal function is constant time (O(1)), but an algorithm that has a linear complexity (O(N)) is also a good candidate (in the absence of an O(1)).

It is important to notice that these are relative comparisons and so while a constant time algorithm (O(1)) may not be the fastest, it has the advantage of being consistent. Whether we have a small or a large input, its performance is generally unaffected. For example, our Log10-based algorithm is constant time. Regardless of the size of the input, the performance is consistent. To know more about Big-O notation, take a look at this Wikipedia article.

From our benchmark results, we can see that the Iterative approach has a linear performance scaling (O(N)):

| Method    | N     | Mean       | Error     | StdDev    |
|---------- |------ |-----------:|----------:|----------:|
| Iterative | 1     |  1.0013 ns | 0.0259 ns | 0.0230 ns |
| Iterative | 12    |  2.2130 ns | 0.0884 ns | 0.1960 ns |
| Iterative | 123   |  3.0586 ns | 0.0647 ns | 0.0605 ns |
| Iterative | 1234  |  4.2291 ns | 0.1261 ns | 0.1768 ns |
| Iterative | 12345 |  5.2694 ns | 0.0534 ns | 0.0499 ns |

Recursive‘s running time is fast for the 1-digit case (which is the terminal condition of the recursion). In the other cases, it changes in an almost linear way, but the growth rate is faster than Iterative:

| Method    | N     | Mean       | Error     | StdDev    |
|---------- |------ |-----------:|----------:|----------:|
| Recursive | 1     |  0.6717 ns | 0.0072 ns | 0.0060 ns |
| Recursive | 12    |  1.7166 ns | 0.0605 ns | 0.0537 ns |
| Recursive | 123   |  3.7313 ns | 0.0997 ns | 0.0933 ns |
| Recursive | 1234  |  5.1854 ns | 0.1434 ns | 0.1535 ns |
| Recursive | 12345 |  7.3216 ns | 0.0432 ns | 0.0383 ns |

Log10‘s running time is nearly constant:

| Method    | N     | Mean       | Error     | StdDev    |
|---------- |------ |-----------:|----------:|----------:|
| Log10     | 1     | 10.9773 ns | 0.0410 ns | 0.0383 ns |
| Log10     | 12    |  9.6166 ns | 0.2069 ns | 0.3097 ns |
| Log10     | 123   |  9.3436 ns | 0.1533 ns | 0.1434 ns |
| Log10     | 1234  |  9.9891 ns | 0.2384 ns | 0.3419 ns |
| Log10     | 12345 |  9.0507 ns | 0.0346 ns | 0.0324 ns |

String changes almost linearly, but there’s a jump after 3-digits. This is because, up to the value 300, .NET CLR uses a cache to convert numbers to strings (see this function, which is called when we convert a positive Int32 number to a string):

| Method    | N     | Mean       | Error     | StdDev    |
|---------- |------ |-----------:|----------:|----------:|
| String    | 1     |  2.7018 ns | 0.0153 ns | 0.0135 ns |
| String    | 12    |  2.8126 ns | 0.0670 ns | 0.0559 ns |
| String    | 123   |  2.7609 ns | 0.0524 ns | 0.0464 ns |
| String    | 1234  | 12.5010 ns | 0.2763 ns | 0.2307 ns |
| String    | 12345 | 12.3008 ns | 0.0352 ns | 0.0312 ns |

IfChain also increases linearly its runtime with the number of digits. This makes sense because for each additional digit, we have one more condition to check:

| Method    | N     | Mean       | Error     | StdDev    |
|---------- |------ |-----------:|----------:|----------:|
| IfChain   | 1     |  1.0114 ns | 0.0060 ns | 0.0056 ns |
| IfChain   | 12    |  1.0341 ns | 0.0196 ns | 0.0184 ns |
| IfChain   | 123   |  1.1291 ns | 0.0401 ns | 0.0375 ns |
| IfChain   | 1234  |  1.0231 ns | 0.0098 ns | 0.0076 ns |
| IfChain   | 12345 |  1.3565 ns | 0.0085 ns | 0.0080 ns |

BitManipulation is constant because the array lookup operation is O(1):

| Method          | N     | Mean       | Error     | StdDev    |
|---------------- |------ |-----------:|----------:|----------:|
| BitManipulation | 1     |  1.3625 ns | 0.0151 ns | 0.0134 ns |
| BitManipulation | 12    |  1.3988 ns | 0.0341 ns | 0.0319 ns |
| BitManipulation | 123   |  1.3690 ns | 0.0128 ns | 0.0120 ns |
| BitManipulation | 1234  |  1.3600 ns | 0.0064 ns | 0.0057 ns |
| BitManipulation | 12345 |  1.4143 ns | 0.0680 ns | 0.0728 ns |

Finally, Fast is also constant, for the same reason as BitManipulation:

| Method    | N     | Mean       | Error     | StdDev    |
|---------- |------ |-----------:|----------:|----------:|
| Fast      | 1     |  1.0208 ns | 0.0087 ns | 0.0073 ns |
| Fast      | 12    |  1.0496 ns | 0.0265 ns | 0.0248 ns |
| Fast      | 123   |  1.0252 ns | 0.0127 ns | 0.0113 ns |
| Fast      | 1234  |  1.0153 ns | 0.0086 ns | 0.0080 ns |
| Fast      | 12345 |  1.0362 ns | 0.0235 ns | 0.0208 ns |

In the previous analysis, we noticed that Iterative, Recursive, String, and IfChain have a linear runtime, whereas Log10, BitManipulation, and Fast have a constant runtime. The best-performing function is the Fast method. The fastest growth belongs to Recursive.

Conclusion

In this article, we have shown different ways to count the number of digits in a number. We explained each of them, examining the edge cases. In the end, we have used a benchmark to compare them and understand which is the best performing one.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!