]> granicus.if.org Git - python/commitdiff
Issue #2531: Make float-to-decimal comparisons return correct results.
authorMark Dickinson <dickinsm@gmail.com>
Fri, 2 Apr 2010 08:53:22 +0000 (08:53 +0000)
committerMark Dickinson <dickinsm@gmail.com>
Fri, 2 Apr 2010 08:53:22 +0000 (08:53 +0000)
Float to decimal comparison operations now return a result based on
the numeric values of the operands.  Decimal.__hash__ has also been
fixed so that Decimal and float values that compare equal have equal
hash value.

Doc/library/decimal.rst
Lib/decimal.py
Lib/test/test_decimal.py
Misc/NEWS

index d8ce673d4c87b80f3647455ad9b24312a5a8bf1d..7c03b6a47c02be653ecafa202e567e58997d7805 100644 (file)
@@ -364,6 +364,24 @@ Decimal objects
    compared, sorted, and coerced to another type (such as :class:`float` or
    :class:`long`).
 
+   Decimal objects cannot generally be combined with floats in
+   arithmetic operations: an attempt to add a :class:`Decimal` to a
+   :class:`float`, for example, will raise a :exc:`TypeError`.
+   There's one exception to this rule: it's possible to use Python's
+   comparison operators to compare a :class:`float` instance ``x``
+   with a :class:`Decimal` instance ``y``.  Without this exception,
+   comparisons between :class:`Decimal` and :class:`float` instances
+   would follow the general rules for comparing objects of different
+   types described in the :ref:`expressions` section of the reference
+   manual, leading to confusing results.
+
+   .. versionchanged:: 2.7
+      A comparison between a :class:`float` instance ``x`` and a
+      :class:`Decimal` instance ``y`` now returns a result based on
+      the values of ``x`` and ``y``.  In earlier versions ``x < y``
+      returned the same (arbitrary) result for any :class:`Decimal`
+      instance ``x`` and any :class:`float` instance ``y``.
+
    In addition to the standard numeric properties, decimal floating point
    objects also have a number of specialized methods:
 
index a10bdf2502ceeac482883083e0115e986870666a..159669c3f3ceec0cbc95ae2b14fd2da9336284e4 100644 (file)
@@ -855,7 +855,7 @@ class Decimal(object):
     # that specified by IEEE 754.
 
     def __eq__(self, other):
-        other = _convert_other(other)
+        other = _convert_other(other, allow_float=True)
         if other is NotImplemented:
             return other
         if self.is_nan() or other.is_nan():
@@ -863,7 +863,7 @@ class Decimal(object):
         return self._cmp(other) == 0
 
     def __ne__(self, other):
-        other = _convert_other(other)
+        other = _convert_other(other, allow_float=True)
         if other is NotImplemented:
             return other
         if self.is_nan() or other.is_nan():
@@ -871,7 +871,7 @@ class Decimal(object):
         return self._cmp(other) != 0
 
     def __lt__(self, other, context=None):
-        other = _convert_other(other)
+        other = _convert_other(other, allow_float=True)
         if other is NotImplemented:
             return other
         ans = self._compare_check_nans(other, context)
@@ -880,7 +880,7 @@ class Decimal(object):
         return self._cmp(other) < 0
 
     def __le__(self, other, context=None):
-        other = _convert_other(other)
+        other = _convert_other(other, allow_float=True)
         if other is NotImplemented:
             return other
         ans = self._compare_check_nans(other, context)
@@ -889,7 +889,7 @@ class Decimal(object):
         return self._cmp(other) <= 0
 
     def __gt__(self, other, context=None):
-        other = _convert_other(other)
+        other = _convert_other(other, allow_float=True)
         if other is NotImplemented:
             return other
         ans = self._compare_check_nans(other, context)
@@ -898,7 +898,7 @@ class Decimal(object):
         return self._cmp(other) > 0
 
     def __ge__(self, other, context=None):
-        other = _convert_other(other)
+        other = _convert_other(other, allow_float=True)
         if other is NotImplemented:
             return other
         ans = self._compare_check_nans(other, context)
@@ -932,12 +932,18 @@ class Decimal(object):
         # The hash of a nonspecial noninteger Decimal must depend only
         # on the value of that Decimal, and not on its representation.
         # For example: hash(Decimal('100E-1')) == hash(Decimal('10')).
-        if self._is_special:
-            if self._isnan():
-                raise TypeError('Cannot hash a NaN value.')
-            return hash(str(self))
-        if not self:
-            return 0
+        if self._is_special and self._isnan():
+            raise TypeError('Cannot hash a NaN value.')
+
+        # In Python 2.7, we're allowing comparisons (but not
+        # arithmetic operations) between floats and Decimals;  so if
+        # a Decimal instance is exactly representable as a float then
+        # its hash should match that of the float.  Note that this takes care
+        # of zeros and infinities, as well as small integers.
+        self_as_float = float(self)
+        if Decimal.from_float(self_as_float) == self:
+            return hash(self_as_float)
+
         if self._isinteger():
             op = _WorkRep(self.to_integral_value())
             # to make computation feasible for Decimals with large
@@ -5695,15 +5701,21 @@ def _log10_lb(c, correction = {
 
 ##### Helper Functions ####################################################
 
-def _convert_other(other, raiseit=False):
+def _convert_other(other, raiseit=False, allow_float=False):
     """Convert other to Decimal.
 
     Verifies that it's ok to use in an implicit construction.
+    If allow_float is true, allow conversion from float;  this
+    is used in the comparison methods (__eq__ and friends).
+
     """
     if isinstance(other, Decimal):
         return other
     if isinstance(other, (int, long)):
         return Decimal(other)
+    if allow_float and isinstance(other, float):
+        return Decimal.from_float(other)
+
     if raiseit:
         raise TypeError("Unable to convert %s to Decimal" % other)
     return NotImplemented
index 35d74056c9501763c7d0099d2945fed767bc8b5e..4071eff8db9c619ecaae9c93c51b9aa5be2b74dc 100644 (file)
@@ -1208,6 +1208,23 @@ class DecimalUsabilityTest(unittest.TestCase):
             self.assertFalse(Decimal(1) < None)
             self.assertTrue(Decimal(1) > None)
 
+    def test_decimal_float_comparison(self):
+        da = Decimal('0.25')
+        db = Decimal('3.0')
+        self.assert_(da < 3.0)
+        self.assert_(da <= 3.0)
+        self.assert_(db > 0.25)
+        self.assert_(db >= 0.25)
+        self.assert_(da != 1.5)
+        self.assert_(da == 0.25)
+        self.assert_(3.0 > da)
+        self.assert_(3.0 >= da)
+        self.assert_(0.25 < db)
+        self.assert_(0.25 <= db)
+        self.assert_(0.25 != db)
+        self.assert_(3.0 == db)
+        self.assert_(0.1 != Decimal('0.1'))
+
     def test_copy_and_deepcopy_methods(self):
         d = Decimal('43.24')
         c = copy.copy(d)
@@ -1256,6 +1273,15 @@ class DecimalUsabilityTest(unittest.TestCase):
         self.assertTrue(hash(Decimal('Inf')))
         self.assertTrue(hash(Decimal('-Inf')))
 
+        # check that the hashes of a Decimal float match when they
+        # represent exactly the same values
+        test_strings = ['inf', '-Inf', '0.0', '-.0e1',
+                        '34.0', '2.5', '112390.625', '-0.515625']
+        for s in test_strings:
+            f = float(s)
+            d = Decimal(s)
+            self.assertEqual(hash(f), hash(d))
+
         # check that the value of the hash doesn't depend on the
         # current context (issue #1757)
         c = getcontext()
index 3715520cf065cbd20650bad9fa4fe0874a3868d7..4b2255d6d7f02d7aff9ef258f8aeec77309db0fd 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -35,6 +35,11 @@ Core and Builtins
 Library
 -------
 
+- Issue #2531: Comparison operations between floats and Decimal
+  instances now return a result based on the numeric values of the
+  operands;  previously they returned an arbitrary result based on
+  the relative ordering of id(float) and id(Decimal).
+
 - Issue #8233: When run as a script, py_compile.py optionally takes a single
   argument `-` which tells it to read files to compile from stdin.  Each line
   is read on demand and the named file is compiled immediately.  (Original