+1  A: 

It appears there is no string formatting trick which allows you to (1) print floats whose first significant digit appears after the 15th decimal place and (2) not in scientific notation. So that leaves manual string manipulation.

Below I use the decimal module to extract the decimal digits from the float. The float_to_decimal function is used to convert the float to a Decimal object. The obvious way decimal.Decimal(str(f)) is wrong because str(f) can lose significant digits.

float_to_decimal was lifted from the decimal module's documentation.

Once the decimal digits are obtained as a tuple of ints, the code below does the obvious thing: chop off the desired number of sigificant digits, round up if necessary, join the digits together into a string, tack on a sign, place a decimal point and zeros to the left or right as appropriate.

At the bottom you'll find a few cases I used to test the f function.

import decimal

def float_to_decimal(f):
    # http://docs.python.org/library/decimal.html#decimal-faq
    "Convert a floating point number to a Decimal with no loss of information"
    n, d = f.as_integer_ratio()
    numerator, denominator = decimal.Decimal(n), decimal.Decimal(d)
    ctx = decimal.Context(prec=60)
    result = ctx.divide(numerator, denominator)
    while ctx.flags[decimal.Inexact]:
        ctx.flags[decimal.Inexact] = False
        ctx.prec *= 2
        result = ctx.divide(numerator, denominator)
    return result 

 f(number, sigfig):
    # http://stackoverflow.com/questions/2663612/nicely-representing-a-floating-point-number-in-python/2663623#2663623
    assert(sigfig>0)
    try:
        d=decimal.Decimal(number)
    except TypeError:
        d=float_to_decimal(float(number))
    sign,digits,exponent=d.as_tuple()
    if len(digits) < sigfig:
        digits = list(digits)
        digits.extend([0] * (sigfig - len(digits)))    
    shift=d.adjusted()
    result=int(''.join(map(str,digits[:sigfig])))
    # Round the result
    if len(digits)>sigfig and digits[sigfig]>=5: result+=1
    result=list(str(result))
    # Rounding can change the length of result
    # If so, adjust shift
    shift+=len(result)-sigfig
    # reset len of result to sigfig
    result=result[:sigfig]
    if shift >= sigfig-1:
        # Tack more zeros on the end
        result+=['0']*(shift-sigfig+1)
    elif 0<=shift:
        # Place the decimal point in between digits
        result.insert(shift+1,'.')
    else:
        # Tack zeros on the front
        assert(shift<0)
        result=['0.']+['0']*(-shift-1)+result
    if sign:
        result.insert(0,'-')
    return ''.join(result)

if __name__=='__main__':
    tests=[
        (0.1, 1, '0.1'),
        (0.0000000000368568, 2,'0.000000000037'),           
        (0.00000000000000000000368568, 2,'0.0000000000000000000037'),
        (756867, 3, '757000'),
        (-756867, 3, '-757000'),
        (-756867, 1, '-800000'),
        (0.0999999999999,1,'0.1'),
        (0.00999999999999,1,'0.01'),
        (0.00999999999999,2,'0.010'),
        (0.0099,2,'0.0099'),         
        (1.999999999999,1,'2'),
        (1.999999999999,2,'2.0'),           
        (34500000000000000000000, 17, '34500000000000000000000'),
        ('34500000000000000000000', 17, '34500000000000000000000'),  
        (756867, 7, '756867.0'),
        ]

    for number,sigfig,answer in tests:
        try:
            result=f(number,sigfig)
            assert(result==answer)
            print(result)
        except AssertionError:
            print('Error',number,sigfig,result,answer)
unutbu
That's some beautiful code. It's exactly what I wanted. If you change it so that it takes a Decimal instead of float (which is trivial to do), then we can avoid floating-point errors all together.The only problem I find is with long integers. For example, try `(34500000000000000000000, 17, '34500000000000000000000')`. Though, again, this is probably just from floating-point errors.
dln385
Good catch. I made a slight change to the definition of `d` which fixes the problem with long ints, and as a side-effect, allows you to also pass numeric strings for the `number` arg as well.
unutbu
That fix works, but I found other, unrelated errors. Using `(756867, 7, '756867.0')`, I get back `75686.7`. It seems the conditions for padding the result are a little off.
dln385
The problem happens when the user asks for more sigfigs than there are digits. This is most likely to happen when the number entered is an integer. This wasn't an issue before because the integer used to be converted to a float. The solution is to add more zeros to 'digits' before turning it into an integer. Adding this code makes it work for me:`if len(digits) < sigfig:``digits = list(digits)``digits.extend([0] * (sigfig - len(digits)))`
dln385
Yes, that's another good catch. I've edited my code.
unutbu
Hm, I think I like how you handled it better. Editing my post again...
unutbu
+5  A: 

If you want floating point precision you need to use the decimal module, which is part of the Python Standard Library:

>>> import decimal
>>> d = decimal.Decimal('0.0000000000368568')
>>> print '%.15f' % d
0.000000000036857
jathanism
The decimal module looks like it might help, but this really doesn't answer my question.
dln385
@dln385: How so? It meets all the requirements you listed.
Billy ONeal
1. That specifies precision, not significant digits. There's a big difference. 2. It won't round past the decimal point. For example, 756867 with 3 significant digits is 757000. See the original question. 3. That method breaks down with large numbers, such as long ints.
dln385
@dln385: Did you read the docs? **1.** "The decimal module incorporates a notion of significant places so that 1.30 + 1.20 is 2.50. The trailing zero is kept to indicate significance. This is the customary presentation for monetary applications. For multiplication, the “schoolbook” approach uses all the figures in the multiplicands. For instance, 1.3 * 1.2 gives 1.56 while 1.30 * 1.20 gives 1.5600." **2.** it uses fixed point rather than floating so you don't run into such issues in the first place and **3.** the decimal module supports arbitrary precision.
Billy ONeal
@Billy ONeal: Yes, I did read the docs. In my last comment, I was explaining why `'%.15f' % d` didn't answer my question. As for the decimal module in general, I could not find a method that rounds to significant digits (including to the left of the decimal point) or displays the number without exponents. Also, it should be noted that `'%.15f' % d` still breaks down for large integers, even if decimals are being used.
dln385
A: 

Arbitrary precision floats are needed to properly answer this question. Therefore using the decimal module is a must. There is no method to convert a decimal to a string without ever using the exponential format (part of the original question), so I wrote a function to do just that:

def removeExponent(decimal):
    digits = [str(n) for n in decimal.as_tuple().digits]
    length = len(digits)
    exponent = decimal.as_tuple().exponent
    if length <= -1 * exponent:
        zeros = -1 * exponent - length
        digits[0:0] = ["0."] + ["0"] * zeros
    elif 0 < -1 * exponent < length:
        digits.insert(exponent, ".")
    elif 0 <= exponent:
        digits.extend(["0"] * exponent)
    sign = []
    if decimal.as_tuple().sign == 1:
        sign = ["-"]
    print "".join(sign + digits)

The problem is trying to round to significant figures. Decimal's "quantize()" method won't round higher than the decimal point, and the "round()" function always returns a float. I don't know if these are bugs, but it means that the only way to round infinite precision floating point numbers is to parse it as a list or string and do the rounding manually. In other words, there is no sane answer to this question.

dln385
You don't need arbitrary precision floats. Notice that he never neds the rounded answer as a float, only as a string. BTW `-1 * anything` can be written as `-anything`.
Wallacoloo
I don't understand your "won't round higher than the decimal point". Try: `Decimal('123.456').quantize(Decimal('1e1'))`, for example.
Mark Dickinson
BTW, rounding a Decimal instance returns another Decimal in Python 3.x.
Mark Dickinson
@Mark Dickinson: You're right, I didn't expect 'Decimal('123.456').quantize(Decimal('10'))' to have different behavior. Also, if Decimal rounding returns a Decimal in Python 3.x, then it seems it's time for me to upgrade. Thanks for your pointers.
dln385