3

I'm struggling with how to correctly round the result of a calculation (a double) in c++ to n decimal places. Upon trying the standard approach (multiplying the number by 10^n, using the 'round' function to get the nearest long integer, then dividing that result by 10^n and recasting as double, as explained here: http://en.wikipedia.org/wiki/Rounding), I find that it gives unexpected results when applied to the results of a calculation (whose answer I think I know, but apparently the computer disagrees). An example for n=2:

#include <iostream>
#include "math.h"

int main()
{
   double x 177.95d; //original number
   double y yr;

   y = x*(1.0d-0.3d); //perform a calculation. Should be 124.565
   yr = (double) round(y/.01)*0.01; //round up y for n=2
   printf("rounded value of y = %.2f\n",yr); //prints out 124.56
}

To be clear, I know that all the numbers involved don't have an exact binary representation, but I wouldn't expect a double (which I take it on faith has approximately 15 decimal-digit accuracy) to have trouble approximating a number to within 5 significant (decimal) figures. As many programs round numbers in this fashion, I assume it's possible (right?) Even spreadsheets do this...

UPDATE: Apparently, I can get this to work by simply increasing the rounding accuracy. For example: yr = (double) round(y/.0001)*0.0001; seems to yield the correct results for numbers rounded to two decimal digits. However, I can't quite figure out the relationship between the decimal rounding digits (n) and the number of required zeroes in the rounding function (let's call it m).

UPDATE UPDATE: I think I've cracked it. To begin, I note that the output stream formatters for doubles already round up decimal representations to whatever format you request (eg. printf("%.2f\n",12.345) will produce the output 12.35). This means that a float or double whose decimal representation is to be rounded to the nth decimal place must first be converted to a representation whose (n+1)th decimal digit is correct. Now, assuming that the cumulative error (e) in the decimal representation of y is << 1.0E-(n+1) (Machine epsilon for 64-bit doubles is approximately 2.E-16), the only time the round function gives a bad result is when the (n+1)th digit of y is 5, but the decimal approximation gives an (n+1)th digit of 4 and the (n+2)th digit is >=5. In fact, in the example I give, the decimal approximation of y to 15 digits is 124.564999999999984 --just such a pathological case. The round function must simply account for these cases.

Thus, I conclude: The minimum value of the exponent, m required in the rounding function (double) round(y/1.0E-m)*1.0E-m that's guaranteed to always give acceptable results for doubles rounded to the nth decimal place is equal to two more than that: m=n+2. This explains the positive result in my first update...

agrishin
  • 31
  • 1
  • 3
  • 1
    Floating point math isn't exact – sbooth Sep 01 '14 at 17:13
  • Use `100`, not `0.01` when you calculate `yr`. – eduffy Sep 01 '14 at 17:14
  • http://stackoverflow.com/questions/4217510/how-to-cout-the-correct-number-of-decimal-places-of-a-double-value?rq=1 – Captain Giraffe Sep 01 '14 at 17:16
  • 2
    Floating point math is exact, but it is not what you think it is. It is precise, but not accurate. There is no `124.565` value that can be stored exactly as a `double`, and same for `0.3`: none of your equations are doing what you think they are doing. Go learn how floating point math works and then come back! – Yakk - Adam Nevraumont Sep 01 '14 at 17:20
  • Floating-point values don't have decimal places. They have binary places. Ergo you cannot round them to specific numbers of decimal places, except in the sparse cases where decimal and binary places coincide. If you want decimal places you must use a decimal radix. – user207421 Sep 01 '14 at 17:21
  • Not a direct answer to your question, still... "the compiler doesn't think seventy percent of 177.95 actually is 124.565" - the compiler doesn't invest too much effort thinking about it. The processor, on the other hand, does (well, a little more than the compiler anyhow)... And it does so **during runtime**, i.e., while your program is in execution. – barak manos Sep 01 '14 at 17:30
  • @barakmanos Actually the compiler does have a part in this. It is responsible for the FP representation of the constant 177.95, which is already imprecise, and similarly the value (1.0d-0.3d), ditto, and 0.01d, ditto. – user207421 Sep 01 '14 at 17:32
  • Thanks, eduffy. Your trick seems to work in c++. I find that it produces something entirely different in java (but that goes beyond my original question). Although, I have no idea why it works, I'm going to assume the answer lies in the mysterious netherworld of floating point arithmetic. Thanks to Yakk for pointing out that indeed 124.565 does not have an exact representation in floating point arithmetic (few numbers do). I think what I find surprising is how 'off' it is. Observe that this problem manifests itself in the second decimal place representation of a double. – agrishin Sep 01 '14 at 17:54
  • http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html – OldProgrammer Sep 01 '14 at 20:16
  • @agrishin Take the other hints first. Print out the values of x and y. You'll get a surprise. You need three extra 'guard bits' to get any kind of decimal accuracy in floating point. – user207421 Sep 02 '14 at 02:16
  • @EJP x to 15 places = 177.949999999999984. y to 15 places is 124.564999999999984. Did you read my updated update? According to my current understanding of things, x will round up correctly if you just feed it 'raw' to an output format statement for two-digit accuracy such as "%.2f". Y will round correctly using the rule I propose in my updated update. The key restriction (or assumption) behind my reasoning is that any error in the binary-decimal conversion is much less than 1.0E-n, where the nth (decimal) digit is to be rounded. – agrishin Sep 02 '14 at 02:54

1 Answers1

-2

Use double rounding. The first rounding will make the next to final digit what you expect. Add a very small offset to counteract the tendency of binary floating point numbers to be below the actual desired value, then round again.

yr = round((round(y/0.001)+0.1)/10.0)*0.01;

This won't work in all cases, but it should be a lot more consistent than the naive method.

Mark Ransom
  • 299,747
  • 42
  • 398
  • 622