Python float to Decimal conversion

109,315

Solution 1

Python <2.7

"%.15g" % f

Or in Python 3.0:

format(f, ".15g")

Python 2.7+, 3.2+

Just pass the float to Decimal constructor directly, like this:

from decimal import Decimal
Decimal(f)

Solution 2

You said in your question:

Can someone suggest a good way to convert from float to Decimal preserving value as the user has entered

But every time the user enters a value, it is entered as a string, not as a float. You are converting it to a float somewhere. Convert it to a Decimal directly instead and no precision will be lost.

Solution 3

I suggest this

>>> a = 2.111111
>>> a
2.1111110000000002
>>> str(a)
'2.111111'
>>> decimal.Decimal(str(a))
Decimal('2.111111')

Solution 4

Python does support Decimal creation from a float. You just cast it as a string first. But the precision loss doesn't occur with string conversion. The float you are converting doesn't have that kind of precision in the first place. (Otherwise you wouldn't need Decimal)

I think the confusion here is that we can create float literals in decimal format, but as soon as the interpreter consumes that literal the inner representation becomes a floating point number.

Solution 5

you can convert and than quantize to keep 5 digits after comma via

Decimal(float).quantize(Decimal("1.00000"))
Share:
109,315
Kozyarchuk
Author by

Kozyarchuk

Updated on July 05, 2022

Comments

  • Kozyarchuk
    Kozyarchuk almost 2 years

    Python Decimal doesn't support being constructed from float; it expects that you have to convert float to a string first.

    This is very inconvenient since standard string formatters for float require that you specify number of decimal places rather than significant places. So if you have a number that could have as many as 15 decimal places you need to format as Decimal("%.15f" % my_float), which will give you garbage at the 15th decimal place if you also have any significant digits before decimal (Decimal("%.15f" % 100000.3) == Decimal('100000.300000000002910')).

    Can someone suggest a good way to convert from float to Decimal preserving value as the user has entered, perhaps limiting number of significant digits that can be supported?

  • Kozyarchuk
    Kozyarchuk over 15 years
    repr returns 17 significant places, but there are often errors in 16th and 17th significant place. So the following does not work. from decimal import Decimal start = Decimal('500.123456789016') assert start == Decimal(repr(float(start))), "%s != %s " % (start, Decimal(repr(float(start))))
  • Colbern
    Colbern over 15 years
    @Kozyarchuk: This is not an issue with the number of significant digits. It's a problem with binary representation of decimal numbers. Your example fails for 0.6, too.
  • Kozyarchuk
    Kozyarchuk over 15 years
    Unfortunately it's stored as float in the DB and I can't change the schema
  • Amit Patil
    Amit Patil over 15 years
    Then you have already lost “the value as the user has entered” and you can't get it back. All you can do is apply an arbitrary rounding and hope. Python's treatment of Decimal correctly brings this to your attention so you understand the problem now instead of getting a weird bug later.
  • Nick Chammas
    Nick Chammas over 10 years
    This approach gives the most sensible result. Constructing a Decimal directly off the float as in Sebastian's answer can give surprising results in Python 2.7. Well, not surprising at all if you've played with floats before...
  • Pedro Werneck
    Pedro Werneck over 5 years
    repr returns the shortest representation that results in the same IEEE-754 double.
  • Gary
    Gary over 5 years
    One note with this (as raised in comment for answer using repr()) is that str() here will limit to 17 decimal places, e.g. Decimal(str(0.12345678901234567890)) results in Decimal('0.12345678901234568').
  • Aliaksandr Adzinets
    Aliaksandr Adzinets over 4 years
    Passing float directly to Decimal constructor introduces a rounding error. It's better to convert a float to a string before passing it to the constructor. E.g. Decimal(0.30000000000000004) results in Decimal('0.3000000000000000444089209850062616169452667236328‌​125'), and Decimal(str(0.30000000000000004)) results in Decimal('0.30000000000000004').
  • jfs
    jfs over 4 years
    @AliaksandrAdzinets: it depends on what you want to achieve. To get the value corresponding to the float (0.30000000000000004 .as_integer_ratio() -- the internal representation could be think of as binary numbers written in scientific notation), pass float directly. To get (naive) "what you see is what you get" decimal ratio: 30000000000000004/100000000000000000 , pass string. docs.python.org/3/tutorial/floatingpoint.html
  • jfs
    jfs about 4 years
    @AliaksandrAdzinets: to be clear: it is NOT a rounding error. a float 0.30000000000000004 and a number represented by '0.30000000000000004' are different (because the later can't be represented exactly as a float) i.e., Decimal does NOT introduce the error here, it is the opposite str() changes float's value: it just happens that the error that str() produces might be desirable for a naive view on floats
  • Chris
    Chris about 4 years
    Note that format(0.012345, ".2g") == 0.012 - three dps, whereas format(0.12345, ".2g") == 0.12. The f formatter should be used: format(0.012345, ".2f") == 0.01
  • jfs
    jfs about 4 years
    @Chris: no. f would be wrong here. Try 1.23e-20
  • dotz
    dotz over 2 years
    This does not seem right. JSON is used as data interchange format and all the code will do is convert float to string and then load that string as Decimal. Downvoted.
  • Solomon Duskis
    Solomon Duskis over 2 years
    This answer is really confusing, as the first version using %g behaves differently from the second version: the former attempts to round the float to 15 significant digits, while the second one doesn't.
  • Solomon Duskis
    Solomon Duskis over 2 years
    Yet it reminds us that we got the float by parsing JSON in the first place, there's a way to sidestep this whole float->decimal conversion altogether. Thanks!
  • Solomon Duskis
    Solomon Duskis over 2 years
    It appears that a similar algorithm was later implemented in Python 3, so Decimal(repr(float_value)), as suggested in another answer, works fine today.
  • Solomon Duskis
    Solomon Duskis over 2 years
    I should note that the '500.123456789016' testcase works fine in the modern versions of Python (possibly thanks to bugs.python.org/issue1580 ?), so Decimal(repr(my_float)) seems to be the way to go if you absolutely can't avoid the float in the first place.
  • jfs
    jfs over 2 years
    @Nickolay: please read the comments above (the 1st variant is "what you see is what you get", the 2nd one likely to get the exact representation that may be surprising for some people
  • Solomon Duskis
    Solomon Duskis over 2 years
    @jfs please don't assume that I haven't read the comments; I have, and I believe the answer is confusing for the reasons stated. (And not even the comments explained what ".15g" was trying to do.) Anyways, seems that people on modern python versions should just use Decimal(repr(my_float)) if they absolutely can't fix their code to avoid floats in the first place.
  • jfs
    jfs over 2 years
    @Nickolay: no. There is no one preferred way that is suitable in all cases (what to choose 1st or 2nd depends on context and if somebody doesn't care they can use either)