Steven Hepting
Steven Hepting

Reputation: 12504

Percentage with variable precision

I would like to display a percentage with three decimal places unless it's greater than 99%. Then, I'd like to display the number with all the available nines plus 3 non-nine characters.

How can I write this in Python? The "%.8f" string formatting works decently, but I need to keep the last three characters after the last string of nines.

So:
54.8213% -> 54.821%
95.42332% -> 95.423%
99.9932983% -> 99.99330%
99.99999999992318 -> 99.9999999999232%

Upvotes: 0

Views: 840

Answers (6)

steveha
steveha

Reputation: 76715

Mark Ransom's answer is a beautiful thing. With a little bit of work, it can solve the problem for any inputs. I went ahead and did the little bit of work.

You just need to add some code to nines():

def nines(x):
    x = abs(x)  # avoid exception caused if x is negative
    x -= int(x)  # keep fractional part of x only
    cx = ceilpowerof10(x) - x
    if 0 == cx:
        return 0  # if x is a power of 10, it doesn't have a string of 9's!
    return -int(math.log10(cx))

Then threeplaces() works for anything. Here are a few test cases:

>>> threeplaces(0.9999357)
'0.9999357'
>>> threeplaces(1000.9999357)
'1000.9999357'
>>> threeplaces(-1000.9999357)
'-1000.9999357'
>>> threeplaces(0.9900357)
'0.99004'
>>> threeplaces(1000.9900357)
'1000.99004'
>>> threeplaces(-1000.9900357)
'-1000.99004'

Upvotes: 2

DigitalRoss
DigitalRoss

Reputation: 146043

 def ilike9s(f):
   return re.sub(r"(\d*\.9*\d\d\d)\d*",r"\1","%.17f" % f)

So...

>>> ilike9s(1.0)
'1.000'
>>> ilike9s(12.9999991232132132)
'12.999999123'
>>> ilike9s(12.345678901234)
'12.345'

And don't forget to import re

Upvotes: 0

Daniel Brückner
Daniel Brückner

Reputation: 59645

I am quite confident that this is not possible with standard formating. I suggest to use something like the following (C# like pseudo code). Especially I suggest to rely on string operations and not to use math code because of many possible precision and rounding problems.

string numberString = number.ToStringWithFullPrecision();

int index = numberString.IndexOf('.');

while ((index < numberString.Length - 1) && (numberString[index + 1] == '9'))
{
    index++;
}

WriteLine(number.PadRightWithThreeZeros().SubString(0, index + 4));

If you like regular expression, you can use them to. Take the following expression and match it against the full precision number string padded with three zeros and you are done.

^([0-9]|[1-9][0-9]|100)\.(9*)([0-8][0-9]{2})

I just realized that both suggestion may cause rounding errors. 99.91238123 becomes 99.9123 when it should become 99.9124 - so the last digits requires additional correction. Easy to do, but makes my suggestion even uglier. This is far away from an elegant and smart algorithm.

Upvotes: 0

Ants Aasma
Ants Aasma

Reputation: 54882

Try this:

import math
def format_percentage(x, precision=3):
    return ("%%.%df%%%%" % (precision - min(0,math.log10(100-x)))) % x

Upvotes: 3

Mark Ransom
Mark Ransom

Reputation: 308138

def ceilpowerof10(x):
    return math.pow(10, math.ceil(math.log10(x)))

def nines(x):
    return -int(math.log10(ceilpowerof10(x) - x))

def threeplaces(x):
    return ('%.' + str(nines(x) + 3) + 'f') % x

Note that nines() throws an error on numbers that are a power of 10 to begin with, it would take a little more work to make it safe for all input. There are probably some issues with negative numbers as well.

Upvotes: 1

astrofrog
astrofrog

Reputation: 34091

Try this:

def print_percent(p):    
    for i in range(30):
        if p <= 100. - 10.**(-i):
            print ("%." + str(max(3,3+i-1)) + "f") % p
            return

or this if you just want to retrieve the string

def print_percent(p):    
    for i in range(20):
        if p <= 100. - 10.**(-i):
            return ("%." + str(max(3,3+i-1)) + "f") % p

Upvotes: 0

Related Questions