32

I want a rounding method on double values in C#. It needs to be able to round a double value to any rounding precision value. My code on hand looks like:

public static double RoundI(double number, double roundingInterval) {

    if (roundingInterval == 0.0)
    {
        return;
    }

    double intv = Math.Abs(roundingInterval);
    double sign = Math.Sign(number);
    double val = Math.Abs(number);

    double valIntvRatio = val / intv;
    double k = Math.Floor(valIntvRatio);
    double m = valIntvRatio - k;

    bool mGreaterThanMidPoint = ((m - 0.5) >= 1e-14) ? true : false;
    bool mInMidpoint = (Math.Abs(m - 0.5) < 1e-14) ? true : false;
    return (mGreaterThanMidPoint || mInMidpoint) ? sign * ((k + 1) * intv) : sign * (k * intv);
}

So RoundI(100, 3) should give 99 and RoundI(1.2345, 0.001) should give 1.235.

The problem is, RoundI(1.275, 0.01) returns 1.27, rather than 1.28. This is because when executing double valIntvRatio = val/intv, that is, double valIntvRatio = 1.275 / 0.01, it gives 0.12749999999999. I know this is a problem with double representation in any programming language. My question is, is there a standard code to do things like this, without the need to worry about precision on double? Here I set the tolerant to 1e-14, but this is too restrict for this problem and I don't know what is the correct tolerance to be set. Thank you for any help.

csjohnst
  • 1,678
  • 3
  • 17
  • 24
Steve
  • 4,935
  • 11
  • 56
  • 83
  • 5
    Perhaps you should consider using the Decimal data type. – Kibbee Jan 25 '10 at 02:03
  • Why would round(100,3) give 99? If you're rounding to the same fractional position as the 3 (0 places), you'd get 100, not 99. – paxdiablo Jan 25 '10 at 02:17
  • paxdiablo: sorry, the purpose of RoundI is not to round to round the first parameter to the same fractional position as the second parameter. The second parameter is the round interval and the rounding rounds the first parameter to the closet value that has mode 0 to the second parameter. – Steve Jan 25 '10 at 03:37
  • Ah, that makes more sense (and my answer quite a bit less useful). Apologies for that. – paxdiablo Jan 25 '10 at 12:18

4 Answers4

49

Example of using decimal, as Kibbee pointed out

double d = 1.275;
Math.Round(d, 2);          // 1.27
Math.Round((decimal)d, 2); // 1.28 
Saro Taşciyan
  • 5,210
  • 5
  • 31
  • 50
Jimmy
  • 89,068
  • 17
  • 119
  • 137
  • 1
    While this great solution generally works, when the significant digits of `d` are near the limitation of the precision of a `double`, the conversion to `decimal` actually removes too much precision. As an example, take `d = 12345678901234.256;` (you need to output this with `d.ToString("R")` to reveal "hidden" precision). If you use simply `Math.Round(d, 2)` in this example (remember to write out result with `"R"`) you get a better result than if you do `Math.Round((decimal)d, 2)`. That cast to `decimal` removes too much precision here. In such cases, use `decimal` from the start, no casts. – Jeppe Stig Nielsen Jan 02 '14 at 09:36
  • +1 very nice solution, but it unfortunately doesn't round **0.005** to **0.01** . The result is **0** – Saro Taşciyan Jan 02 '14 at 16:53
  • 10
    @Zefnus because it uses Banker's rounding. If you want it to round to 0.01, use `Math.Round((decimal)d, 2, MidpointRounding.AwayFromZero)` – Jimmy Jan 02 '14 at 21:06
  • Why 0.125 result is 0.12? – Nguyễn Văn Quang Dec 12 '17 at 10:34
5
double d = 1.2345;

Math.Round(d, 2);

the code above should do the trick.

AceMark
  • 701
  • 1
  • 11
  • 21
1

If you actually need to use double just replace it below and it will work but with the usual precision problems of binary floating-point arithmetics.

There's most certainly a better way to implement the "rounding" (almost a kind of bankers' rounding) than my string juggling below.

public static decimal RoundI(decimal number, decimal roundingInterval)
{
   if (roundingInterval == 0) { return 0;}

   decimal intv = Math.Abs(roundingInterval);
   decimal modulo = number % intv;
   if ((intv - modulo) == modulo) {
       var temp = (number - modulo).ToString("#.##################");
       if (temp.Length != 0 && temp[temp.Length - 1] % 2 == 0) modulo *= -1;
   }
    else if ((intv - modulo) < modulo)
        modulo = (intv - modulo);
    else
        modulo *= -1;

    return number + modulo;
}
Jonas Elfström
  • 30,834
  • 6
  • 70
  • 106
  • possibly found a bug. If you pass in: number = 0.5 rounding interval 1.0 things go a bit wrong with the var temp array bit. – Jon Nov 04 '11 at 19:30
  • That's true but a bigger interval than number doesn't make any sense, does it? I added an extra check in the method. – Jonas Elfström Nov 04 '11 at 22:01
  • in the scenario that i am looking at it could happen. Trying to round a currency of X with a roundingInterval of Y, both unknown at compile time. So could get a small value to be rounded by a larger interval. Thanks for coming back to me. – Jon Nov 05 '11 at 15:00
  • 2
    It was indeed missing a `temp.Length != 0` now RoundI(50m,100m) returns 100 instead of an out of index exception. – Jonas Elfström Nov 06 '11 at 10:48
1

The examples using decimal casting provided in Jimmy's answer don't answer the question, since they do not show how to round a double value to any rounding precision value as requested. I believe the correct answer using decimal casting is the following:

    public static double RoundI(double number, double roundingInterval)
    {
        return (double)((decimal)roundingInterval * Math.Round((decimal)number / (decimal)roundingInterval, MidpointRounding.AwayFromZero));
    }

Because it uses decimal casting, this solution is subject to the casting errors mentioned by Jeppe Stig Nielsen in his comment to Jimmy's answer.

Also, note that I specified MidpointRounding.AwayFromZero, since that is consistent with the requester's specification that RoundI(1.2345, 0.001) should give 1.235.

R.T.
  • 39
  • 5