Issue 4796: Add from_float methods to the decimal module.

This commit is contained in:
Raymond Hettinger 2009-01-03 19:20:32 +00:00
parent 5f81741106
commit 771ed76102
4 changed files with 145 additions and 2 deletions

View File

@ -453,6 +453,29 @@ Decimal objects
>>> Decimal(321).exp() >>> Decimal(321).exp()
Decimal('2.561702493119680037517373933E+139') Decimal('2.561702493119680037517373933E+139')
.. method:: from_float(f)
Classmethod that converts a float to a decimal number, exactly.
Note `Decimal.from_float(0.1)` is not the same as `Decimal('0.1')`.
Since 0.1 is not exactly representable in binary floating point, the
value is stored as the nearest representable value which is
`0x1.999999999999ap-4`. That equivalent value in decimal is
`0.1000000000000000055511151231257827021181583404541015625`.
.. doctest::
>>> Decimal.from_float(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>> Decimal.from_float(float('nan'))
Decimal('NaN')
>>> Decimal.from_float(float('inf'))
Decimal('Infinity')
>>> Decimal.from_float(float('-inf'))
Decimal('-Infinity')
.. versionadded:: 2.7
.. method:: fma(other, third[, context]) .. method:: fma(other, third[, context])
Fused multiply-add. Return self*other+third with no rounding of the Fused multiply-add. Return self*other+third with no rounding of the
@ -910,6 +933,26 @@ In addition to the three supplied contexts, new contexts can be created with the
If the argument is a string, no leading or trailing whitespace is If the argument is a string, no leading or trailing whitespace is
permitted. permitted.
.. method:: create_decimal_from_float(f)
Creates a new Decimal instance from a float *f* but rounding using *self*
as the context. Unlike the :method:`Decimal.from_float` class method,
the context precision, rounding method, flags, and traps are applied to
the conversion.
.. doctest::
>>> context = Context(prec=5, rounding=ROUND_DOWN)
>>> context.create_decimal_from_float(math.pi)
Decimal('3.1415')
>>> context = Context(prec=5, traps=[Inexact])
>>> context.create_decimal_from_float(math.pi)
Traceback (most recent call last):
...
decimal.Inexact: None
.. versionadded:: 2.7
.. method:: Etiny() .. method:: Etiny()
Returns a value equal to ``Emin - prec + 1`` which is the minimum exponent Returns a value equal to ``Emin - prec + 1`` which is the minimum exponent

View File

@ -136,6 +136,7 @@ __all__ = [
import numbers as _numbers import numbers as _numbers
import copy as _copy import copy as _copy
import math as _math
try: try:
from collections import namedtuple as _namedtuple from collections import namedtuple as _namedtuple
@ -654,6 +655,38 @@ class Decimal(_numbers.Real):
raise TypeError("Cannot convert %r to Decimal" % value) raise TypeError("Cannot convert %r to Decimal" % value)
@classmethod
def from_float(cls, f):
"""Converts a float to a decimal number, exactly.
Note that Decimal.from_float(0.1) is not the same as Decimal('0.1').
Since 0.1 is not exactly representable in binary floating point, the
value is stored as the nearest representable value which is
0x1.999999999999ap-4. The exact equivalent of the value in decimal
is 0.1000000000000000055511151231257827021181583404541015625.
>>> Decimal.from_float(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>> Decimal.from_float(float('nan'))
Decimal('NaN')
>>> Decimal.from_float(float('inf'))
Decimal('Infinity')
>>> Decimal.from_float(-float('inf'))
Decimal('-Infinity')
>>> Decimal.from_float(-0.0)
Decimal('-0')
"""
if isinstance(f, int): # handle integer inputs
return cls(f)
if _math.isinf(f) or _math.isnan(f): # raises TypeError if not a float
return cls(repr(f))
sign = 0 if _math.copysign(1.0, f) == 1.0 else 1
n, d = abs(f).as_integer_ratio()
k = d.bit_length() - 1
result = _dec_from_triple(sign, str(n*5**k), -k)
return result if cls is Decimal else cls(result)
def _isnan(self): def _isnan(self):
"""Returns whether the number is not actually one. """Returns whether the number is not actually one.
@ -3830,6 +3863,23 @@ class Context(object):
"diagnostic info too long in NaN") "diagnostic info too long in NaN")
return d._fix(self) return d._fix(self)
def create_decimal_from_float(self, f):
"""Creates a new Decimal instance from a float but rounding using self
as the context.
>>> context = Context(prec=5, rounding=ROUND_DOWN)
>>> context.create_decimal_from_float(3.1415926535897932)
Decimal('3.1415')
>>> context = Context(prec=5, traps=[Inexact])
>>> context.create_decimal_from_float(3.1415926535897932)
Traceback (most recent call last):
...
decimal.Inexact: None
"""
d = Decimal.from_float(f) # An exact conversion
return d._fix(self) # Apply the context rounding
# Methods # Methods
def abs(self, a): def abs(self, a):
"""Returns the absolute value of the operand. """Returns the absolute value of the operand.

View File

@ -1421,6 +1421,55 @@ class DecimalPythonAPItests(unittest.TestCase):
r = d.to_integral(ROUND_DOWN) r = d.to_integral(ROUND_DOWN)
self.assertEqual(Decimal(math.trunc(d)), r) self.assertEqual(Decimal(math.trunc(d)), r)
def test_from_float(self):
class MyDecimal(Decimal):
pass
r = MyDecimal.from_float(0.1)
self.assertEqual(type(r), MyDecimal)
self.assertEqual(str(r),
'0.1000000000000000055511151231257827021181583404541015625')
bigint = 12345678901234567890123456789
self.assertEqual(MyDecimal.from_float(bigint), MyDecimal(bigint))
self.assert_(MyDecimal.from_float(float('nan')).is_qnan())
self.assert_(MyDecimal.from_float(float('inf')).is_infinite())
self.assert_(MyDecimal.from_float(float('-inf')).is_infinite())
self.assertEqual(str(MyDecimal.from_float(float('nan'))),
str(Decimal('NaN')))
self.assertEqual(str(MyDecimal.from_float(float('inf'))),
str(Decimal('Infinity')))
self.assertEqual(str(MyDecimal.from_float(float('-inf'))),
str(Decimal('-Infinity')))
self.assertRaises(TypeError, MyDecimal.from_float, 'abc')
for i in range(200):
x = random.expovariate(0.01) * (random.random() * 2.0 - 1.0)
self.assertEqual(x, float(MyDecimal.from_float(x))) # roundtrip
def test_create_decimal_from_float(self):
context = Context(prec=5, rounding=ROUND_DOWN)
self.assertEqual(
context.create_decimal_from_float(math.pi),
Decimal('3.1415')
)
context = Context(prec=5, rounding=ROUND_UP)
self.assertEqual(
context.create_decimal_from_float(math.pi),
Decimal('3.1416')
)
context = Context(prec=5, traps=[Inexact])
self.assertRaises(
Inexact,
context.create_decimal_from_float,
math.pi
)
self.assertEqual(repr(context.create_decimal_from_float(-0.0)),
"Decimal('-0')")
self.assertEqual(repr(context.create_decimal_from_float(1.0)),
"Decimal('1')")
self.assertEqual(repr(context.create_decimal_from_float(10)),
"Decimal('10')")
class ContextAPItests(unittest.TestCase): class ContextAPItests(unittest.TestCase):
def test_pickle(self): def test_pickle(self):

View File

@ -12,8 +12,6 @@ What's New in Python 3.1 alpha 0
Core and Builtins Core and Builtins
----------------- -----------------
- Issue #4817: Remove unused function PyOS_GetLastModificationTime.
- Issue #4580: Fix slicing of memoryviews when the item size is greater than - Issue #4580: Fix slicing of memoryviews when the item size is greater than
one byte. Also fixes the meaning of len() so that it returns the number of one byte. Also fixes the meaning of len() so that it returns the number of
items, rather than the size in bytes. items, rather than the size in bytes.
@ -88,6 +86,9 @@ Library
Python 3.x, in accordance with the `official amendments of the spec Python 3.x, in accordance with the `official amendments of the spec
<http://www.wsgi.org/wsgi/Amendments_1.0>`_. <http://www.wsgi.org/wsgi/Amendments_1.0>`_.
- Issue #4796: Added Decimal.from_float() and Context.create_decimal_from_float()
to the decimal module.
- Issue #4812: add missing underscore prefix to some internal-use-only - Issue #4812: add missing underscore prefix to some internal-use-only
constants in the decimal module. (Dec_0 becomes _Dec_0, etc.) constants in the decimal module. (Dec_0 becomes _Dec_0, etc.)