52

How would I manage to perform math.ceil such that a number is assigned to the next highest power of 10?

# 0.04  ->  0.1
# 0.7   ->  1
# 1.1   ->  10  
# 90    ->  100  
# ...

My current solution is a dictionary that checks the range of the input number, but it's hardcoded and I would prefer a one-liner solution. Maybe I am missing a simple mathematical trick or a corresponding numpy function here?

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
offeltoffel
  • 2,691
  • 2
  • 21
  • 35
  • 3
    @bold looks like those solutions work from `10` up, this'll need something with e.g. `log10`. – jonrsharpe Apr 03 '20 at 09:40
  • 3
    The word you want is "power". Maybe you got the wrong translation for your native language's word for it. – user2357112 Apr 03 '20 at 09:45
  • Thanks, Monica! @bold: I found this question, but it's a different problem. Jonrsharpe has provided a perfect answer – offeltoffel Apr 03 '20 at 09:46
  • 2
    This is also related to [order of magnitude](https://en.wikipedia.org/wiki/Order_of_magnitude). 1 is 0th order, 10 is 1st order, 100 is 2nd order, etc. – wjandrea Apr 03 '20 at 22:42

7 Answers7

70

You can use math.ceil with math.log10 to do this:

>>> 10 ** math.ceil(math.log10(0.04))
0.1
>>> 10 ** math.ceil(math.log10(0.7))
1
>>> 10 ** math.ceil(math.log10(1.1))
10
>>> 10 ** math.ceil(math.log10(90))
100

log10(n) gives you the solution x that satisfies 10 ** x == n, so if you round up x it gives you the exponent for the next highest power of 10.

Note that for a value n where x is already an integer, the "next highest power of 10" will be n:

>>> 10 ** math.ceil(math.log10(0.1))
0.1
>>> 10 ** math.ceil(math.log10(1))
1
>>> 10 ** math.ceil(math.log10(10))
10
jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
  • 1
    Working with the log function seems to be just the trick I couldn't come up with. I believe this is exactly what I was hoping for! Thanks a lot – offeltoffel Apr 03 '20 at 09:44
  • 2
    NB: depending on your desired behavior, this doesn't work for powers of 10, e.g. `10 ** math.ceil(math.log10(1)) == 1`, which isn't "the next highest power" – Cireo Apr 03 '20 at 18:24
  • @Cireo that's true; without knowing the use case it's hard to say whether that's the right behaviour or not, but I'll add it to the post. – jonrsharpe Apr 03 '20 at 18:26
  • Good point, Cireo. According to my question, the next power of ten for x = 1 is undefined, I'd assume. Anyway, in my application x = 1 is not a valid input, so I won't have to worry about it. – offeltoffel Apr 03 '20 at 19:26
  • 7
    Note: this answer relies on floating point arithmetic and as-such it can fail due to rounding errors. Try feeding in 1000000000000001 for example. – plugwash Apr 03 '20 at 20:24
  • 2
    @plugwash not necessarily, the math functions will also accept e.g. decimal.Decimals. – jonrsharpe Apr 03 '20 at 20:27
  • 6
    Yes you can pass other types, but they will be converted to a double precision floating point number and passed to the C "log10" function. There is a special case to prevent logs of large numbers overflowing, but nothing to prevent rounding errors. – plugwash Apr 03 '20 at 20:50
  • don't forget to check for zero before doing this, since log(0) is -infinity and will throw a ValueError. – Dave Kirby Apr 08 '20 at 06:28
  • @DaveKirby I didn't forget, exactly; a ValueError seems like the correct outcome for input <= 0. – jonrsharpe Apr 08 '20 at 06:33
  • @plugwash When working with positive integers you can simply do `10 ** len(str(x-1))` (with a special check for `x == 1`). – a_guest Apr 08 '20 at 10:28
25

Your problem is under-specified, you need to step back and ask some questions.

  • What type(s) are your inputs?
  • What type(s) do you want for your outputs?
  • For results less than 1, what exactly do you want to round to? Do you want actual powers of 10 or floating point approximations of powers of 10? You are aware that negative powers of 10 can't be expressed exactly in floating point right? Let's assume for now that you want floating point approximations of powers of 10.
  • If the input is exactly a power of 10 (or the closest floating point approximation of a power of 10), should the output be the same as the input? Or should it be the next power of 10 up? "10 -> 10" or "10 -> 100"? Let's assume the former for now.
  • Can your input values be any possible value of the types in question? or are they more constrained.

In another answer it was proposed to take the logarithm, then round up (ceiling function), then exponentiate.

def nextpow10(n):
    return 10 ** math.ceil(math.log10(n))

Unfortunately this suffers from rounding errors. First of all n is converted from whatever data type it happens to have into a double precision floating point number, potentially introducing rounding errors, then the logarithm is calculated potentially introducing more rounding errors both in its internal calculations and in its result.

As such it did not take me long to find an example where it gave an incorrect result.

>>> import math
>>> from numpy import nextafter
>>> n = 1
>>> while (10 ** math.ceil(math.log10(nextafter(n,math.inf)))) > n:
...     n *= 10
... 
>>> n
10
>>> nextafter(n,math.inf)
10.000000000000002
>>> 10 ** math.ceil(math.log10(10.000000000000002))
10

It is also theoretically possible for it to fail in the other direction, though this seems to be much harder to provoke.

So for a robust solution for floats and ints we need to assume that the value of our logarithm is only approximate, and we must therefore test a couple of possibilities. Something along the lines of

def nextpow10(n):
    p = round(math.log10(n))
    r = 10 ** p
    if r < n:
        r = 10 ** (p+1) 
    return r;

I believe this code should give correct results for all arguments in a sensible real-world range of magnitudes. It will break for very small or very large numbers of non integer and non-floating point types because of issues converting them to floating point. Python special cases integer arguments to the log10 function in an attempt to prevent overflow, but still with a sufficiently massive integer it may be possible to force incorrect results due to rounding errors.

To test the two implementations I used the following test program.

n = -323 # 10**-324 == 0
while n < 1000:
    v = 10 ** n
    if v != nextpow10(v): print(str(v)+" bad")
    try:
        v = min(nextafter(v,math.inf),v+1)
    except:
        v += 1
    if v > nextpow10(v): print(str(v)+" bad")
    n += 1

This finds lots of failures in the naive implementation, but none in the improved implementation.

plugwash
  • 9,724
  • 2
  • 38
  • 51
  • Thank you for your effort of going more into detail here. While jonrsharpe's answer has already solved my problem, this answer may be useful for others with similar but more particular questions. – offeltoffel Apr 05 '20 at 08:33
  • 1
    Why do you use `round` instead of `math.ceil`? This will introduce lots of unnecessary cases where `r < n` is true and so it needs to perform additional work. – a_guest Apr 08 '20 at 10:39
  • 1
    Because the log could be off in either direction. – plugwash Apr 08 '20 at 11:47
4

It seems you want rather the lowest next power of 10... Here is a way using pure maths and no log, but recursion.

def ceiling10(x):
    if (x > 10):
        return ceiling10(x / 10) * 10
    else:
        if (x <= 1):
            return ceiling10(10 * x) / 10
        else:
            return 10
for x in [1 / 1235, 0.5, 1, 3, 10, 125, 12345]:
    print(x, ceiling10(x))
plugwash
  • 9,724
  • 2
  • 38
  • 51
0

Check this out!

>>> i = 0.04123
>>> print i, 10 ** len(str(int(i))) if int(i) > 1 else 10 if i > 1.0 else 1 if i > 0.1 else 10 ** (1 - min([("%.100f" % i).replace('.', '').index(k) for k in [str(j) for j in xrange(1, 10) if str(j) in "%.100f" % i]]))               
0.04123 0.1
>>> i = 0.712
>>> print i, 10 ** len(str(int(i))) if int(i) > 1 else 10 if i > 1.0 else 1 if i > 0.1 else 10 ** (1 - min([("%.100f" % i).replace('.', '').index(k) for k in [str(j) for j in xrange(1, 10) if str(j) in "%.100f" % i]]))                 
0.712 1
>>> i = 1.1
>>> print i, 10 ** len(str(int(i))) if int(i) > 1 else 10 if i > 1.0 else 1 if i > 0.1 else 10 ** (1 - min([("%.100f" % i).replace('.', '').index(k) for k in [str(j) for j in xrange(1, 10) if str(j) in "%.100f" % i]]))                   
1.1 10
>>> i = 90
>>> print i, 10 ** len(str(int(i))) if int(i) > 1 else 10 if i > 1.0 else 1 if i > 0.1 else 10 ** (1 - min([("%.100f" % i).replace('.', '').index(k) for k in [str(j) for j in xrange(1, 10) if str(j) in "%.100f" % i]]))                    
90 100

This code based on principle of ten's power in len(str(int(float_number))).

There are 4 cases:

    1. int(i) > 1.

      Float number - converted to int, thereafter string str() from it, will give us a string with length which is we are looking exactly. So, first part, for input i > 1.0 - it is ten 10 in power of this length.

    1. & 3. Little branching: i > 1.0 and i > 0.1 <=> it is 10 and 1 respectively.
    1. And last case, when i < 0.1: Here, ten shall be in negative power. To get first non zero element after comma, I've used such construction ("%.100f" % i).replace('.', '').index(k), where k run over [1:10] interval. Thereafter, take minimum of result list. And decrease by one, it is first zero, which shall be counted. Also, here standard python's index() may crash, if it will not find at least one of non-zero element from [1:10] interval, that is why in the end I must "filter" listing by occurrence: if str(j) in "%.100f" % i. Additionally, to get deeper precise - %.100f may be taken differ.
jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
0

I think the simplest way is:

import math


number = int(input('Enter a number: '))
next_pow_ten = round(10 ** math.ceil(math.log10(number)))
print(str(10) + ' power ' + str(round(math.log10(number))) + ' = '\
      + str(next_pow_ten))

I hope this help you.

Vacendak
  • 11
  • 1
0

a specific shortcut works for big-integers that are already coming in as string-format :

instead of having to first convert it to integer, or running it through the log()/ceiling() function, or perform any sort of modulo math, the next largest power-of-10 is simply :

10 ** length(big_int_str_var)

—- below : 1st one generates a string formatted power-of-10, the 2nd one is numeric

echo 23958699683561808518065081866850688652086158016508618152865101851111111111111 | 
tee >( gpaste | gcat -n >&2; ) | gcat - |
 
python3 -c '\
import sys; [ print("1"+"0"*len(_.strip("\n"))) for _ in sys.stdin ]' 
  
  or   '... [ print(  10 ** len(_.strip("\n"))) for _ in sys.stdin ]' 
1 23958699683561808518065081866850688652086158016508618152865101851111111111111

1 100000000000000000000000000000000000000000000000000000000000000000000000000000
RARE Kpop Manifesto
  • 2,453
  • 3
  • 11
-1
y = math.ceil(x)
z = y + (10 - (y % 10))

Something like this maybe? It's just off the top of my head but it worked when I tried a few numbers in terminal.

Lugene
  • 1
  • This rounds to the next multiple of 10, but the question asks about rounding to the next power of ten. E.g. 42 should be rounded up to 100, and 1234 should be rounded up to 10000. – Marius Gedminas Jan 04 '21 at 21:16