views:

168

answers:

6

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%

A: 

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
astrofrog
(note you can make '30' larger but that's already more than 64-bit float precision)
astrofrog
A: 

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.

Daniel Brückner
+1  A: 
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.

Mark Ransom
I did the little bit of work to make it safe for all input; see my answer below.
steveha
+3  A: 

Try this:

import math
def format_percentage(x, precision=3):
    return ("%%.%df%%%%" % (precision - min(0,math.log10(100-x)))) % x
Ants Aasma
Much simpler than mine - I should have noticed that these were percentages, and the max would always be 100.
Mark Ransom
But notice that format_percentage(100) throws an error.
Mark Ransom
The given function is undefined for values >= 100, exactly as the original problem statement.
Ants Aasma
Wow, that is wonderful. If you wanted the input to be 0-1.0 rather than 0-100, what would have to be changed? And where can I read more about these specialized percentage formatting strings?
Steven Hepting
I actually like Mark Ransom's solution, which would work for input 0-1.0 without changes. This solution should work if you edit the "100-x" to be "1-x". As for where you can read more about this: this is just ordinary Python format strings, only they are building a custom string on the fly. To read about Python format strings, Google search for "Python format string". Here's an official document: <a href="http://www.python.org/doc/2.5.2/lib/typesseq-strings.html">http://www.python.org/doc/2.5.2/lib/typesseq-strings.html</a>
steveha
Sorry, I guess that comments don't accept <a> tags. Here's that URL by itself: http://www.python.org/doc/2.5.2/lib/typesseq-strings.html
steveha
That's not a specialized percentage formatting string - it's a string that will go through two formatting operations. Where you need % in the second operation, you need %% in the original string. Where you need % in the final output, you need %%%% in the original string.
Mark Ransom
A: 
 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

DigitalRoss
That's thinking outside the box. I like it. :-)
steveha
One problem: it doesn't round properly. 12.345678901234 really ought to print as 12.346.
steveha
The rounding issue is a problem for me. Otherwise, I'd love using regular expressions.
Steven Hepting
+2  A: 

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'
steveha