summaryrefslogtreecommitdiffstats
path: root/Objects
diff options
context:
space:
mode:
authorMark Dickinson <dickinsm@gmail.com>2009-11-18 19:33:35 (GMT)
committerMark Dickinson <dickinsm@gmail.com>2009-11-18 19:33:35 (GMT)
commitbd15a06fd3ac256d4f2780c85a9f7e6def1ecd1f (patch)
treed6f4201cd2881e1b646906b5923c51455d96be73 /Objects
parent0516f8138643ca49a6e5fd56e0aa546829465a37 (diff)
downloadcpython-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.c196
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)
{