diff options
author | Mark Dickinson <dickinsm@gmail.com> | 2009-11-18 19:33:35 (GMT) |
---|---|---|
committer | Mark Dickinson <dickinsm@gmail.com> | 2009-11-18 19:33:35 (GMT) |
commit | bd15a06fd3ac256d4f2780c85a9f7e6def1ecd1f (patch) | |
tree | d6f4201cd2881e1b646906b5923c51455d96be73 /Objects | |
parent | 0516f8138643ca49a6e5fd56e0aa546829465a37 (diff) | |
download | cpython-bd15a06fd3ac256d4f2780c85a9f7e6def1ecd1f.zip cpython-bd15a06fd3ac256d4f2780c85a9f7e6def1ecd1f.tar.gz cpython-bd15a06fd3ac256d4f2780c85a9f7e6def1ecd1f.tar.bz2 |
Issue #7117, continued: Change round implementation to use the correctly-rounded
string <-> float conversions; this makes sure that the result of the round
operation is correctly rounded, and hence displays nicely using the new float
repr.
Diffstat (limited to 'Objects')
-rw-r--r-- | Objects/floatobject.c | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/Objects/floatobject.c b/Objects/floatobject.c index 28b2004..461029a 100644 --- a/Objects/floatobject.c +++ b/Objects/floatobject.c @@ -999,6 +999,202 @@ float_long(PyObject *v) return PyLong_FromDouble(x); } +/* _Py_double_round: rounds a finite nonzero 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 _Py_double_round that uses the correctly-rounded string<->double + conversions from Python/dtoa.c */ + +/* FIVE_POW_LIMIT is the largest k such that 5**k is exactly representable as + a double. Since we're using the code in Python/dtoa.c, it should be safe + to assume that C doubles are IEEE 754 binary64 format. To be on the safe + side, we check this. */ +#if DBL_MANT_DIG == 53 +#define FIVE_POW_LIMIT 22 +#else +#error "C doubles do not appear to be IEEE 754 binary64 format" +#endif + +PyObject * +_Py_double_round(double x, int ndigits) { + + double rounded, m; + Py_ssize_t buflen, mybuflen=100; + char *buf, *buf_end, shortbuf[100], *mybuf=shortbuf; + int decpt, sign, val, halfway_case; + PyObject *result = NULL; + + /* The basic idea is very simple: convert and round the double to a + decimal string using _Py_dg_dtoa, then convert that decimal string + back to a double with _Py_dg_strtod. There's one minor difficulty: + Python 2.x expects round to do round-half-away-from-zero, while + _Py_dg_dtoa does round-half-to-even. So we need some way to detect + and correct the halfway cases. + + Detection: a halfway value has the form k * 0.5 * 10**-ndigits for + some odd integer k. Or in other words, a rational number x is + exactly halfway between two multiples of 10**-ndigits if its + 2-valuation is exactly -ndigits-1 and its 5-valuation is at least + -ndigits. For ndigits >= 0 the latter condition is automatically + satisfied for a binary float x, since any such float has + nonnegative 5-valuation. For 0 > ndigits >= -22, x needs to be an + integral multiple of 5**-ndigits; we can check this using fmod. + For -22 > ndigits, there are no halfway cases: 5**23 takes 54 bits + to represent exactly, so any odd multiple of 0.5 * 10**n for n >= + 23 takes at least 54 bits of precision to represent exactly. + + Correction: a simple strategy for dealing with halfway cases is to + (for the halfway cases only) call _Py_dg_dtoa with an argument of + ndigits+1 instead of ndigits (thus doing an exact conversion to + decimal), round the resulting string manually, and then convert + back using _Py_dg_strtod. + */ + + /* nans, infinities and zeros should have already been dealt + with by the caller (in this case, builtin_round) */ + assert(Py_IS_FINITE(x) && x != 0.0); + + /* find 2-valuation val of x */ + m = frexp(x, &val); + while (m != floor(m)) { + m *= 2.0; + val--; + } + + /* determine whether this is a halfway case */ + if (val == -ndigits-1) { + if (ndigits >= 0) + halfway_case = 1; + else if (ndigits >= -FIVE_POW_LIMIT) { + double five_pow = 1.0; + int i; + for (i=0; i < -ndigits; i++) + five_pow *= 5.0; + halfway_case = fmod(x, five_pow) == 0.0; + } + else + halfway_case = 0; + } + else + halfway_case = 0; + + /* round to a decimal string; use an extra place for halfway case */ + buf = _Py_dg_dtoa(x, 3, ndigits+halfway_case, &decpt, &sign, &buf_end); + if (buf == NULL) { + PyErr_NoMemory(); + return NULL; + } + buflen = buf_end - buf; + + /* in halfway case, do the round-half-away-from-zero manually */ + if (halfway_case) { + int i, carry; + /* sanity check: _Py_dg_dtoa should not have stripped + any zeros from the result: there should be exactly + ndigits+1 places following the decimal point, and + the last digit in the buffer should be a '5'.*/ + assert(buflen - decpt == ndigits+1); + assert(buf[buflen-1] == '5'); + + /* increment and shift right at the same time. */ + decpt += 1; + carry = 1; + for (i=buflen-1; i-- > 0;) { + carry += buf[i] - '0'; + buf[i+1] = carry % 10 + '0'; + carry /= 10; + } + buf[0] = carry + '0'; + } + + /* 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'). */ + 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); + + /* 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; +} + +#undef FIVE_POW_LIMIT + +#else /* PY_NO_SHORT_FLOAT_REPR */ + +/* fallback version, to be used when correctly rounded binary<->decimal + conversions aren't available */ + +PyObject * +_Py_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; + } + + z = round(y); + if (fabs(y-z) == 0.5) + /* halfway between two integers; use round-away-from-zero */ + z = y + copysign(0.5, y); + + if (ndigits >= 0) + z = (z / pow2) / pow1; + else + z *= pow1; + + /* if computation resulted in overflow, raise OverflowError */ + if (!Py_IS_FINITE(z)) { + PyErr_SetString(PyExc_OverflowError, + "overflow occurred during round"); + return NULL; + } + + return PyFloat_FromDouble(z); +} + +#endif /* PY_NO_SHORT_FLOAT_REPR */ + static PyObject * float_float(PyObject *v) { |