4

When you convert a float to Decimal, the Decimal will contain as accurate a representation of the binary number that it can. It's nice to be accurate, but it isn't always what you want. Since many decimal numbers can't be represented exactly in binary, the resulting Decimal will be a little off - sometimes a little high, sometimes a little low.

>>> from decimal import Decimal
>>> for f in (0.1, 0.3, 1e25, 1e28, 1.0000000000001):
    print Decimal(f)

0.1000000000000000055511151231257827021181583404541015625
0.299999999999999988897769753748434595763683319091796875
10000000000000000905969664
9999999999999999583119736832
1.000000000000099920072216264088638126850128173828125

Ideally we'd like the Decimal to be rounded to the most likely decimal equivalent.

I tried converting to str since a Decimal created from a string will be exact. Unfortunately str rounds a little too much.

>>> for f in (0.1, 0.3, 1e25, 1e28, 1.0000000000001):
    print Decimal(str(f))

0.1
0.3
1E+25
1E+28
1.0

Is there a way of getting a nicely rounded Decimal from a float?

Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
  • 1
    Can anybody explain the downvote? I won't take it personally, I promise. – Mark Ransom Sep 19 '13 at 04:10
  • 1
    If you want to make a Decimal corresponding to a constant, literal value, it's better to use a string as input rather than a float. If you instead got the float from some computation somewhere, you probably want to do one of the following: keep all the precision, round to the context's precision, or change the source of the float to provide a Decimal. Rounding to the float's `repr`esentation precision is generally undesirable. – user2357112 Sep 19 '13 at 04:36
  • (I'm not the downvoter.) – user2357112 Sep 19 '13 at 04:38
  • May be because it is complicated problem. @user2357112 I am also not :) – Grijesh Chauhan Sep 19 '13 at 04:40
  • @user2357112, I guess it didn't occur to me to state explicitly that it's better to generate the Decimal from a string directly, I just assumed that up front. But sometimes it isn't possible. And I would never accuse anybody of being a downvoter no matter what their comment says, I know StackOverflow enough to know there's no correlation. – Mark Ransom Sep 19 '13 at 04:43
  • 2
    Note that prior to Python 2.7, the Decimal constructor wouldn't accept floats as input, specifically to make people think about representation error. The 2.6 documentation says, "To create a Decimal from a float, first convert it to a string. This serves as an explicit reminder of the details of the conversion (including representation error)." – user2357112 Sep 19 '13 at 04:45
  • @user2357112, very interesting! I never saw the 2.6 documentation. It also isn't obvious how to do the string conversion in the best way, and I assume the documentation didn't give any guidance. – Mark Ransom Sep 19 '13 at 04:50
  • Define “the most likely Decimal equivalent”. Java uses just as many digits as needed to uniquely distinguish the result from adjacent values in the source format. – Eric Postpischil Sep 19 '13 at 12:32
  • 1
    @EricPostpischil: Python's `repr` does exactly the same (a la Burger and Dybvig). – Mark Dickinson Sep 19 '13 at 14:47

2 Answers2

4

It turns out that repr does a better job of converting a float to a string than str does. It's the quick-and-easy way to do the conversion.

>>> for f in (0.1, 0.3, 1e25, 1e28, 1.0000000000001):
    print Decimal(repr(f))

0.1
0.3
1E+25
1E+28
1.0000000000001

Before I discovered that, I came up with a brute-force way of doing the rounding. It has the advantage of recognizing that large numbers are accurate to 15 digits - the repr method above only recognizes one significant digit for the 1e25 and 1e28 examples.

from decimal import Decimal,DecimalTuple

def _increment(digits, exponent):
    new_digits = [0] + list(digits)
    new_digits[-1] += 1
    for i in range(len(new_digits)-1, 0, -1):
        if new_digits[i] > 9:
            new_digits[i] -= 10
            new_digits[i-1] += 1
    if new_digits[0]:
        return tuple(new_digits[:-1]), exponent + 1
    return tuple(new_digits[1:]), exponent

def nearest_decimal(f):
    sign, digits, exponent = Decimal(f).as_tuple()
    if len(digits) > 15:
        round_up = digits[15] >= 5
        exponent += len(digits) - 15
        digits = digits[:15]
        if round_up:
            digits, exponent = _increment(digits, exponent)
    while digits and digits[-1] == 0 and exponent < 0:
        digits = digits[:-1]
        exponent += 1
    return Decimal(DecimalTuple(sign, digits, exponent))

>>> for f in (0.1, 0.3, 1e25, 1e28, 1.0000000000001):
    print nearest_decimal(f)

0.1
0.3
1.00000000000000E+25
1.00000000000000E+28
1.0000000000001

Edit: I discovered one more reason to use the brute-force rounding. repr tries to return a string that uniquely identifies the underlying float bit representation, but it doesn't necessarily ensure the accuracy of the last digit. By using one less digit, my rounding function will more often be the number you would expect.

>>> print Decimal(repr(2.0/3.0))
0.6666666666666666
>>> print dec.nearest_decimal(2.0/3.0)
0.666666666666667

The decimal created with repr is actually more accurate, but it implies a level of precision that doesn't exist. The nearest_decimal function delivers a better match between precision and accuracy.

Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
  • I don't understand only last value is different. - – Grijesh Chauhan Sep 19 '13 at 04:42
  • If you find yourself guessing what might have been the decimal representation that was converted to `float` in order to recover that as `Decimal`, you or the APIs you are forced to use are doing something wrong. I am not a little disappointed that your solution reinforces the idea that the algorithm known as `repr` in Python is somehow “better” than decimal conversion to a fixed number of digits. `repr` is neither “better” nor “proper” as that comment puts it,it only looks nice: http://stackoverflow.com/questions/18686269/compare-floats-to-three-decimal-places/18686358#comment27531331_18686946 – Pascal Cuoq Sep 19 '13 at 08:27
  • @PascalCuoq, I don't claim that `repr` is the best, just that it's adequate for many purposes and arguably better than the default conversion from `float` to `Decimal` or from `str(float)` to `Decimal`. My latest edit should reinforce that. – Mark Ransom Sep 20 '13 at 03:57
0

I have implemented this in Pharo Smalltalk, in a Float method named asMinimalDecimalFraction.

It is exactly the same problem as printing the shortest decimal fraction that would be re-interpreted as the same float/double, assuming correct rounding (to nearest).

See my answer at Count number of digits after `.` in floating point numbers? for more references

Community
  • 1
  • 1
aka.nice
  • 9,100
  • 1
  • 28
  • 40
  • Thanks, but the question is specifically about Python. – Mark Ransom Sep 19 '13 at 22:11
  • Doesn't python implement a form of Robert G. Burger and R. Kent Dybvig algorithm for printing shortest decimal form of a float? If yes, you could do like I did in Smalltalk, hijack the algorithm to produce a Fraction rather than a String. – aka.nice Sep 19 '13 at 22:15
  • Ah Ah, but I see that repr is doing exactly this... as reported by Mark Dickinson. – aka.nice Sep 19 '13 at 22:22