Issue #6431: Fix Fraction comparisons with unknown types, and with

float infinities and nans.  Backport of r74078 from py3k.
This commit is contained in:
Mark Dickinson 2009-07-18 15:18:18 +00:00
parent 3bb474714b
commit 88a0a2e47f
3 changed files with 162 additions and 30 deletions

View File

@ -486,54 +486,56 @@ class Fraction(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):
return a == a.from_float(b) 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)
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, 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, 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 __nonzero__(a): def __nonzero__(a):
"""a != 0""" """a != 0"""

View File

@ -3,6 +3,7 @@
from decimal import Decimal from decimal import Decimal
from test.test_support import run_unittest from test.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 cPickle 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):
@ -311,6 +375,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))
@ -322,6 +430,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))
@ -332,6 +447,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))
@ -356,6 +478,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)))

View File

@ -352,6 +352,10 @@ Core and Builtins
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 #6466: now distutils.cygwinccompiler and distutils.emxccompiler - Issue #6466: now distutils.cygwinccompiler and distutils.emxccompiler