Index: Doc/library/decimal.rst =================================================================== --- Doc/library/decimal.rst (revision 68185) +++ Doc/library/decimal.rst (working copy) @@ -484,6 +484,29 @@ .. versionadded:: 2.6 + .. 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]) Fused multiply-add. Return self*other+third with no rounding of the @@ -1007,6 +1030,26 @@ If the argument is a string, no leading or trailing whitespace is 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): + ... + Inexact: None + + .. versionadded:: 2.7 + .. method:: Etiny() Returns a value equal to ``Emin - prec + 1`` which is the minimum exponent Index: Lib/decimal.py =================================================================== --- Lib/decimal.py (revision 68185) +++ Lib/decimal.py (working copy) @@ -135,6 +135,7 @@ ] import copy as _copy +import math as _math try: from collections import namedtuple as _namedtuple @@ -653,6 +654,40 @@ 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 equilvalent of the value in decimal + is Decimal('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') + + """ + if isinstance(f, (int, long)): # handle integer inputs + return cls(f) + if _math.isinf(f) or _math.isnan(f): # raises TypeError if not a float + return cls(str(f)) + n, d = f.as_integer_ratio() + numerator, denominator = Decimal(n), Decimal(d) + ctx = Context(prec=60) + result = ctx.divide(numerator, denominator) + while ctx.flags[Inexact]: + ctx.flags[Inexact] = False + ctx.prec *= 2 + result = ctx.divide(numerator, denominator) + return result if cls is Decimal else cls(result) + def _isnan(self): """Returns whether the number is not actually one. @@ -3744,6 +3779,23 @@ "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): + ... + Inexact: None + + """ + d = Decimal.from_float(f) # An exact conversion + return self.plus(d) # Apply the context rounding + # Methods def abs(self, a): """Returns the absolute value of the operand. Index: Lib/test/test_decimal.py =================================================================== --- Lib/test/test_decimal.py (revision 68185) +++ Lib/test/test_decimal.py (working copy) @@ -1361,6 +1361,46 @@ 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') + + 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 + ) + class ContextAPItests(unittest.TestCase): def test_pickle(self):