Saturday, April 28, 2012

C++: rounding a double to a fixed number of decimals

An accounting program handles money amounts, so it is important to avoid computing errors.

The problem is that, using a double, most values cannot be stored exactly but there is a small error. Numbers are stored in base 2, and math tells us that only the decimal numbers obtained dividing some integer value by a power of two will have an exact representation in that base.
So 2.5, 5.25, 3.125 and similar numbers will be stored exactly, while all other number will be represented by a periodic number.
What happens is similar to representing 1/3 in base 10: the result is 0.333333333... with an infinite number of decimals.

A double value has a finite size so it cannot contain infinite decimals: the result is a small error.
There other ways to store decimal values exactly, using a custom class, but I prefer to use a standard type so I use doubles.
Doubles have a precision of about 15 digits, so there is plenty of room to handle money amounts. Rounding errors will be much smaller than the printed values (we will print few decimals) so there should be no problems if computations are handled correctly.

This is true, but I have found a situation where rounding errors lead to a wrong result. I was testing the program and it computed the VAT for an invoice: the right value was 3.16 but the program printed 3.15.

A quick investigation showed that the exact value was 3.155: rounding to two decimals leads to 3.16.
The debuggers showed me that the approximated value contained in the double variable was 3.1549999999999998. The error is very small, but is is enough to cause rounding to 3.15.

I made some quick searches but I did not find anything about this problem, so I had to find a solution by myself.
In the end I decided to modify the function that I use to round to a certain number of decimals: now, before rounding, I add a very small amount (say 0.00000000001) to the number that will be rounded. The value is very small but it is enough to make the number (in the previous example) to become a little more than 3.155 so now rounding is correct.

I don't know if there are better solution to this problem, but I think that adding such a small value to a number that represents a money amount is not likely to introduce errors (at least I have not found any), and it solves the problem.

6 comments:

  1. I'm using function pasted below to round doubles. Prec parameter defines how many digits after decimal point are significant for you (in case of money it's obviously 2).
    // -- cut --
    double round(double x, int prec)
    {
    double power = 1.0;
    int i;

    if (prec > 0)
    for (i = 0; i < prec; i++)
    power *= 10.0;
    else if (prec < 0)
    for (i = 0; i < prec; i++)
    power /= 10.0;

    if (x > 0)
    x = floor(x * power + 0.5) / power;
    else if (x < 0)
    x = ceil(x * power - 0.5) / power;

    if (x == -0)
    x = 0;

    return x;
    }
    // -- cut --
    Greetings
    wg

    ReplyDelete
    Replies
    1. Thank you for sharing. I use a different function but they both follow the mathematical rules for rounding, so they should yield the same result.

      I am pretty sure that if you send 3.155 to your function you get 3.15, a wrong result.

      The post was about a situation where rounding a number using correct rules yields a wrong result, and about a possible solution to the problem.

      Delete
  2. Thanks for comment. As a matter of fact when I call round(3.155,2) I get a result of 3.16 :-) (at least on my Windows machine with Visual C++ compiler). That's why I decided to share the code.
    Greetings
    wg

    ReplyDelete
    Replies
    1. I tested your code and it works: very interesting!
      I debugged it and it looks like the trick is done when computing "val * power": in my example it is "val * 100" and the result is 315.5 instead of the expected 315.4999999...
      As I wrote in the post 315.5 is a number with a finite representation in binary form, so it looks like the runtime is smart enough to understand it and to change the result to the right value.

      It looks like your code solves the problem: as soon as I will have some time I will test it under Linux with the gcc compiler to see if it works too.

      Thank you!

      Delete
  3. I tested this code and didn't work for all cases:
    for example:
    Profit vs Rounded: 3.70500 vs 3.70000
    Profit vs Rounded: 6.13500 vs 6.14000
    Profit vs Rounded: 6.18500 vs 6.19000
    Profit vs Rounded: 2.04500 vs 2.05000

    ReplyDelete
    Replies
    1. Which code did not work, mine or Wacec's?

      Delete