]> granicus.if.org Git - python/commitdiff
Issue #1869 (and 4707, 5118, 5473, 1456775): use the new
authorMark Dickinson <dickinsm@gmail.com>
Sat, 18 Apr 2009 11:48:33 +0000 (11:48 +0000)
committerMark Dickinson <dickinsm@gmail.com>
Sat, 18 Apr 2009 11:48:33 +0000 (11:48 +0000)
string <-> float conversion routines to make round(x, n) correctly
rounded for floats x, so that it always agrees with format(x, '.<n>f').

Also fix some other round nuisances, like round(123.456, 1-2**31) giving
an integer rather than a float.

Lib/test/test_float.py
Misc/NEWS
Objects/floatobject.c

index 24202be7f62ebce145a3bfcc2c181bc3af371e85..91ed054987a4de500136156ab80a30b3295d363e 100644 (file)
@@ -389,6 +389,88 @@ class ReprTestCase(unittest.TestCase):
             self.assertEqual(s, repr(float(s)))
             self.assertEqual(negs, repr(float(negs)))
 
+class RoundTestCase(unittest.TestCase):
+    @unittest.skipUnless(float.__getformat__("double").startswith("IEEE"),
+                         "test requires IEEE 754 doubles")
+    def test_inf_nan(self):
+        self.assertRaises(OverflowError, round, INF)
+        self.assertRaises(OverflowError, round, -INF)
+        self.assertRaises(ValueError, round, NAN)
+
+    @unittest.skipUnless(float.__getformat__("double").startswith("IEEE"),
+                         "test requires IEEE 754 doubles")
+    def test_large_n(self):
+        for n in [324, 325, 400, 2**31-1, 2**31, 2**32, 2**100]:
+            self.assertEqual(round(123.456, n), 123.456)
+            self.assertEqual(round(-123.456, n), -123.456)
+            self.assertEqual(round(1e300, n), 1e300)
+            self.assertEqual(round(1e-320, n), 1e-320)
+        self.assertEqual(round(1e150, 300), 1e150)
+        self.assertEqual(round(1e300, 307), 1e300)
+        self.assertEqual(round(-3.1415, 308), -3.1415)
+        self.assertEqual(round(1e150, 309), 1e150)
+        self.assertEqual(round(1.4e-315, 315), 1e-315)
+
+    @unittest.skipUnless(float.__getformat__("double").startswith("IEEE"),
+                         "test requires IEEE 754 doubles")
+    def test_small_n(self):
+        for n in [-308, -309, -400, 1-2**31, -2**31, -2**31-1, -2**100]:
+            self.assertEqual(round(123.456, n), 0.0)
+            self.assertEqual(round(-123.456, n), -0.0)
+            self.assertEqual(round(1e300, n), 0.0)
+            self.assertEqual(round(1e-320, n), 0.0)
+
+    @unittest.skipUnless(float.__getformat__("double").startswith("IEEE"),
+                         "test requires IEEE 754 doubles")
+    def test_overflow(self):
+        self.assertRaises(OverflowError, round, 1.6e308, -308)
+        self.assertRaises(OverflowError, round, -1.7e308, -308)
+
+    @unittest.skipUnless(getattr(sys, 'float_repr_style', '') == 'short',
+                         "applies only when using short float repr style")
+    def test_previous_round_bugs(self):
+        # particular cases that have occurred in bug reports
+        self.assertEqual(round(562949953421312.5, 1),
+                          562949953421312.5)
+        self.assertEqual(round(56294995342131.5, 3),
+                         56294995342131.5)
+        # round-half-even
+        self.assertEqual(round(25.0, -1), 20.0)
+        self.assertEqual(round(35.0, -1), 40.0)
+        self.assertEqual(round(45.0, -1), 40.0)
+        self.assertEqual(round(55.0, -1), 60.0)
+        self.assertEqual(round(65.0, -1), 60.0)
+        self.assertEqual(round(75.0, -1), 80.0)
+        self.assertEqual(round(85.0, -1), 80.0)
+        self.assertEqual(round(95.0, -1), 100.0)
+
+    @unittest.skipUnless(getattr(sys, 'float_repr_style', '') == 'short',
+                         "applies only when using short float repr style")
+    def test_matches_float_format(self):
+        # round should give the same results as float formatting
+        for i in range(500):
+            x = i/1000.
+            self.assertEqual(float(format(x, '.0f')), round(x, 0))
+            self.assertEqual(float(format(x, '.1f')), round(x, 1))
+            self.assertEqual(float(format(x, '.2f')), round(x, 2))
+            self.assertEqual(float(format(x, '.3f')), round(x, 3))
+
+        for i in range(5, 5000, 10):
+            x = i/1000.
+            self.assertEqual(float(format(x, '.0f')), round(x, 0))
+            self.assertEqual(float(format(x, '.1f')), round(x, 1))
+            self.assertEqual(float(format(x, '.2f')), round(x, 2))
+            self.assertEqual(float(format(x, '.3f')), round(x, 3))
+
+        for i in range(500):
+            x = random.random()
+            self.assertEqual(float(format(x, '.0f')), round(x, 0))
+            self.assertEqual(float(format(x, '.1f')), round(x, 1))
+            self.assertEqual(float(format(x, '.2f')), round(x, 2))
+            self.assertEqual(float(format(x, '.3f')), round(x, 3))
+
+
+
 # Beginning with Python 2.6 float has cross platform compatible
 # ways to create and represent inf and nan
 class InfNanTest(unittest.TestCase):
@@ -878,6 +960,7 @@ def test_main():
         IEEEFormatTestCase,
         FormatTestCase,
         ReprTestCase,
+        RoundTestCase,
         InfNanTest,
         HexFloatTestCase,
         )
index f0ed225cb64091421047ede67dd9827afe6dd40e..295ebd013906d460042e982d976d0127488d7436 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -12,6 +12,11 @@ What's New in Python 3.1 beta 1?
 Core and Builtins
 -----------------
 
+- Issue #1869 (and many duplicates): make round(x, n) correctly
+  rounded for a float x, by using the decimal <-> binary conversions
+  from Python/dtoa.c.  As a consequence, (e.g.) round(x, 2) now
+  consistently agrees with format(x, '.2f').
+
 - Issue #5772: format(1e100, '<') produces '1e+100', not '1.0e+100'.
 
 - Issue #5515: str.format() type 'n' combined with commas and leading
index 2fbe810f15b0d3dfe704cea94428d00ce6d1cc65..b7b52207e36b67ee4a2523b4c88009d65c7572f7 100644 (file)
@@ -899,43 +899,161 @@ float_trunc(PyObject *v)
        return PyLong_FromDouble(wholepart);
 }
 
+/* double_round: rounds a finite double to the closest multiple of
+   10**-ndigits; here ndigits is within reasonable bounds (typically, -308 <=
+   ndigits <= 323).  Returns a Python float, or sets a Python error and
+   returns NULL on failure (OverflowError and memory errors are possible). */
+
+#ifndef PY_NO_SHORT_FLOAT_REPR
+/* version of double_round that uses the correctly-rounded string<->double
+   conversions from Python/dtoa.c */
+
 static PyObject *
-float_round(PyObject *v, PyObject *args)
-{
-#define UNDEF_NDIGITS (-0x7fffffff) /* Unlikely ndigits value */
-       double x;
-       double f = 1.0;
-       double flr, cil;
+double_round(double x, int ndigits) {
+
        double rounded;
-       int ndigits = UNDEF_NDIGITS;
+       Py_ssize_t buflen, mybuflen=100;
+       char *buf, *buf_end, shortbuf[100], *mybuf=shortbuf;
+       int decpt, sign;
+       PyObject *result = NULL;
 
-       if (!PyArg_ParseTuple(args, "|i", &ndigits))
+       /* round to a decimal string */
+       buf = _Py_dg_dtoa(x, 3, ndigits, &decpt, &sign, &buf_end);
+       if (buf == NULL) {
+               PyErr_NoMemory();
                return NULL;
+       }
 
-       x = PyFloat_AsDouble(v);
+       /* Get new buffer if shortbuf is too small.  Space needed <= buf_end -
+       buf + 8: (1 extra for '0', 1 for sign, 5 for exp, 1 for '\0').  */
+       buflen = buf_end - buf;
+       if (buflen + 8 > mybuflen) {
+               mybuflen = buflen+8;
+               mybuf = (char *)PyMem_Malloc(mybuflen);
+               if (mybuf == NULL) {
+                       PyErr_NoMemory();
+                       goto exit;
+               }
+       }
+       /* copy buf to mybuf, adding exponent, sign and leading 0 */
+       PyOS_snprintf(mybuf, mybuflen, "%s0%se%d", (sign ? "-" : ""),
+                     buf, decpt - (int)buflen);
 
-       if (ndigits != UNDEF_NDIGITS) {
-               f = pow(10.0, ndigits);
-               x *= f;
+       /* and convert the resulting string back to a double */
+       errno = 0;
+       rounded = _Py_dg_strtod(mybuf, NULL);
+       if (errno == ERANGE && fabs(rounded) >= 1.)
+               PyErr_SetString(PyExc_OverflowError,
+                               "rounded value too large to represent");
+       else
+               result = PyFloat_FromDouble(rounded);
+
+       /* done computing value;  now clean up */
+       if (mybuf != shortbuf)
+               PyMem_Free(mybuf);
+  exit:
+       _Py_dg_freedtoa(buf);
+       return result;
+}
+
+#else /* PY_NO_SHORT_FLOAT_REPR */
+
+/* fallback version, to be used when correctly rounded binary<->decimal
+   conversions aren't available */
+
+static PyObject *
+double_round(double x, int ndigits) {
+       double pow1, pow2, y, z;
+       if (ndigits >= 0) {
+               if (ndigits > 22) {
+                       /* pow1 and pow2 are each safe from overflow, but
+                          pow1*pow2 ~= pow(10.0, ndigits) might overflow */
+                       pow1 = pow(10.0, (double)(ndigits-22));
+                       pow2 = 1e22;
+               }
+               else {
+                       pow1 = pow(10.0, (double)ndigits);
+                       pow2 = 1.0;
+               }
+               y = (x*pow1)*pow2;
+               /* if y overflows, then rounded value is exactly x */
+               if (!Py_IS_FINITE(y))
+                       return PyFloat_FromDouble(x);
+       }
+       else {
+               pow1 = pow(10.0, (double)-ndigits);
+               pow2 = 1.0; /* unused; silences a gcc compiler warning */
+               y = x / pow1;
        }
 
-       flr = floor(x);
-       cil = ceil(x);
+       z = round(y);
+       if (fabs(y-z) == 0.5)
+               /* halfway between two integers; use round-half-even */
+               z = 2.0*round(y/2.0);
 
-       if (x-flr > 0.5)
-               rounded = cil;
-       else if (x-flr == 0.5)
-               rounded = fmod(flr, 2) == 0 ? flr : cil;
+       if (ndigits >= 0)
+               z = (z / pow2) / pow1;
        else
-               rounded = flr;
+               z *= pow1;
 
-       if (ndigits != UNDEF_NDIGITS) {
-               rounded /= f;
-               return PyFloat_FromDouble(rounded);
+       /* if computation resulted in overflow, raise OverflowError */
+       if (!Py_IS_FINITE(z)) {
+               PyErr_SetString(PyExc_OverflowError,
+                               "overflow occurred during round");
+               return NULL;
        }
 
-       return PyLong_FromDouble(rounded);
-#undef UNDEF_NDIGITS
+       return PyFloat_FromDouble(z);
+}
+
+#endif /* PY_NO_SHORT_FLOAT_REPR */
+
+/* round a Python float v to the closest multiple of 10**-ndigits */
+
+static PyObject *
+float_round(PyObject *v, PyObject *args)
+{
+       double x, rounded;
+       PyObject *o_ndigits = NULL;
+       Py_ssize_t ndigits;
+
+       x = PyFloat_AsDouble(v);
+       if (!PyArg_ParseTuple(args, "|O", &o_ndigits))
+               return NULL;
+       if (o_ndigits == NULL) {
+               /* single-argument round: round to nearest integer */
+               rounded = round(x);
+               if (fabs(x-rounded) == 0.5)
+                       /* halfway case: round to even */
+                       rounded = 2.0*round(x/2.0);
+               return PyLong_FromDouble(rounded);
+       }
+
+       /* interpret second argument as a Py_ssize_t; clips on overflow */
+       ndigits = PyNumber_AsSsize_t(o_ndigits, NULL);
+       if (ndigits == -1 && PyErr_Occurred())
+               return NULL;
+
+       /* nans and infinities round to themselves */
+       if (!Py_IS_FINITE(x))
+               return PyFloat_FromDouble(x);
+
+       /* Deal with extreme values for ndigits. For ndigits > NDIGITS_MAX, x
+          always rounds to itself.  For ndigits < NDIGITS_MIN, x always
+          rounds to +-0.0.  Here 0.30103 is an upper bound for log10(2). */
+#define NDIGITS_MAX ((int)((DBL_MANT_DIG-DBL_MIN_EXP) * 0.30103))
+#define NDIGITS_MIN (-(int)((DBL_MAX_EXP + 1) * 0.30103))
+       if (ndigits > NDIGITS_MAX)
+               /* return x */
+               return PyFloat_FromDouble(x);
+       else if (ndigits < NDIGITS_MIN)
+               /* return 0.0, but with sign of x */
+               return PyFloat_FromDouble(0.0*x);
+       else
+               /* finite x, and ndigits is not unreasonably large */
+               return double_round(x, (int)ndigits);
+#undef NDIGITS_MAX
+#undef NDIGITS_MIN
 }
 
 static PyObject *