Have you tried using Math.Round() before? Did you REALLY test it out?
Monday, May 03, 2010 8:38 PM
I'm sure you've used Math.Round() before. But have you tried testing out the "fringe" cases? Do you KNOW or, do you THINK you know what it'll do with values like 3.5 or 2.5? Read on, you might be surprised what Math.Round()s default behaviour is.
Friday I was given a bug, no big deal right? We've all gotten bugs with ambigious algorithms before right? No? You haven't? hhhmmm you must be special! LOL For the REST of us, I was tasked with calculating the "median" value. Seems simple right? Until you actually go look for ONE defintion. Turns out there are a few.
MathsFun:How to find the Median Value
Anyways, I could list links until the cows come home, so what's the big deal? The summary of the different median definitions come down to one decision, do you want the resulting value to be one of the current values or not? If you do, then you CANNOT pick the definition which averages out the two middle numbes in an even number of elements. Why? Cause the resulting value most likely will NOT be one of the existing values. However, IF you want the result to be a currently existing value, then you can safely pick the (int)((n+1)/2) element.
Why this debate? It depends on the purpose of the value to be used, maybe you want the resulting number to be limited to the pool of values you currently have and not made up numbers are allowed (which is what the "average" calculation would give). On the other hand, maybe you fear your numbers are skewed and all over the map, and therefore, you feel the "average" is going to be a more realistic view on reality. The one you pick, is up to your specific needs.
ANYWAYS, I originally picked the latter one (middle element)), and the client wanted the former one (the averages one), hence why I got the bug. No biggie, this is cool, easy enough to change. (haha, I had kind of anticipated this coming down the pipes, so ya, I architected things to help me out juuuuuuuust in case of this very thing happening. LOL This time I got lucky, cool!)
Well, the next problem that creeped up was how to handle the resulting rounding of the averages. The client wanted to have integer values, or have everything rounded to zero decimal places. hhhmm how do you round 2.5 or 3.5? I was told, 2.5 will equal 3 and 3.5 will equal 4, in other words, always round up. hhhhmmmm I seem to remember my grade 7 science class talking about even go down and odds go up......hhhhmmmm I asked BertC, our QA guru, and he looked at me like I was from Pluto! DOH! hhhmm maybe I need another coffee? Turns out we were BOTH right. And Microsoft even accounted for it in it's Math.Round function. Ya, they did, check this out.!
Below you'll find the code to a sample test program I created to try things out, check it out and I think you'll be as surprised as we were to find out the default behaviour of Math.Round might not be doing what you originally expected.
Console.WriteLine( "Casting to an int" );
Console.WriteLine( "2.5 cast to an int = " + (int)2.5 );
Console.WriteLine( "3.5 cast to an int = " + (int)3.5 );
Console.WriteLine( "Default behaviour" );
Console.WriteLine( "2.5 rounded to 0 decimal places = " + Math.Round( 2.5 ) );
Console.WriteLine( "3.5 rounded to 0 decimal places = " + Math.Round( 3.5 ) );
Console.WriteLine( "MidpointRounding.ToEven behaviour" );
Console.WriteLine( "2.5 rounded to 0 decimal places (MidpointRounding.ToEven ) = " + Math.Round( 2.5, MidpointRounding.ToEven ) );
Console.WriteLine( "3.5 rounded to 0 decimal places (MidpointRounding.ToEven ) = " + Math.Round( 3.5, MidpointRounding.ToEven ) );
Console.WriteLine( "MidpointRounding.AwayFromZero behaviour" );
Console.WriteLine( "2.5 rounded to 0 decimal places (MidpointRounding.AwayFromZero ) = " + Math.Round( 2.5, MidpointRounding.AwayFromZero ) );
Console.WriteLine( "3.5 rounded to 0 decimal places (MidpointRounding.AwayFromZero ) = " + Math.Round( 3.5, MidpointRounding.AwayFromZero ) );
Console.WriteLine( "Int math trick, old-school (thanks to ShaneG)" );
Console.WriteLine( "2.5 rounded to 0 decimal places (+.5 cast to int) = " + (int)(2.5 + .5) );
Console.WriteLine( "3.5 rounded to 0 decimal places (+.5 cast to int) = " + (int)(3.5 + .5) );
The above code, creates the following output in a console app.
From above, you can see I've illustrated FOUR different ways, that I know of, to "round" off a number.
- Cast to an int
- "Rounds down"
- Fastest trick
- Same affect as using the Math.Truncate()
- Good for Graphics operations
- Good for array indexing, helps to ensure you don't have index out of bound exceptions cause you're always rounding down
- Not really "rounding" though is it?
- Math.Round default and with MidpointRounding.ToEven
- Default behaviour of Math.Round
- "Evens go down" (from their current value) and "Odds go up", hence 2.5 goes down to 2 and 3.5 goes up to 4
- Results in a statistically even distribution of resulting values, complicated way to say, this way is mathematically fair for rounding, half the time you go up and the other half you go down
- Sometimes, not what people expect to happen
- Always round up. That's it, always around up!
- Sometimes called "Bankers Rule" (thanks to PeterB) for this one
- What people MIGHT be expecting more
- BertC indicates this is more of what the layman expects from a rounding function instead of the current default Microsoft thinks people want
- ShaneG showed me this one, very cool
- identical to the AwayFrom Zero but without the extra method calls (well, if you don't count the addition and casting)
So there you have it, a brief description of the Math.Round method and just a few of it's intricacies. Now it's time to grab a coffee and get coding!
MSDN: Math.Round Method (Decimal, Int32)
MSDN: Math.Round Method (Decimal, MidpointRounding)
MSDN: MidpointRounding Enumeration