diff options
author | Mark Dickinson <dickinsm@gmail.com> | 2010-04-02 08:53:22 +0000 |
---|---|---|
committer | Mark Dickinson <dickinsm@gmail.com> | 2010-04-02 08:53:22 +0000 |
commit | 99d8096c174ccb025e8ff55e614ea3820f89e204 (patch) | |
tree | b93c027821ca0e003ce090b97319f151246230ff | |
parent | 6eba77923565dc90d5b77b4b3f0e8f82f6109714 (diff) | |
download | cpython-git-99d8096c174ccb025e8ff55e614ea3820f89e204.tar.gz |
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.
-rw-r--r-- | Doc/library/decimal.rst | 18 | ||||
-rw-r--r-- | Lib/decimal.py | 38 | ||||
-rw-r--r-- | Lib/test/test_decimal.py | 26 | ||||
-rw-r--r-- | Misc/NEWS | 5 |
4 files changed, 74 insertions, 13 deletions
diff --git a/Doc/library/decimal.rst b/Doc/library/decimal.rst index d8ce673d4c..7c03b6a47c 100644 --- a/Doc/library/decimal.rst +++ b/Doc/library/decimal.rst @@ -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: diff --git a/Lib/decimal.py b/Lib/decimal.py index a10bdf2502..159669c3f3 100644 --- a/Lib/decimal.py +++ b/Lib/decimal.py @@ -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 diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 35d74056c9..4071eff8db 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -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() @@ -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 |