diff options
author | Raymond Hettinger <python@rcn.com> | 2009-01-03 19:20:32 (GMT) |
---|---|---|
committer | Raymond Hettinger <python@rcn.com> | 2009-01-03 19:20:32 (GMT) |
commit | 771ed76102a0c8a1c13dcd80d682f64c5e94c043 (patch) | |
tree | ac76febff16820cc07ed78954aa8b9affdc98263 /Lib | |
parent | 5f81741106ebf500ce249c37b86642344c4215c9 (diff) | |
download | cpython-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.py | 50 | ||||
-rw-r--r-- | Lib/test/test_decimal.py | 49 |
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): |