Issue #2531: Make float-to-decimal comparisons return correct results.
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.
diff --git a/Doc/library/decimal.rst b/Doc/library/decimal.rst
index d8ce673..7c03b6a 100644
--- a/Doc/library/decimal.rst
+++ b/Doc/library/decimal.rst
@@ -364,6 +364,24 @@
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:
diff --git a/Lib/decimal.py b/Lib/decimal.py
index a10bdf2..159669c 100644
--- a/Lib/decimal.py
+++ b/Lib/decimal.py
@@ -855,7 +855,7 @@
# 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
# 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 @@
##### 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
diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py
index 35d7405..4071eff 100644
--- a/Lib/test/test_decimal.py
+++ b/Lib/test/test_decimal.py
@@ -1208,6 +1208,23 @@
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 @@
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()
diff --git a/Misc/NEWS b/Misc/NEWS
index 3715520..4b2255d 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -35,6 +35,11 @@
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