Skip to content

Commit af11762

Browse files
committed
py/formatfloat: Back off float comparisons by 2 eps to avoid rounding errors.
1 parent 9a929b2 commit af11762

File tree

2 files changed

+28
-9
lines changed

2 files changed

+28
-9
lines changed

py/formatfloat.c

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,22 @@ static inline int fp_isless1(float x) {
9696
#define fp_iszero(x) (x == 0)
9797
#define fp_isless1(x) (x < 1.0)
9898

99+
union floatbits {
100+
double f;
101+
uint64_t u;
102+
};
103+
99104
#endif // MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_FLOAT/DOUBLE
100105

106+
static inline int fp_ge_eps(FPTYPE x, FPTYPE y) {
107+
union floatbits fb_y = {y};
108+
// Back off 2 eps.
109+
// This is valid for almost all values, but in practice
110+
// it's only used when y = 1eX for X>=0.
111+
fb_y.u -= 2;
112+
return (x >= fb_y.f);
113+
}
114+
101115
static const FPTYPE g_pos_pow[] = {
102116
#if FPDECEXP > 32
103117
MICROPY_FLOAT_CONST(1e256), MICROPY_FLOAT_CONST(1e128), MICROPY_FLOAT_CONST(1e64),
@@ -254,14 +268,14 @@ int mp_format_float(FPTYPE f, char *buf, size_t buf_size, char fmt, int prec, ch
254268
// mantissa.
255269
FPTYPE u_base = FPCONST(1.0);
256270
for (e = 0, e1 = FPDECEXP; e1; e1 >>= 1, pos_pow++) {
257-
// Use "volatile" to force the result to be stored in a float64.
258-
// Without this, nanbox build would sometimes fail to report
259-
// "equality" leading to weird results.
260-
#if defined(__i386__)
261-
volatile
262-
#endif
263271
FPTYPE next_u = u_base * *pos_pow;
264-
if (f >= next_u) {
272+
// fp_ge_eps performs "f >= (next_u - 2eps)" so that if, for
273+
// numerical reasons, f is very close to a power of ten but
274+
// not strictly equal, we still treat it as that power of 10.
275+
// The comparison was failing for maybe 10% of 1eX values, but
276+
// although rounding fixed many of them, there were still some
277+
// rendering as 9.99999998e(X-1).
278+
if (fp_ge_eps(f, next_u)) {
265279
u_base = next_u;
266280
e += e1;
267281
}
@@ -359,9 +373,11 @@ int mp_format_float(FPTYPE f, char *buf, size_t buf_size, char fmt, int prec, ch
359373
}
360374
}
361375
}
362-
// Figure how many u_bases we can fit into f.
363376
for (d = 0; d < 9; ++d) {
364-
if (f < u_base) {
377+
// This is essentially "if (f < u_base)", but with 2eps margin
378+
// so that if f is just a tiny bit smaller, we treat it as
379+
// equal (and accept the additional digit value).
380+
if (!fp_ge_eps(f, u_base)) {
365381
break;
366382
}
367383
f -= u_base;

tests/float/float_format_ints.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
print(title, "with format", f_fmt, "gives", f_fmt.format(f))
1313
print(title, "with format", g_fmt, "gives", g_fmt.format(f))
1414

15+
# Check that all the powers of 10 (that fit in float32) format right.
16+
for i in range(32):
17+
print("{:.12g}".format(float("1e" + str(i))))
1518

1619
# 16777215 is 2^24 - 1, the largest integer that can be completely held
1720
# in a float32.

0 commit comments

Comments
 (0)