Optimus Prime
Optimus Prime

Reputation: 439

Float point numbers and incorrect result due to rounding behavior

I need to output float point numbers with two digits after the decimal point. In addition, I also need to round off the numbers. However, sometimes I don't get the results I need. Below is an example.

#include <iomanip>
#include <iostream>
using namespace std;

int main(){
    cout << setprecision(2);
    cout << fixed;
    cout << (1.7/20) << endl;
    cout << (1.1/20) << endl;
}

The results are:

0.08
0.06

Since 1.7/20=0.085 and 1.1/20=0.055. In theory I should get 0.09 and 0.06. I know it has something to do with the binary expression of floating point numbers. My questions is how can I get the right results when fixing the number of digits after the decimal point with rounding off?

EDIT: This is not a duplicate of another question. Using fesetround(FE_UPWARD) will not solve the problem. fesetround(FE_UPWARD) will round (1.0/30) to 0.04 while the correct results should be 0.03. In addition, fesetround(FE_TONEAREST) doesn't help either. (1.7/20) still round to 0.08.

EDIT 2: Now I understand that this behavior might be due to the half-to-even rounding. But how can I avoid this? Namely, if the result is exact half, it should round up.

Upvotes: 3

Views: 904

Answers (3)

vinc17
vinc17

Reputation: 3476

The issue is due to binary floating-point representation and floating-point constants in C. The fact is that 1.7 and 1.1 are not exactly representable in binary. The ISO C standard says (I suppose that this is similar in C++): "Floating constants are converted to internal format as if at translation-time." This means that the active rounding mode (set by fesetround) will not have any influence at all for the constant (it may have an influence for the roundings that occur at run time).

The division by 20 will introduce another rounding error. Depending on the full code and compiler options, it may or may not be done at compile time, so that the active rounding mode may be ignored. In any case, if you expect 0.085 and 0.055 exactly, this is not possible because these values are not representable exactly in binary.

So, even if you have perfect code that rounds double values on 2 decimal digits, this may not work as you want, because of the rounding errors that occurred before, and it is too late to recover the information in a way that works in all cases.

If you want to be able to handle "midpoint" values such as 0.085 exactly, you need to use a number system that can represent them exactly, such as decimal arithmetic (but you may still get rounding errors in other kinds of operations). You may also want to use integers scaled by a power of 10. There is no general answer because this really depends on the application, as any workaround will have drawbacks.

For more information, see all the general articles on floating point and Goldberg's article (PDF version).

Upvotes: 0

Saurav Sahu
Saurav Sahu

Reputation: 13974

You can define round_with_precision() method of your own, which would invoke tgmath.h provided round() method passing modified value, and then returning the value after dividing with same factor.

#include <tgmath.h> 
double round_with_precision(double d, const size_t &prec)
{
    d *= pow(10, prec);
    return (std::round(d) / pow(10, prec));
}
int main(){
    const size_t prec = 2;
    cout << round_with_precision(1.7/20, prec) << endl;  //prints 0.09
    cout << round_with_precision(1.1/20, prec) << endl;  //prints 0.06
}

Upvotes: 2

Mark Ransom
Mark Ransom

Reputation: 308520

Yes, you're right - it has to do with the representation in base 2, and the fact that sometimes the base 2 value will be higher than the base 10 number and sometimes it will be lower. But never by much!

If you want something that matches expectations more often, you can do two stage rounding. A double is generally accurate to at least 15 digits (total, including those to the left of the decimal point). Your first rounding will leave you with a number that has more stability for the second phase of rounding. No rounding is going to match the results you would get in decimal 100%, but it's possible to get very close.

double round_2digits(double d)
{
    double intermediate = floor(d * 100000000000000.0 + 0.5); // round to 14 digits
    return floor(intermediate / 1000000000000.0 + 0.5) / 100.0;
}

See it in action.


For a totally different approach, you can simply ensure that the base 2 number that you start with is always larger than the desired decimal, instead of being larger half the time and smaller half the time. Simply increment the least significant bit of the number with nextafter before rounding.

double round_2digits(double d)
{
    return floor(100.0 * std::nextafter(d, std::numeric_limits<double>::max())) / 100.0;
}

Upvotes: 3

Related Questions