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.
This commit is contained in:
Mark Dickinson 2010-04-02 08:53:22 +00:00
parent 6eba779235
commit 99d8096c17
4 changed files with 74 additions and 13 deletions

View File

@ -364,6 +364,24 @@ Decimal objects
compared, sorted, and coerced to another type (such as :class:`float` or compared, sorted, and coerced to another type (such as :class:`float` or
:class:`long`). :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 In addition to the standard numeric properties, decimal floating point
objects also have a number of specialized methods: objects also have a number of specialized methods:

View File

@ -855,7 +855,7 @@ class Decimal(object):
# that specified by IEEE 754. # that specified by IEEE 754.
def __eq__(self, other): def __eq__(self, other):
other = _convert_other(other) other = _convert_other(other, allow_float=True)
if other is NotImplemented: if other is NotImplemented:
return other return other
if self.is_nan() or other.is_nan(): if self.is_nan() or other.is_nan():
@ -863,7 +863,7 @@ class Decimal(object):
return self._cmp(other) == 0 return self._cmp(other) == 0
def __ne__(self, other): def __ne__(self, other):
other = _convert_other(other) other = _convert_other(other, allow_float=True)
if other is NotImplemented: if other is NotImplemented:
return other return other
if self.is_nan() or other.is_nan(): if self.is_nan() or other.is_nan():
@ -871,7 +871,7 @@ class Decimal(object):
return self._cmp(other) != 0 return self._cmp(other) != 0
def __lt__(self, other, context=None): def __lt__(self, other, context=None):
other = _convert_other(other) other = _convert_other(other, allow_float=True)
if other is NotImplemented: if other is NotImplemented:
return other return other
ans = self._compare_check_nans(other, context) ans = self._compare_check_nans(other, context)
@ -880,7 +880,7 @@ class Decimal(object):
return self._cmp(other) < 0 return self._cmp(other) < 0
def __le__(self, other, context=None): def __le__(self, other, context=None):
other = _convert_other(other) other = _convert_other(other, allow_float=True)
if other is NotImplemented: if other is NotImplemented:
return other return other
ans = self._compare_check_nans(other, context) ans = self._compare_check_nans(other, context)
@ -889,7 +889,7 @@ class Decimal(object):
return self._cmp(other) <= 0 return self._cmp(other) <= 0
def __gt__(self, other, context=None): def __gt__(self, other, context=None):
other = _convert_other(other) other = _convert_other(other, allow_float=True)
if other is NotImplemented: if other is NotImplemented:
return other return other
ans = self._compare_check_nans(other, context) ans = self._compare_check_nans(other, context)
@ -898,7 +898,7 @@ class Decimal(object):
return self._cmp(other) > 0 return self._cmp(other) > 0
def __ge__(self, other, context=None): def __ge__(self, other, context=None):
other = _convert_other(other) other = _convert_other(other, allow_float=True)
if other is NotImplemented: if other is NotImplemented:
return other return other
ans = self._compare_check_nans(other, context) ans = self._compare_check_nans(other, context)
@ -932,12 +932,18 @@ class Decimal(object):
# The hash of a nonspecial noninteger Decimal must depend only # The hash of a nonspecial noninteger Decimal must depend only
# on the value of that Decimal, and not on its representation. # on the value of that Decimal, and not on its representation.
# For example: hash(Decimal('100E-1')) == hash(Decimal('10')). # For example: hash(Decimal('100E-1')) == hash(Decimal('10')).
if self._is_special: if self._is_special and self._isnan():
if self._isnan(): raise TypeError('Cannot hash a NaN value.')
raise TypeError('Cannot hash a NaN value.')
return hash(str(self)) # In Python 2.7, we're allowing comparisons (but not
if not self: # arithmetic operations) between floats and Decimals; so if
return 0 # 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(): if self._isinteger():
op = _WorkRep(self.to_integral_value()) op = _WorkRep(self.to_integral_value())
# to make computation feasible for Decimals with large # to make computation feasible for Decimals with large
@ -5695,15 +5701,21 @@ def _log10_lb(c, correction = {
##### Helper Functions #################################################### ##### Helper Functions ####################################################
def _convert_other(other, raiseit=False): def _convert_other(other, raiseit=False, allow_float=False):
"""Convert other to Decimal. """Convert other to Decimal.
Verifies that it's ok to use in an implicit construction. 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): if isinstance(other, Decimal):
return other return other
if isinstance(other, (int, long)): if isinstance(other, (int, long)):
return Decimal(other) return Decimal(other)
if allow_float and isinstance(other, float):
return Decimal.from_float(other)
if raiseit: if raiseit:
raise TypeError("Unable to convert %s to Decimal" % other) raise TypeError("Unable to convert %s to Decimal" % other)
return NotImplemented return NotImplemented

View File

@ -1208,6 +1208,23 @@ class DecimalUsabilityTest(unittest.TestCase):
self.assertFalse(Decimal(1) < None) self.assertFalse(Decimal(1) < None)
self.assertTrue(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): def test_copy_and_deepcopy_methods(self):
d = Decimal('43.24') d = Decimal('43.24')
c = copy.copy(d) c = copy.copy(d)
@ -1256,6 +1273,15 @@ class DecimalUsabilityTest(unittest.TestCase):
self.assertTrue(hash(Decimal('Inf'))) self.assertTrue(hash(Decimal('Inf')))
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 # check that the value of the hash doesn't depend on the
# current context (issue #1757) # current context (issue #1757)
c = getcontext() c = getcontext()

View File

@ -35,6 +35,11 @@ Core and Builtins
Library 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 - 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 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 is read on demand and the named file is compiled immediately. (Original