Commit 85424c93 authored by Mark Dickinson's avatar Mark Dickinson

Issue #6431: Fix Fraction comparisons to return NotImplemented when

the Fraction type doesn't know how to handle the comparison without
loss of accuracy.  Also, make sure that comparisons between Fractions
and float infinities or nans do the right thing.
parent 8c1a50ab
...@@ -501,54 +501,56 @@ class Fraction(numbers.Rational): ...@@ -501,54 +501,56 @@ class Fraction(numbers.Rational):
if isinstance(b, numbers.Complex) and b.imag == 0: if isinstance(b, numbers.Complex) and b.imag == 0:
b = b.real b = b.real
if isinstance(b, float): if isinstance(b, float):
if math.isnan(b) or math.isinf(b):
# comparisons with an infinity or nan should behave in
# the same way for any finite a, so treat a as zero.
return 0.0 == b
else:
return a == a.from_float(b) return a == a.from_float(b)
else: else:
# XXX: If b.__eq__ is implemented like this method, it may # Since a doesn't know how to compare with b, let's give b
# give the wrong answer after float(a) changes a's # a chance to compare itself with a.
# value. Better ways of doing this are welcome. return NotImplemented
return float(a) == b
def _subtractAndCompareToZero(a, b, op): def _richcmp(self, other, op):
"""Helper function for comparison operators. """Helper for comparison operators, for internal use only.
Subtracts b from a, exactly if possible, and compares the Implement comparison between a Rational instance `self`, and
result with 0 using op, in such a way that the comparison either another Rational instance or a float `other`. If
won't recurse. If the difference raises a TypeError, returns `other` is not a Rational instance or a float, return
NotImplemented instead. NotImplemented. `op` should be one of the six standard
comparison operators.
""" """
if isinstance(b, numbers.Complex) and b.imag == 0: # convert other to a Rational instance where reasonable.
b = b.real if isinstance(other, numbers.Rational):
if isinstance(b, float): return op(self._numerator * other.denominator,
b = a.from_float(b) self._denominator * other.numerator)
try: if isinstance(other, numbers.Complex) and other.imag == 0:
# XXX: If b <: Real but not <: Rational, this is likely other = other.real
# to fall back to a float. If the actual values differ by if isinstance(other, float):
# less than MIN_FLOAT, this could falsely call them equal, if math.isnan(other) or math.isinf(other):
# which would make <= inconsistent with ==. Better ways of return op(0.0, other)
# doing this are welcome. else:
diff = a - b return op(self, self.from_float(other))
except TypeError: else:
return NotImplemented return NotImplemented
if isinstance(diff, numbers.Rational):
return op(diff.numerator, 0)
return op(diff, 0)
def __lt__(a, b): def __lt__(a, b):
"""a < b""" """a < b"""
return a._subtractAndCompareToZero(b, operator.lt) return a._richcmp(b, operator.lt)
def __gt__(a, b): def __gt__(a, b):
"""a > b""" """a > b"""
return a._subtractAndCompareToZero(b, operator.gt) return a._richcmp(b, operator.gt)
def __le__(a, b): def __le__(a, b):
"""a <= b""" """a <= b"""
return a._subtractAndCompareToZero(b, operator.le) return a._richcmp(b, operator.le)
def __ge__(a, b): def __ge__(a, b):
"""a >= b""" """a >= b"""
return a._subtractAndCompareToZero(b, operator.ge) return a._richcmp(b, operator.ge)
def __bool__(a): def __bool__(a):
"""a != 0""" """a != 0"""
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
from decimal import Decimal from decimal import Decimal
from test.support import run_unittest from test.support import run_unittest
import math import math
import numbers
import operator import operator
import fractions import fractions
import unittest import unittest
...@@ -11,6 +12,69 @@ from pickle import dumps, loads ...@@ -11,6 +12,69 @@ from pickle import dumps, loads
F = fractions.Fraction F = fractions.Fraction
gcd = fractions.gcd gcd = fractions.gcd
class DummyFloat(object):
"""Dummy float class for testing comparisons with Fractions"""
def __init__(self, value):
if not isinstance(value, float):
raise TypeError("DummyFloat can only be initialized from float")
self.value = value
def _richcmp(self, other, op):
if isinstance(other, numbers.Rational):
return op(F.from_float(self.value), other)
elif isinstance(other, DummyFloat):
return op(self.value, other.value)
else:
return NotImplemented
def __eq__(self, other): return self._richcmp(other, operator.eq)
def __le__(self, other): return self._richcmp(other, operator.le)
def __lt__(self, other): return self._richcmp(other, operator.lt)
def __ge__(self, other): return self._richcmp(other, operator.ge)
def __gt__(self, other): return self._richcmp(other, operator.gt)
# shouldn't be calling __float__ at all when doing comparisons
def __float__(self):
assert False, "__float__ should not be invoked for comparisons"
# same goes for subtraction
def __sub__(self, other):
assert False, "__sub__ should not be invoked for comparisons"
__rsub__ = __sub__
class DummyRational(object):
"""Test comparison of Fraction with a naive rational implementation."""
def __init__(self, num, den):
g = gcd(num, den)
self.num = num // g
self.den = den // g
def __eq__(self, other):
if isinstance(other, fractions.Fraction):
return (self.num == other._numerator and
self.den == other._denominator)
else:
return NotImplemented
def __lt__(self, other):
return(self.num * other._denominator < self.den * other._numerator)
def __gt__(self, other):
return(self.num * other._denominator > self.den * other._numerator)
def __le__(self, other):
return(self.num * other._denominator <= self.den * other._numerator)
def __ge__(self, other):
return(self.num * other._denominator >= self.den * other._numerator)
# this class is for testing comparisons; conversion to float
# should never be used for a comparison, since it loses accuracy
def __float__(self):
assert False, "__float__ should not be invoked"
class GcdTest(unittest.TestCase): class GcdTest(unittest.TestCase):
...@@ -324,6 +388,50 @@ class FractionTest(unittest.TestCase): ...@@ -324,6 +388,50 @@ class FractionTest(unittest.TestCase):
self.assertFalse(F(1, 2) != F(1, 2)) self.assertFalse(F(1, 2) != F(1, 2))
self.assertTrue(F(1, 2) != F(1, 3)) self.assertTrue(F(1, 2) != F(1, 3))
def testComparisonsDummyRational(self):
self.assertTrue(F(1, 2) == DummyRational(1, 2))
self.assertTrue(DummyRational(1, 2) == F(1, 2))
self.assertFalse(F(1, 2) == DummyRational(3, 4))
self.assertFalse(DummyRational(3, 4) == F(1, 2))
self.assertTrue(F(1, 2) < DummyRational(3, 4))
self.assertFalse(F(1, 2) < DummyRational(1, 2))
self.assertFalse(F(1, 2) < DummyRational(1, 7))
self.assertFalse(F(1, 2) > DummyRational(3, 4))
self.assertFalse(F(1, 2) > DummyRational(1, 2))
self.assertTrue(F(1, 2) > DummyRational(1, 7))
self.assertTrue(F(1, 2) <= DummyRational(3, 4))
self.assertTrue(F(1, 2) <= DummyRational(1, 2))
self.assertFalse(F(1, 2) <= DummyRational(1, 7))
self.assertFalse(F(1, 2) >= DummyRational(3, 4))
self.assertTrue(F(1, 2) >= DummyRational(1, 2))
self.assertTrue(F(1, 2) >= DummyRational(1, 7))
self.assertTrue(DummyRational(1, 2) < F(3, 4))
self.assertFalse(DummyRational(1, 2) < F(1, 2))
self.assertFalse(DummyRational(1, 2) < F(1, 7))
self.assertFalse(DummyRational(1, 2) > F(3, 4))
self.assertFalse(DummyRational(1, 2) > F(1, 2))
self.assertTrue(DummyRational(1, 2) > F(1, 7))
self.assertTrue(DummyRational(1, 2) <= F(3, 4))
self.assertTrue(DummyRational(1, 2) <= F(1, 2))
self.assertFalse(DummyRational(1, 2) <= F(1, 7))
self.assertFalse(DummyRational(1, 2) >= F(3, 4))
self.assertTrue(DummyRational(1, 2) >= F(1, 2))
self.assertTrue(DummyRational(1, 2) >= F(1, 7))
def testComparisonsDummyFloat(self):
x = DummyFloat(1./3.)
y = F(1, 3)
self.assertTrue(x != y)
self.assertTrue(x < y or x > y)
self.assertFalse(x == y)
self.assertFalse(x <= y and x >= y)
self.assertTrue(y != x)
self.assertTrue(y < x or y > x)
self.assertFalse(y == x)
self.assertFalse(y <= x and y >= x)
def testMixedLess(self): def testMixedLess(self):
self.assertTrue(2 < F(5, 2)) self.assertTrue(2 < F(5, 2))
self.assertFalse(2 < F(4, 2)) self.assertFalse(2 < F(4, 2))
...@@ -335,6 +443,13 @@ class FractionTest(unittest.TestCase): ...@@ -335,6 +443,13 @@ class FractionTest(unittest.TestCase):
self.assertTrue(0.4 < F(1, 2)) self.assertTrue(0.4 < F(1, 2))
self.assertFalse(0.5 < F(1, 2)) self.assertFalse(0.5 < F(1, 2))
self.assertFalse(float('inf') < F(1, 2))
self.assertTrue(float('-inf') < F(0, 10))
self.assertFalse(float('nan') < F(-3, 7))
self.assertTrue(F(1, 2) < float('inf'))
self.assertFalse(F(17, 12) < float('-inf'))
self.assertFalse(F(144, -89) < float('nan'))
def testMixedLessEqual(self): def testMixedLessEqual(self):
self.assertTrue(0.5 <= F(1, 2)) self.assertTrue(0.5 <= F(1, 2))
self.assertFalse(0.6 <= F(1, 2)) self.assertFalse(0.6 <= F(1, 2))
...@@ -345,6 +460,13 @@ class FractionTest(unittest.TestCase): ...@@ -345,6 +460,13 @@ class FractionTest(unittest.TestCase):
self.assertTrue(F(4, 2) <= 2) self.assertTrue(F(4, 2) <= 2)
self.assertFalse(F(5, 2) <= 2) self.assertFalse(F(5, 2) <= 2)
self.assertFalse(float('inf') <= F(1, 2))
self.assertTrue(float('-inf') <= F(0, 10))
self.assertFalse(float('nan') <= F(-3, 7))
self.assertTrue(F(1, 2) <= float('inf'))
self.assertFalse(F(17, 12) <= float('-inf'))
self.assertFalse(F(144, -89) <= float('nan'))
def testBigFloatComparisons(self): def testBigFloatComparisons(self):
# Because 10**23 can't be represented exactly as a float: # Because 10**23 can't be represented exactly as a float:
self.assertFalse(F(10**23) == float(10**23)) self.assertFalse(F(10**23) == float(10**23))
...@@ -369,6 +491,10 @@ class FractionTest(unittest.TestCase): ...@@ -369,6 +491,10 @@ class FractionTest(unittest.TestCase):
self.assertFalse(2 == F(3, 2)) self.assertFalse(2 == F(3, 2))
self.assertTrue(F(4, 2) == 2) self.assertTrue(F(4, 2) == 2)
self.assertFalse(F(5, 2) == 2) self.assertFalse(F(5, 2) == 2)
self.assertFalse(F(5, 2) == float('nan'))
self.assertFalse(float('nan') == F(3, 7))
self.assertFalse(F(5, 2) == float('inf'))
self.assertFalse(float('-inf') == F(2, 5))
def testStringification(self): def testStringification(self):
self.assertEquals("Fraction(7, 3)", repr(F(7, 3))) self.assertEquals("Fraction(7, 3)", repr(F(7, 3)))
......
...@@ -40,6 +40,10 @@ C-API ...@@ -40,6 +40,10 @@ C-API
Library Library
------- -------
- Issue #6431: Make Fraction type return NotImplemented when it doesn't
know how to handle a comparison without loss of precision. Also add
correct handling of infinities and nans for comparisons with float.
- Issue #6415: Fixed warnings.warn sagfault on bad formatted string. - Issue #6415: Fixed warnings.warn sagfault on bad formatted string.
- Issue #6358: The exit status of a command started with os.popen() was - Issue #6358: The exit status of a command started with os.popen() was
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment