6

I'm trying to round a number to it's first decimal place and, considering the different MidpointRounding options, that seems to work well. A problem arises though when that number has sunsequent decimal places that would arithmetically affect the rounding.

An example:

With 0.1, 0.11..0.19 and 0.141..0.44 it works:

Math.Round(0.1, 1) == 0.1
Math.Round(0.11, 1) == 0.1
Math.Round(0.14, 1) == 0.1
Math.Round(0.15, 1) == 0.2
Math.Round(0.141, 1) == 0.1

But with 0.141..0.149 it always returns 0.1, although 0.146..0.149 should round to 0.2:

Math.Round(0.145, 1, MidpointRounding.AwayFromZero) == 0.1
Math.Round(0.146, 1, MidpointRounding.AwayFromZero) == 0.1
Math.Round(0.146, 1, MidpointRounding.ToEven) == 0.1
Math.Round(0.146M, 1, MidpointRounding.ToEven) == 0.1M
Math.Round(0.146M, 1, MidpointRounding.AwayFromZero) == 0.1M

I tried to come up with a function that addresses this problem, and it works well for this case, but of course it glamorously fails if you try to round i.e. 0.144449 to it's first decimal digit (which should be 0.2, but results 0.1.) (That doesn't work with Math.Round() either.)

private double "round"(double value, int digit)
{
    // basically the old "add 0.5, then truncate to integer" trick
    double fix = 0.5D/( Math.Pow(10D, digit+1) )*( value >= 0 ? 1D : -1D );
    double fixedValue = value + fix;

    // 'truncate to integer' - shift left, round, shift right
    return Math.Round(fixedValue * Math.Pow(10D, digit)) / Math.Pow(10D, digit);
}

I assume a solution would be to enumerate all digits, find the first value larger than 4 and then round up, or else round down. Problem 1: That seems idiotic, Problem 2: I have no idea how to enumerate the digits without a gazillion of multiplications and subtractios.

Long story short: What is the best way to do that?

Charles
  • 50,943
  • 13
  • 104
  • 142
sunside
  • 8,069
  • 9
  • 51
  • 74
  • 6
    why should 0.149 round to 0.2 it is less than the midpoint so it should round down not up. do you actually want to truncate and add 1 maybe? – jk. Mar 25 '10 at 11:13

4 Answers4

21

Math.Round() is behaving correctly.

The idea with midpoint rounding is that half of the in-between numbers should round up and half should round down. So for numbers between 0.1 and 0.2, half of them should round to 0.1 and half should round to 0.2. The midpoint between these two numbers is 0.15, so that's the threshold for rounding up. 0.146 is less than 0.15, therefore it must round down to 0.1.

                    Midpoint
0.10                  0.15                  0.20
 |----------------|----|---------------------|
                0.146
       <---- Rounds Down
Stephen Jennings
  • 12,494
  • 5
  • 47
  • 66
  • Actually, you do sometimes need to look beyond. For example, suppose we're using round-to-even to tenths; 0.250 rounds to 0.2 (it's exactly on the midpoint, so it goes to even 0.2 rather than odd 0.3), but 0.251 rounds to 0.3 (as it is closer to 0.3 than 0.2). I think the digit sequence "50̅" (5 after your rounding place followed by all zeros) is the only case you need to consider, though. – Kevin Reid Mar 25 '10 at 11:39
  • Math.Round doesn't behave correctly, it rounds to the next even integer, e.g. `Math.Round(1.5) == 2` as well as `Math.Round(2.5) == 2` – K. Frank May 24 '20 at 15:07
  • To do mitdpoint rounding, use `Math.Round(2.5, [System.MidpointRounding]::AwayFromZero)` the default if not specified is "ToEven" – K. Frank May 24 '20 at 15:15
  • @K.Frank The default of ToEven is "correct", it just isn't what you were probably expecting. ToEven (which I've known as "[bankers rounding](https://en.wikipedia.org/wiki/Rounding#Round_half_to_even)" but apparently has many names) tends to avoid the bias that AwayFromZero can introduce. The question on this page was making the mistake of rounding iteratively (rounding to the hundredths place, then again to the tenths), so that's what this answer is pointing out. – Stephen Jennings May 24 '20 at 22:12
  • @KevinReid You're right. I'm surprised that incorrect statement made it 10 years without someone editing the answer for me. I've removed it from my answer since it didn't matter for addressing the question. – Stephen Jennings May 24 '20 at 22:16
15

I don't get what you are trying to accomplish here. 0.149 rounded to one decimal place is 0.1, not 0.2

  • Yep, think some basic maths revision is required first ;) – Paolo Mar 25 '10 at 11:13
  • I understood (and understand) rounding as truncating to a number of digits with adjustment of that specific digit according to the value of, well, the rest. Or, 0.149 being 0.15, being 0.2. Don't destroy my world! – sunside Mar 25 '10 at 11:14
  • 3
    Yea, you don't start rounding from the last decimal place, then work in. You simply look at the decimal place after the place you want to round to. – Benny Jobigan Mar 25 '10 at 11:15
  • 2
    if 1.49 -> 0.15 -> 0.2 you are rounding twice! once to 2dp and once to 1dp. you generally don't want to round twice as each round adds error to the 'real' value – jk. Mar 25 '10 at 11:15
  • 4
    I think a strong coffee is needed right now. :D Thanks anyone! – sunside Mar 25 '10 at 11:21
6

Rounding is not an iterative process, you round only once.

So 0.146 rounded to 1 decimal digit is 0.1.

You don't do this:

0.146 --> 0.15
0.15 -->  0.2

You only do this:

0.146 --> 0.1

Otherwise, the following:

0.14444444444444446

would also round to 0.2, but it doesn't, and shouldn't.

Lasse V. Karlsen
  • 380,855
  • 102
  • 628
  • 825
5

Don't try and compound the rounding 'errors'. Which is what you're trying to do.

.146 should round down to .1 if you're going to one decimal place.

By rounding it to .15 first, then again to .2 you're just introducing more rounding error, not less.

davewasthere
  • 2,988
  • 2
  • 24
  • 25