mirror of
https://github.com/python/cpython.git
synced 2025-09-19 07:00:59 +00:00
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.
This commit is contained in:
parent
8c1a50abd3
commit
85424c9354
3 changed files with 162 additions and 30 deletions
|
@ -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 @@
|
||||||
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
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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
|
||||||
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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue