Number of Digits in a Number

· mathtil

For reasons involving string-formatting and width adjustments in a project, I ran into the pickle of figuring out how to get the number of digits in a number.

This isn’t a serious problem. In theory I could have just used foobar.digits.size, be done with it, and save the hassle of writing this post. While premature optimization is said to be the root of all evil, this in particular felt more like a challenge to figure out how to get the number of digits in a number without going through a loop. Constant time rather than linear just to get this info is a bonus.

Since this project is written in Crystal, so is the code here.

Traditional Way

Like before, foobar.digits.size would be the easy way out. Int#digits just gives us an Array of digits in a number by looping, dividing and getting the remainder from 10 and stopping when the quotient reaches zero.

# Int#digits is part of the standard library in Crystal
# but this method illustrates the gist of this.
def digits(number : Int32)
  digits = Array(Int32).new

  while !number.zero?
    digits.push (number % 10).to_i
    number = number.tdiv 10
  end

  return digits
end

The amount of iterations in this kind of loop is never large. A UInt32’s maximum limit is ten digits and a UInt64’s, the largest fixed number type, twenty digits.

The rocx-y Road

For some reason this situation brought up “powers of ten” in my head. Specifically the part where, say, 103 means there’s three zeroes in 1,000. What’s the inverse of doing that? The logarithm. That looks like a good place to start.

Math.log10(2024)
# ⇒ 3.3062105081677613 : Float64

Math.log10(30)
# ⇒ 1.4771212547196624 : Float64

# Am I on the right track? 🤔 Let's try this...

Math.log10(2024).ceil.to_i
# ⇒ 4 : Int32

Math.log10(30)
# ⇒ 2 : Int32
# 😎

Nice. But there’s one snag I encountered while testing: dealing with powers of ten (or whatever the base used is).

Math.log10(1000).ceil.to_i
# ⇒ 3 : Int32
# 🫤

Well drat. Time to pull up a playground and tinker around. There has to be a pattern somewhere

log_999  = Math.log10  999
# ⇒ 2.9995654882259823 : Float64
log_1000 = Math.log10 1000
# ⇒ 3.0 : Float64
log_1001 = Math.log10 1001
# ⇒ 3.000434077479319 : Float64

log_999.ceil  # ⇒ 3.0 : Float64
log_1000.ceil # ⇒ 3.0 : Float64
log_1001.ceil # ⇒ 4.0 : Float64

Ooooooh I see now…

Aha! A Solution!

There’s the pattern. It looks like it’s a typical off-by-one error in my logic. What needs to be done is to get the logarithm of a given number plus one, turning log10(1000) into log10(1000 + 1).

def digit_count(number : Int32) : Int32
  return Math.log10(number.succ).ceil.to_i
end

digit_count 999  # ⇒ 3 : Int32
digit_count 1000 # ⇒ 4 : Int32
digit_count 1001 # ⇒ 4 : Int32

No loops. Just raw mathematics. You never think this kind of stuff comes up outside of a problem domain in computer science and yet it still comes in handy.