mirror of
				https://github.com/python/cpython.git
				synced 2025-10-30 21:21:22 +00:00 
			
		
		
		
	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:
		
							parent
							
								
									6eba779235
								
							
						
					
					
						commit
						99d8096c17
					
				
					 4 changed files with 74 additions and 13 deletions
				
			
		|  | @ -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: | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -855,7 +855,7 @@ def _cmp(self, other): | ||||||
|     # 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 @@ def __eq__(self, other): | ||||||
|         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 @@ def __ne__(self, other): | ||||||
|         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 @@ def __lt__(self, other, context=None): | ||||||
|         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 @@ def __le__(self, other, context=None): | ||||||
|         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 @@ def __gt__(self, other, context=None): | ||||||
|         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 @@ def __hash__(self): | ||||||
|         # 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)) | 
 | ||||||
|         if not self: |         # In Python 2.7, we're allowing comparisons (but not | ||||||
|             return 0 |         # 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(): |         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 | ||||||
|  |  | ||||||
|  | @ -1208,6 +1208,23 @@ def test_comparison_operators(self): | ||||||
|             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 @@ def test_hash_method(self): | ||||||
|         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() | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Mark Dickinson
						Mark Dickinson