Monday, January 9, 2012

Why C#'s Math.Round is not Working Correctly?

Is it a bug? Yes, in my opinion, it is. In MSDN library, Math.Round method in C# uses Round-Half-Even AKA Banker's Rounding as its default rounding method. However, a lot of people claim that it is not working as accurate as it should be. I am going to tell you how it happened and how to prevent it happens.

Although, Microsoft did explain why its behavior not like it should be, they didn't clearly or precisely say in what circumstance will cause it happen. I still don't know when it will happen. It'll take me a lot of time to figure it out; though, I don't want to waste my time to do this job. I just want to remind myself in here that C#'s Math.Round is not working now (maybe only when you use double variables. I am going to tell why).

First, here are two links that I believe will help you easily to get into rounding word:

Second, if you look into the MSDN online document and scroll it to Notes to Callers in Math.Round Method (Double) and  Math.Round Method (Double, Int32), you will found that Microsoft did say because of the loss of precision......may not appear to round midpoint values to the nearest even value......

Here is a copy of both of them (go and click the links above to take a look into them if you want to know how they happened):

  Notes to Callers (a copy from Math.Round Method (Double) ) 

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) method may not appear to round midpoint values to the nearest even integer. In the following example, because the floating-point value .1 has no finite binary representation, the first call to the Round(Double) method with a value of 11.5 returns 11 instead of 12.

  Notes to Callers (a copy from Math.Round Method (Double, Int32) )

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) method may not appear to round midpoint values to the nearest even value in the digits decimal position. 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.

Those Notes to Callers also appear in the following documents: Math.Round Method ( Double, MidpointRounding) and Math.Round Method (Double, Int32, MidpointRounding).

It means that this kind of wrong behavior not only appear when you use MidpointRounding.ToEven but also appear when you use MidpointRounding.AwayFromZero.

Fortunately, it looks like this kind of wrong doing only happen when you use double (Double) as your variable. I test it, and it does look like only effect on double variables. If I use decimal (Decimal), it will run correctly (looks like correctly. Because I just did a small test not a lot)

Here is a copy of my code to test Math.Round in C#:

static void Main(string[] args)

{

    //roundTest();

    roundToEvenDoubleTest();

    roundToEvenDecimalTest();

    Console.ReadKey();

}

 

private static void roundToEvenDoubleTest()

{

    Console.WriteLine("Ori        ToEven    AwayFromZero");

    double doubleA = 2.005;

    for (int i = 0; i < 200; i++)

    {

        double doubleB = doubleA + ( (double)i / 100d );

        double doubleC = Math.Round(doubleB, 2, MidpointRounding.ToEven);

        double doubleD = Math.Round(doubleB, 2, MidpointRounding.AwayFromZero);

        Console.Write("{0:N3} --> {1:n3} --> {2:N3}", doubleB, doubleC, doubleD);

        // Check. Note: This check has some problems in some of the begining figures

        if (((doubleC * 100) % 2) != 0) Console.Write(" <-- Round To Even Error");

        if (doubleD < doubleB) Console.WriteLine(" <-- Round Away From Zero Erro");

        else Console.WriteLine();

    }

}

 

private static void roundToEvenDecimalTest()

{

    Console.WriteLine("Ori        ToEven    AwayFromZero");

    decimal decimalA = 2.005m;

    for (int i = 0; i < 200; i++)

    {

        decimal decimalB = decimalA + ((decimal)i / 100m);

        decimal decimalC = Math.Round(decimalB, 2, MidpointRounding.ToEven);

        decimal decimalD = Math.Round(decimalB, 2, MidpointRounding.AwayFromZero);

        Console.Write("{0:N3} --> {1:n3} --> {2:N3}", decimalB, decimalC, decimalD);

        // Check. Note: This check has some problems in some of the begining figures

        if (((decimalC * 100) % 2) != 0) Console.Write(" <-- Round To Even Error");

        if (decimalD < decimalB) Console.WriteLine(" <-- Round Away From Zero Erro");

        else Console.WriteLine();

    }

}

3 comments:

  1. So it's really a bug! Man... I thought I was getting crazy. Very, very nice example.

    Thanks for the decimal tip!

    ReplyDelete
    Replies
    1. Hi, I really am sorry! It was an oversight that I did not see the word "bug" you wrote here. While my topic is "not working correctly", it means it's practically not a bug here (or may be it is. I just don't want to definitely say it and we shouldn't. Because it's up them to fix it or not.)

      It's a physically defect in physical binary bit to represent some of the floating-point values in Double type, and Microsoft seems don't want to fix it programmatically or they may not has any resolution right now. SO, it's the loss of precision in the computer science here.

      Delete