]> granicus.if.org Git - python/commitdiff
Issue 4796: Add from_float methods to the decimal module.
authorRaymond Hettinger <python@rcn.com>
Sat, 3 Jan 2009 19:20:32 +0000 (19:20 +0000)
committerRaymond Hettinger <python@rcn.com>
Sat, 3 Jan 2009 19:20:32 +0000 (19:20 +0000)
Doc/library/decimal.rst
Lib/decimal.py
Lib/test/test_decimal.py
Misc/NEWS

index ad8b65c0175081a7e4fe68cf20d8ea73a36b5cba..76ba2ee68e2d5462af5a203dece9fef38c25545f 100644 (file)
@@ -453,6 +453,29 @@ Decimal objects
       >>> Decimal(321).exp()
       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])
 
       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
       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()
 
       Returns a value equal to ``Emin - prec + 1`` which is the minimum exponent
index 1f6218cd4ac9208fcc986f4c6a8e63541e8afe9b..197269c7d731fb094be15aeff8566ecd2be710c6 100644 (file)
@@ -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.
index d0de64d311d006488c0211303ebc779d63c51950..ade356c12d631c1cc9bfc76afdf203412d29001c 100644 (file)
@@ -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):
index d30eeaa4e73adc3e2d6df7df3e797ac5cbe1eb03..9db89a8c9ce31378dc91f5b067792ab8da18d566 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -12,8 +12,6 @@ What's New in Python 3.1 alpha 0
 Core and Builtins
 -----------------
 
-- Issue #4817: Remove unused function PyOS_GetLastModificationTime.
-
 - 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
   items, rather than the size in bytes.
@@ -88,6 +86,9 @@ Library
   Python 3.x, in accordance with the `official amendments of the spec 
   <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
   constants in the decimal module.  (Dec_0 becomes _Dec_0, etc.)