summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMark Dickinson <dickinsm@gmail.com>2010-04-03 11:08:14 (GMT)
committerMark Dickinson <dickinsm@gmail.com>2010-04-03 11:08:14 (GMT)
commitac256ab2843dfb6c28af0227202df67664ed462e (patch)
treee4b82bd27da37911a04fde01cc06e7ddc39681df
parent5fc16b469ea96f1db11bb2fc9996ac056e8e1c3c (diff)
downloadcpython-ac256ab2843dfb6c28af0227202df67664ed462e.zip
cpython-ac256ab2843dfb6c28af0227202df67664ed462e.tar.gz
cpython-ac256ab2843dfb6c28af0227202df67664ed462e.tar.bz2
Merged revisions 79583,79588-79589 via svnmerge from
svn+ssh://pythondev@svn.python.org/python/trunk ........ r79583 | mark.dickinson | 2010-04-02 09:53:22 +0100 (Fri, 02 Apr 2010) | 7 lines 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. ........ r79588 | mark.dickinson | 2010-04-02 11:17:07 +0100 (Fri, 02 Apr 2010) | 2 lines Issue #7279: Make comparisons involving a Decimal sNaN signal InvalidOperation. ........ r79589 | mark.dickinson | 2010-04-02 11:35:12 +0100 (Fri, 02 Apr 2010) | 6 lines Issue #7279: Make Decimal('nan') hashable. Decimal('snan') remains unhashable. Also rewrite the Decimal __hash__ method so that it doesn't rely on float('inf') being valid: float('inf') could raise an exception on platforms not using IEEE 754 arithmetic. ........
-rw-r--r--Doc/library/decimal.rst18
-rw-r--r--Lib/decimal.py67
-rw-r--r--Lib/test/test_decimal.py87
-rw-r--r--Misc/NEWS10
4 files changed, 155 insertions, 27 deletions
diff --git a/Doc/library/decimal.rst b/Doc/library/decimal.rst
index 9d6ad81..8a806e9 100644
--- a/Doc/library/decimal.rst
+++ b/Doc/library/decimal.rst
@@ -358,6 +358,24 @@ Decimal objects
compared, sorted, and coerced to another type (such as :class:`float` or
:class:`int`).
+ 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 ab38ed4..727aee2 100644
--- a/Lib/decimal.py
+++ b/Lib/decimal.py
@@ -849,8 +849,11 @@ class Decimal(object):
# subject of what should happen for a comparison involving a NaN.
# We take the following approach:
#
- # == comparisons involving a NaN always return False
- # != comparisons involving a NaN always return True
+ # == comparisons involving a quiet NaN always return False
+ # != comparisons involving a quiet NaN always return True
+ # == or != comparisons involving a signaling NaN signal
+ # InvalidOperation, and return False or True as above if the
+ # InvalidOperation is not trapped.
# <, >, <= and >= comparisons involving a (quiet or signaling)
# NaN signal InvalidOperation, and return False if the
# InvalidOperation is not trapped.
@@ -858,25 +861,25 @@ class Decimal(object):
# This behavior is designed to conform as closely as possible to
# that specified by IEEE 754.
- def __eq__(self, other):
- other = _convert_other(other)
+ def __eq__(self, other, context=None):
+ other = _convert_other(other, allow_float=True)
if other is NotImplemented:
return other
- if self.is_nan() or other.is_nan():
+ if self._check_nans(other, context):
return False
return self._cmp(other) == 0
- def __ne__(self, other):
- other = _convert_other(other)
+ def __ne__(self, other, context=None):
+ other = _convert_other(other, allow_float=True)
if other is NotImplemented:
return other
- if self.is_nan() or other.is_nan():
+ if self._check_nans(other, context):
return True
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)
@@ -885,7 +888,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)
@@ -894,7 +897,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)
@@ -903,7 +906,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)
@@ -937,12 +940,34 @@ 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')).
+
+ # Equality comparisons involving signaling nans can raise an
+ # exception; since equality checks are implicitly and
+ # unpredictably used when checking set and dict membership, we
+ # prevent signaling nans from being used as set elements or
+ # dict keys by making __hash__ raise an exception.
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_snan():
+ raise TypeError('Cannot hash a signaling NaN value.')
+ elif self.is_nan():
+ # 0 to match hash(float('nan'))
+ return 0
+ else:
+ # values chosen to match hash(float('inf')) and
+ # hash(float('-inf')).
+ if self._sign:
+ return -271828
+ else:
+ return 314159
+
+ # 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.
+ 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
@@ -5780,15 +5805,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):
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 51bdf9c..7de2400 100644
--- a/Lib/test/test_decimal.py
+++ b/Lib/test/test_decimal.py
@@ -26,6 +26,7 @@ with the corresponding argument.
import math
import os, sys
+import operator
import pickle, copy
import unittest
from decimal import *
@@ -1096,18 +1097,56 @@ class DecimalArithmeticOperatorsTest(unittest.TestCase):
self.assertEqual(abs(Decimal(45)), abs(Decimal(-45))) # abs
def test_nan_comparisons(self):
+ # comparisons involving signaling nans signal InvalidOperation
+
+ # order comparisons (<, <=, >, >=) involving only quiet nans
+ # also signal InvalidOperation
+
+ # equality comparisons (==, !=) involving only quiet nans
+ # don't signal, but return False or True respectively.
+
n = Decimal('NaN')
s = Decimal('sNaN')
i = Decimal('Inf')
f = Decimal('2')
- for x, y in [(n, n), (n, i), (i, n), (n, f), (f, n),
- (s, n), (n, s), (s, i), (i, s), (s, f), (f, s), (s, s)]:
- self.assertTrue(x != y)
- self.assertTrue(not (x == y))
- self.assertTrue(not (x < y))
- self.assertTrue(not (x <= y))
- self.assertTrue(not (x > y))
- self.assertTrue(not (x >= y))
+
+ qnan_pairs = (n, n), (n, i), (i, n), (n, f), (f, n)
+ snan_pairs = (s, n), (n, s), (s, i), (i, s), (s, f), (f, s), (s, s)
+ order_ops = operator.lt, operator.le, operator.gt, operator.ge
+ equality_ops = operator.eq, operator.ne
+
+ # results when InvalidOperation is not trapped
+ for x, y in qnan_pairs + snan_pairs:
+ for op in order_ops + equality_ops:
+ got = op(x, y)
+ expected = True if op is operator.ne else False
+ self.assertIs(expected, got,
+ "expected {0!r} for operator.{1}({2!r}, {3!r}); "
+ "got {4!r}".format(
+ expected, op.__name__, x, y, got))
+
+ # repeat the above, but this time trap the InvalidOperation
+ with localcontext() as ctx:
+ ctx.traps[InvalidOperation] = 1
+
+ for x, y in qnan_pairs:
+ for op in equality_ops:
+ got = op(x, y)
+ expected = True if op is operator.ne else False
+ self.assertIs(expected, got,
+ "expected {0!r} for "
+ "operator.{1}({2!r}, {3!r}); "
+ "got {4!r}".format(
+ expected, op.__name__, x, y, got))
+
+ for x, y in snan_pairs:
+ for op in equality_ops:
+ self.assertRaises(InvalidOperation, operator.eq, x, y)
+ self.assertRaises(InvalidOperation, operator.ne, x, y)
+
+ for x, y in qnan_pairs + snan_pairs:
+ for op in order_ops:
+ self.assertRaises(InvalidOperation, op, x, y)
def test_copy_sign(self):
d = Decimal(1).copy_sign(Decimal(-2))
@@ -1213,6 +1252,23 @@ class DecimalUsabilityTest(unittest.TestCase):
a.sort()
self.assertEqual(a, b)
+ 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)
@@ -1223,6 +1279,10 @@ class DecimalUsabilityTest(unittest.TestCase):
def test_hash_method(self):
#just that it's hashable
hash(Decimal(23))
+ hash(Decimal('Infinity'))
+ hash(Decimal('-Infinity'))
+ hash(Decimal('nan123'))
+ hash(Decimal('-NaN'))
test_values = [Decimal(sign*(2**m + n))
for m in [0, 14, 15, 16, 17, 30, 31,
@@ -1257,10 +1317,19 @@ class DecimalUsabilityTest(unittest.TestCase):
#the same hash that to an int
self.assertEqual(hash(Decimal(23)), hash(23))
- self.assertRaises(TypeError, hash, Decimal('NaN'))
+ self.assertRaises(TypeError, hash, Decimal('sNaN'))
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 561f492..988f71d 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -301,6 +301,16 @@ C-API
Library
-------
+- Issue #7279: Comparisons involving a Decimal signaling NaN now
+ signal InvalidOperation instead of returning False. (Comparisons
+ involving a quiet NaN are unchanged.) Also, Decimal quiet NaNs
+ are now hashable; Decimal signaling NaNs remain unhashable.
+
+- 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).
+
- Added a subtract() method to collections.Counter().
- Issue #8233: When run as a script, py_compile.py optionally takes a single