Gauche Devlog

< Pretty printer (and more) in REPL | Procedure inliner improvement >


Rounding 1.15

When you round 1.15 to the nearest tenths, what do you get?

The elementary math gives 1.2. Many programming language implementations disagree, however:

$ cat tmp.c
#include <stdio.h>
int main()
    printf("%5.1f\n", 1.15);
    return 0;
$ gcc tmp.c && ./a.out

Is it a bug? No, it's working as intended. The intention, however, is different from what people naturally expect.

* * *

Decimal 1.15 can't be represented exactly with a binary floating point number; so internally, the runtime picks the closest binary floating point number to 1.15, which happens to be very slightly smaller than the actual 1.15. So, when you need to choose to round it to either 1.1 or 1.2---if you look at the actual number you have, you should say 1.1 is closer. (By the way, if you use 4.15 instead in the above example, you'll get 4.2. That's because the binary floating point number closest to 4.15 is slightly greater than that.)

You can use Gauche to check if that's really the case. The exact function tries to find the simplest rational number within the error boundary of the floating point number, but using real->rational you can get the exact number represented internally by the floating point number.

gosh> (exact 1.15)
gosh> (real->rational 1.15 0 0 #f)

And indeed, the exact one is smaller than the one you naturally expect from the notation:

gosh> (< 2589569785738035/2251799813685248 23/20)

With 4.15, the exact one is greater than the closest simplified one:

gosh> (exact 4.15)
gosh> (real->rational 4.15 0 0 #f)
gosh> (> 2336242306698445/562949953421312 83/20)

So, if you take a point of view that a binary floating point number stands for the value it exactly represents (which is how they're treated inside the computer), you should round "the closest number to 1.15" to 1.1.

When users complain, we programmer tend to say "Floating point numbers have error. Use arbitrary precison arithmetic!" Well, floating point numbers themselves don't have error, per se. It has a precisely defined exact value, sign * mantissa * 2^exponent. It is an operation that has error, and in this case, it is the conversion from the decimal notation 1.15 to a binary floating point number.

* * *

But is it the only valid interpretation?

Another view is that when we treat a value noted "1.15", it is intended to be exactly 1.15 but we take the closest floating-point number as an approximation. The distinction is subtle but important---in the previous view, the intended value is 2589569785738035/2251799813685248 in floating-point number, and 1.15 is approximation. In the current view, the intended value is 1.15, and 2589569785738035/2251799813685248 in floating-point number is the approximation.

In this view, rounding "1.15" to the nearest tenths should result "1.2". (To be precise, we must also assume round-half-up or round-half-to-even rule). This usually fits user's expectation better. But it may be costly that we have to first obtain the optimal decimal representation of the given floating point number to decide which way to round.

* * *

We see both views are useful depending on circumstances. So we decided to support both.

The format procedure now supports floating-point number output directive, ~f. You can specify the field width and precision:

(format "~6,3f" 3.141592) ⇒ " 3.142"

If we need to round to the given precision, the default is to take the exact value of the floating-point number---the first view we discussed above. We call it effective rounding.

(format "~6,1f" 1.15)  ⇒ "   1.1"

However, if you need the latter view---we call it notational rounding---you can have it with : flag.

(format "~6,1:f" 1.15) ⇒ "   1.2"

Tags: 0.9.6, Flonums, format

Post a comment