]> granicus.if.org Git - python/commitdiff
Issue #8188: Comparisons between Decimal objects and other numeric
authorMark Dickinson <dickinsm@gmail.com>
Fri, 11 Jun 2010 10:44:52 +0000 (10:44 +0000)
committerMark Dickinson <dickinsm@gmail.com>
Fri, 11 Jun 2010 10:44:52 +0000 (10:44 +0000)
objects (Fraction, float, complex, int) now all function as expected.

Doc/library/decimal.rst
Lib/decimal.py
Lib/test/test_fractions.py
Lib/test/test_numeric_tower.py
Misc/NEWS

index c20188dabdb0c7ef287c497df94a650fefa1250f..bc495ae2566f29f66630b7e6bcfc85c8d98b688e 100644 (file)
@@ -363,23 +363,17 @@ 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.
+   Decimal objects cannot generally be combined with floats or
+   instances of :class:`fractions.Fraction` in arithmetic operations:
+   an attempt to add a :class:`Decimal` to a :class:`float`, for
+   example, will raise a :exc:`TypeError`.  However, it is possible to
+   use Python's comparison operators to compare a :class:`Decimal`
+   instance ``x`` with another number ``y``.  This avoids confusing results
+   when doing equality comparisons between numbers of different types.
 
    .. versionchanged:: 3.2
-      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``.
+      Mixed-type comparisons between :class:`Decimal` instances and
+      other numeric types are now fully supported.
 
    In addition to the standard numeric properties, decimal floating point
    objects also have a number of specialized methods:
index 29ce39838f399647ec2d92ecf4f5b1b263748ba7..0e6a966d0fd55d7fbc41d3165fafb5ef1a81700e 100644 (file)
@@ -862,7 +862,7 @@ class Decimal(object):
     # that specified by IEEE 754.
 
     def __eq__(self, other, context=None):
-        other = _convert_other(other, allow_float = True)
+        self, other = _convert_for_comparison(self, other, equality_op=True)
         if other is NotImplemented:
             return other
         if self._check_nans(other, context):
@@ -870,7 +870,7 @@ class Decimal(object):
         return self._cmp(other) == 0
 
     def __ne__(self, other, context=None):
-        other = _convert_other(other, allow_float = True)
+        self, other = _convert_for_comparison(self, other, equality_op=True)
         if other is NotImplemented:
             return other
         if self._check_nans(other, context):
@@ -879,7 +879,7 @@ class Decimal(object):
 
 
     def __lt__(self, other, context=None):
-        other = _convert_other(other, allow_float = True)
+        self, other = _convert_for_comparison(self, other)
         if other is NotImplemented:
             return other
         ans = self._compare_check_nans(other, context)
@@ -888,7 +888,7 @@ class Decimal(object):
         return self._cmp(other) < 0
 
     def __le__(self, other, context=None):
-        other = _convert_other(other, allow_float = True)
+        self, other = _convert_for_comparison(self, other)
         if other is NotImplemented:
             return other
         ans = self._compare_check_nans(other, context)
@@ -897,7 +897,7 @@ class Decimal(object):
         return self._cmp(other) <= 0
 
     def __gt__(self, other, context=None):
-        other = _convert_other(other, allow_float = True)
+        self, other = _convert_for_comparison(self, other)
         if other is NotImplemented:
             return other
         ans = self._compare_check_nans(other, context)
@@ -906,7 +906,7 @@ class Decimal(object):
         return self._cmp(other) > 0
 
     def __ge__(self, other, context=None):
-        other = _convert_other(other, allow_float = True)
+        self, other = _convert_for_comparison(self, other)
         if other is NotImplemented:
             return other
         ans = self._compare_check_nans(other, context)
@@ -5860,6 +5860,37 @@ def _convert_other(other, raiseit=False, allow_float=False):
         raise TypeError("Unable to convert %s to Decimal" % other)
     return NotImplemented
 
+def _convert_for_comparison(self, other, equality_op=False):
+    """Given a Decimal instance self and a Python object other, return
+    an pair (s, o) of Decimal instances such that "s op o" is
+    equivalent to "self op other" for any of the 6 comparison
+    operators "op".
+
+    """
+    if isinstance(other, Decimal):
+        return self, other
+
+    # Comparison with a Rational instance (also includes integers):
+    # self op n/d <=> self*d op n (for n and d integers, d positive).
+    # A NaN or infinity can be left unchanged without affecting the
+    # comparison result.
+    if isinstance(other, _numbers.Rational):
+        if not self._is_special:
+            self = _dec_from_triple(self._sign,
+                                    str(int(self._int) * other.denominator),
+                                    self._exp)
+        return self, Decimal(other.numerator)
+
+    # Comparisons with float and complex types.  == and != comparisons
+    # with complex numbers should succeed, returning either True or False
+    # as appropriate.  Other comparisons return NotImplemented.
+    if equality_op and isinstance(other, _numbers.Complex) and other.imag == 0:
+        other = other.real
+    if isinstance(other, float):
+        return self, Decimal.from_float(other)
+    return NotImplemented, NotImplemented
+
+
 ##### Setup Specific Contexts ############################################
 
 # The default context prototype used by Context()
index dd51f9b9e6b55fb1029e3715d736a55c05f3e433..a41fd9c37c26fb36737fb79952078dc7dcd214e7 100644 (file)
@@ -395,12 +395,11 @@ class FractionTest(unittest.TestCase):
         self.assertTypedEquals(1.0 + 0j, (1.0 + 0j) ** F(1, 10))
 
     def testMixingWithDecimal(self):
-        # Decimal refuses mixed comparisons.
+        # Decimal refuses mixed arithmetic (but not mixed comparisons)
         self.assertRaisesMessage(
             TypeError,
             "unsupported operand type(s) for +: 'Fraction' and 'Decimal'",
             operator.add, F(3,11), Decimal('3.1415926'))
-        self.assertNotEquals(F(5, 2), Decimal('2.5'))
 
     def testComparisons(self):
         self.assertTrue(F(1, 2) < F(2, 3))
index eafdb0f81feb92803c5ab7f7653d6f5bc46714e8..b0c953731e5cab52ba81af36c35c585d27e5d53a 100644 (file)
@@ -143,9 +143,64 @@ class HashTest(unittest.TestCase):
         x = {'halibut', HalibutProxy()}
         self.assertEqual(len(x), 1)
 
+class ComparisonTest(unittest.TestCase):
+    def test_mixed_comparisons(self):
+
+        # ordered list of distinct test values of various types:
+        # int, float, Fraction, Decimal
+        test_values = [
+            float('-inf'),
+            D('-1e999999999'),
+            -1e308,
+            F(-22, 7),
+            -3.14,
+            -2,
+            0.0,
+            1e-320,
+            True,
+            F('1.2'),
+            D('1.3'),
+            float('1.4'),
+            F(275807, 195025),
+            D('1.414213562373095048801688724'),
+            F(114243, 80782),
+            F(473596569, 84615),
+            7e200,
+            D('infinity'),
+            ]
+        for i, first in enumerate(test_values):
+            for second in test_values[i+1:]:
+                self.assertLess(first, second)
+                self.assertLessEqual(first, second)
+                self.assertGreater(second, first)
+                self.assertGreaterEqual(second, first)
+
+    def test_complex(self):
+        # comparisons with complex are special:  equality and inequality
+        # comparisons should always succeed, but order comparisons should
+        # raise TypeError.
+        z = 1.0 + 0j
+        w = -3.14 + 2.7j
+
+        for v in 1, 1.0, F(1), D(1), complex(1):
+            self.assertEqual(z, v)
+            self.assertEqual(v, z)
+
+        for v in 2, 2.0, F(2), D(2), complex(2):
+            self.assertNotEqual(z, v)
+            self.assertNotEqual(v, z)
+            self.assertNotEqual(w, v)
+            self.assertNotEqual(v, w)
+
+        for v in (1, 1.0, F(1), D(1), complex(1),
+                  2, 2.0, F(2), D(2), complex(2), w):
+            for op in operator.le, operator.lt, operator.ge, operator.gt:
+                self.assertRaises(TypeError, op, z, v)
+                self.assertRaises(TypeError, op, v, z)
+
 
 def test_main():
-    run_unittest(HashTest)
+    run_unittest(HashTest, ComparisonTest)
 
 if __name__ == '__main__':
     test_main()
index 24d1e7f008ee1c3d616db4491f851f98ebab0379..8b8636679edee97c1275e4ef366a245f1bf387cc 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -417,6 +417,13 @@ C-API
 Library
 -------
 
+- Issue #8118: Comparisons between Decimal and Fraction objects are
+  now permitted, returning a result based on the exact numerical
+  values of the operands.  This builds on issue #2531, which allowed
+  Decimal-to-float comparisons;  all comparisons involving numeric
+  types (bool, int, float, complex, Decimal, Fraction) should now
+  act as expected.
+
 - Issue #8897: Fix sunau module, use bytes to write the header. Patch written
   by Thomas Jollans.
 
@@ -714,7 +721,8 @@ 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).
+  the relative ordering of id(float) and id(Decimal).  See also
+  issue #8118, which adds Decimal-to-Fraction comparisons.
 
 - Added a subtract() method to collections.Counter().