2

Consider the following code sample:

var tests = new List<double> { 131.505, 131.515, 131.525, 131.535, 131.545, 131.555, 131.565, 131.575, 131.585, 131.595 };
foreach (double n in tests)
{
    Console.WriteLine("{0} => {1}", n, Math.Round(n, 2, MidpointRounding.ToEven));
}

And its output:

131.505 => 131.5
131.515 => 131.51 <- wt*
131.525 => 131.52
131.535 => 131.54
131.545 => 131.54
131.555 => 131.56
131.565 => 131.56
131.575 => 131.57 <- wt*
131.585 => 131.58
131.595 => 131.6

I was expecting:

131.515 => 131.52
131.575 => 131.58

Why does MidpointRounding.ToEven algorithm produce a number that has an odd number at the end; and is there something I could do to fix this?

Background: I am passing the same numbers to PHP round($n, 2, PHP_ROUND_HALF_EVEN) function. The objective is to have both scripts produce same results.

I would appreciate an explanation of what is going on behind the scenes in this particular example instead of a canned "because floating point math is broken" response. I would like to know why PHP is able to produce the expected results but .NET is not? I would like to know if .NET's floating point is broken instead of floating point itself.

Salman A
  • 262,204
  • 82
  • 430
  • 521
  • 2
    From the docs: "Because of the loss of precision that can result from representing decimal values as floating-point numbers or performing arithmetic operations on floating-point values, in some cases the Round(Double, Int32, MidpointRounding) method may not appear to round midpoint values as specified by the mode parameter. This is illustrated in the following example, where 2.135 is rounded to 2.13 instead of 2.14. This occurs because internally the method multiplies value by 10digits, and the multiplication operation in this case suffers from a loss of precision." – Camilo Terevinto Aug 29 '18 at 11:46
  • @mjwills Yup, it works fine with decimal. – Nisarg Shah Aug 29 '18 at 11:48
  • Awesome - so use `decimal` or read my duplicate or Camilo's comment. – mjwills Aug 29 '18 at 11:49
  • @mjwills I am restricted to using float or double data types. – Salman A Aug 29 '18 at 11:51
  • 1
    Well then you are restricted to an 'incorrect' (in your eyes) answer. – mjwills Aug 29 '18 at 11:51

1 Answers1

4

Multiple roundings occur when working with floating-point numbers.

In the code you show, the source text 131.515 is converted from a decimal numeral to a double value. Since 131.515 cannot be represented exactly in double, the nearest representable value is produced. This is 131.5149999999999863575794734060764312744140625.

Thus, when Math.round is called, it is given the value 131.5149999999999863575794734060764312744140625. As this is less than 131.515, it is rounded to 131.51.

As Mark Dickinson noted in a comment, Math.Round is itself an imperfect implementation and contains internal rounding errors. For the source text 131.525, the conversion to double produces 131.525000000000005684341886080801486968994140625. Rounding this to two decimal digits after the decimal point ought to produce 131.53. However, Math.Round apparently computes the result by first multiplying by 100. Since the mathematical result of multiplying by 100 is not exactly representable, it is rounded to the nearest representable value, which is 13152.5. Then rounding this to integer with the round-to-nearest-ties-to-even rule produces 13152. Then dividing that and converting it to decimal produces “131.52”.

So we cannot expect Math.round to produce correct results.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • Where did you get 131.5149999999999... from? – Salman A Aug 29 '18 at 13:50
  • "At this is less than 131.515, it is rounded to 131.51." <- This seems to be based on the premise that `Math.Round` is doing correct rounding. But from the `131.525` example (for which the nearest representable binary64 value should round up under correct rounding to 2 decimal places), apparently it isn't. (From inspection, I'd guess that it's instead based on multiplying by 100.0, then rounding to the nearest integer under round-ties-to-even.) – Mark Dickinson Aug 29 '18 at 15:36
  • @SalmanA: there are websites and little programs that can show the *exact* values of floats or doubles. The values you see in your normal output are usually rounded by the formatting code. – Rudy Velthuis Aug 29 '18 at 16:31
  • @SalmanA: Using a good quality C implementation, I executed `printf(%.99g\n", 131.515);`. Manually, the math could be done: Representing 131.515 requires a high bit of 2^7 (128). The `double` format used has 53 bits in the significand, so the low bit would represent 2^(7−52) = 2^−45. So multiply 131.515 by 2^45, round to integer, and divide. [Wolfram Alpha](http://www.wolframalpha.com/input/?i=131515%2F1000*2**45) can be coaxed into doing the work; the product is 4627272695262740+12/25, so it rounds to 4627272695262740, and then dividing gives 131.5149999999999863575794734060764312744140625. – Eric Postpischil Aug 29 '18 at 17:06
  • @SalmanA: I actually posted code to convert decimal numerals to binary floating-point in [this answer](https://stackoverflow.com/a/51304463/298225). It shows how to do the conversion using just elementary-school arithmetic. Converting the resulting floating-point number to decimal can be done similarly. However, as Rudy Velthuis notes, there are web sites that will do this, and there are various mathematical software packages with extended-precision arithmetic. – Eric Postpischil Aug 29 '18 at 17:10
  • @MarkDickinson: Thanks, I have added that to the answer. – Eric Postpischil Aug 29 '18 at 17:13
  • 1
    The edit seems to explain the problem. Math.round _does_ multiply by 10 ^ n as described in the documentation. I'll accept the answer shortly. – Salman A Aug 29 '18 at 19:57