summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorRaymond Hettinger <python@rcn.com>2009-01-03 19:20:32 (GMT)
committerRaymond Hettinger <python@rcn.com>2009-01-03 19:20:32 (GMT)
commit771ed76102a0c8a1c13dcd80d682f64c5e94c043 (patch)
treeac76febff16820cc07ed78954aa8b9affdc98263 /Lib
parent5f81741106ebf500ce249c37b86642344c4215c9 (diff)
downloadcpython-771ed76102a0c8a1c13dcd80d682f64c5e94c043.zip
cpython-771ed76102a0c8a1c13dcd80d682f64c5e94c043.tar.gz
cpython-771ed76102a0c8a1c13dcd80d682f64c5e94c043.tar.bz2
Issue 4796: Add from_float methods to the decimal module.
Diffstat (limited to 'Lib')
-rw-r--r--Lib/decimal.py50
-rw-r--r--Lib/test/test_decimal.py49
2 files changed, 99 insertions, 0 deletions
diff --git a/Lib/decimal.py b/Lib/decimal.py
index 1f6218c..197269c 100644
--- a/Lib/decimal.py
+++ b/Lib/decimal.py
@@ -136,6 +136,7 @@ __all__ = [
import numbers as _numbers
import copy as _copy
+import math as _math
try:
from collections import namedtuple as _namedtuple
@@ -654,6 +655,38 @@ class Decimal(_numbers.Real):
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):
"""Returns whether the number is not actually one.
@@ -3830,6 +3863,23 @@ class Context(object):
"diagnostic info too long in NaN")
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
def abs(self, a):
"""Returns the absolute value of the operand.
diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py
index d0de64d..ade356c 100644
--- a/Lib/test/test_decimal.py
+++ b/Lib/test/test_decimal.py
@@ -1421,6 +1421,55 @@ class DecimalPythonAPItests(unittest.TestCase):
r = d.to_integral(ROUND_DOWN)
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):
def test_pickle(self):