Skip to content

Commit

Permalink
Issue #1869 (and 4707, 5118, 5473, 1456775): use the new
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mdickinson committed Apr 18, 2009
1 parent 60fd099 commit e6a076d
Show file tree
Hide file tree
Showing 3 changed files with 230 additions and 24 deletions.
83 changes: 83 additions & 0 deletions Lib/test/test_float.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,88 @@ def test_short_repr(self):
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):
Expand Down Expand Up @@ -878,6 +960,7 @@ def test_main():
IEEEFormatTestCase,
FormatTestCase,
ReprTestCase,
RoundTestCase,
InfNanTest,
HexFloatTestCase,
)
Expand Down
5 changes: 5 additions & 0 deletions Misc/NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
166 changes: 142 additions & 24 deletions Objects/floatobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
Expand Down

0 comments on commit e6a076d

Please sign in to comment.