Nicely representing a floating-point number in python
Solution 1
It appears there is no built-in 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
def 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)
Solution 2
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
dln385
Updated on July 09, 2022Comments
-
dln385 almost 2 years
I want to represent a floating-point number as a string rounded to some number of significant digits, and never using the exponential format. Essentially, I want to display any floating-point number and make sure it “looks nice”.
There are several parts to this problem:
- I need to be able to specify the number of significant digits.
- The number of significant digits needs to be variable, which can't be done with with the string formatting operator. [edit] I've been corrected; the string formatting operator can do this.
- I need it to be rounded the way a person would expect, not something like 1.999999999999
I've figured out one way of doing this, though it looks like a work-round and it's not quite perfect. (The maximum precision is 15 significant digits.)
>>> def f(number, sigfig): return ("%.15f" % (round(number, int(-1 * floor(log10(number)) + (sigfig - 1))))).rstrip("0").rstrip(".") >>> print f(0.1, 1) 0.1 >>> print f(0.0000000000368568, 2) 0.000000000037 >>> print f(756867, 3) 757000
Is there a better way to do this? Why doesn't Python have a built-in function for this?
-
dln385 about 14 yearsThe decimal module looks like it might help, but this really doesn't answer my question.
-
Billy ONeal about 14 years@dln385: How so? It meets all the requirements you listed.
-
dln385 about 14 years1. 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.
-
Billy ONeal about 14 years@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.
-
dln385 about 14 years@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. -
Ponkadoodle about 14 yearsYou 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
. -
dln385 about 14 yearsThat'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. -
Mark Dickinson about 14 yearsI don't understand your "won't round higher than the decimal point". Try:
Decimal('123.456').quantize(Decimal('1e1'))
, for example. -
Mark Dickinson about 14 yearsBTW, rounding a Decimal instance returns another Decimal in Python 3.x.
-
unutbu about 14 yearsGood 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 thenumber
arg as well. -
dln385 about 14 years@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 about 14 yearsThat fix works, but I found other, unrelated errors. Using
(756867, 7, '756867.0')
, I get back75686.7
. It seems the conditions for padding the result are a little off.