diff --git a/Lib/test/test_float.py b/Lib/test/test_float.py index d9cac74c4f0..eb119ce6f0c 100644 --- a/Lib/test/test_float.py +++ b/Lib/test/test_float.py @@ -52,6 +52,48 @@ class GeneralFloatCases(unittest.TestCase): float('.' + '1'*1000) float(unicode('.' + '1'*1000)) + def check_conversion_to_int(self, x): + """Check that int(x) has the correct value and type, for a float x.""" + n = int(x) + if x >= 0.0: + # x >= 0 and n = int(x) ==> n <= x < n + 1 + self.assertLessEqual(n, x) + self.assertLess(x, n + 1) + else: + # x < 0 and n = int(x) ==> n >= x > n - 1 + self.assertGreaterEqual(n, x) + self.assertGreater(x, n - 1) + + # Result should be an int if within range, else a long. + if -sys.maxint-1 <= n <= sys.maxint: + self.assertEqual(type(n), int) + else: + self.assertEqual(type(n), long) + + # Double check. + self.assertEqual(type(int(n)), type(n)) + + def test_conversion_to_int(self): + # Check that floats within the range of an int convert to type + # int, not long. (issue #11144.) + boundary = float(sys.maxint + 1) + epsilon = 2**-sys.float_info.mant_dig * boundary + + # These 2 floats are either side of the positive int/long boundary on + # both 32-bit and 64-bit systems. + self.check_conversion_to_int(boundary - epsilon) + self.check_conversion_to_int(boundary) + + # These floats are either side of the negative long/int boundary on + # 64-bit systems... + self.check_conversion_to_int(-boundary - 2*epsilon) + self.check_conversion_to_int(-boundary) + + # ... and these ones are either side of the negative long/int + # boundary on 32-bit systems. + self.check_conversion_to_int(-boundary - 1.0) + self.check_conversion_to_int(-boundary - 1.0 + 2*epsilon) + @test_support.run_with_locale('LC_NUMERIC', 'fr_FR', 'de_DE') def test_float_with_comma(self): # set locale to something that doesn't use '.' for the decimal point diff --git a/Misc/NEWS b/Misc/NEWS index d435890033f..d6436ce4f69 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -9,6 +9,10 @@ What's New in Python 2.7.2? Core and Builtins ----------------- +- Issue #11144: Ensure that int(a_float) returns an int whenever possible. + Previously, there were some corner cases where a long was returned even + though the result was within the range of an int. + - Issue #11675: multiprocessing.[Raw]Array objects created from an integer size are now zeroed on creation. This matches the behaviour specified by the documentation. diff --git a/Objects/floatobject.c b/Objects/floatobject.c index 77b10051a7a..63b7c17b6ba 100644 --- a/Objects/floatobject.c +++ b/Objects/floatobject.c @@ -1035,14 +1035,17 @@ float_trunc(PyObject *v) * happens if the double is too big to fit in a long. Some rare * systems raise an exception then (RISCOS was mentioned as one, * and someone using a non-default option on Sun also bumped into - * that). Note that checking for >= and <= LONG_{MIN,MAX} would - * still be vulnerable: if a long has more bits of precision than - * a double, casting MIN/MAX to double may yield an approximation, - * and if that's rounded up, then, e.g., wholepart=LONG_MAX+1 would - * yield true from the C expression wholepart<=LONG_MAX, despite - * that wholepart is actually greater than LONG_MAX. + * that). Note that checking for <= LONG_MAX is unsafe: if a long + * has more bits of precision than a double, casting LONG_MAX to + * double may yield an approximation, and if that's rounded up, + * then, e.g., wholepart=LONG_MAX+1 would yield true from the C + * expression wholepart<=LONG_MAX, despite that wholepart is + * actually greater than LONG_MAX. However, assuming a two's complement + * machine with no trap representation, LONG_MIN will be a power of 2 (and + * hence exactly representable as a double), and LONG_MAX = -1-LONG_MIN, so + * the comparisons with (double)LONG_MIN below should be safe. */ - if (LONG_MIN < wholepart && wholepart < LONG_MAX) { + if ((double)LONG_MIN <= wholepart && wholepart < -(double)LONG_MIN) { const long aslong = (long)wholepart; return PyInt_FromLong(aslong); }