Tuesday, September 26, 2006

### Math 101, a.k.a. what I learned in school.

You may have learned in school (as I did) that rounding the number 2.5 to an integer (in school this was more likely referred to as a "whole number") would produce the result 3.  In a nutshell, that algorithm (at least for integers) is to round the trailing .5 to the next integer farthest away from zero (remember negative numbers can be rounded as well) - check out Wikipedia's Common Rounding Method entry for a better algorithm description that applies to integer and decimals.  This algorithm is known by the names of "Common Rounding", "Symmetric Arithmetic Rounding", or simply "Rounding Away From Zero".

### Math 201, a.k.a. what .NET learned in school.

However, there are alternate rounding algorithms. One of the more common is known by the names of "Banker's Rounding", "Statistician's Rounding" or by the more useful description: "Round to Even".  Surprise!  This is the rounding algorithm implemented in the .NET Framework 1.0/1.1 (and the default in .NET 2.0 - more on that later).  This agorithm would produce a result of 2 (when rounding 2.5) - and why not, who says 2.5 is any closer to 3 than it is to 2?  But that's not all of the story.  This algorithm would also product a result of 2 when rounding 1.5.  No, it's not a split-personality disorder issue with this algorithm.  Remember the moniker "Round to Even"?  Well, that pretty much sums up its behavior.  The idea here is that by utilizing an algorithm that rounds toward zero on evens and away from zero on odds, given that you would generally get a fairly equal distribution of even and odd numbers in any set of numeric data, you tend to prevent bias-ing the totals upwards (or downwards if negative numbers) especially as compared with "Common Rounding".

Here's a quick illustration of the difference between the 2 algorithms:

Using the original un-rounded numbers (remember that the idea behind rounding is to get an estimate that is still mathematically representative of the original number):
1.5 + 2.5 = 4

Using Common Rounding, 1.5 becomes 2 and 2.5 becomes 3:
2 + 3 = 5

Using Round-to-Even Rounding, 1.5 becomes 2 and 2.5 becomes 2:
2 + 2 = 4

In this case, and in many cases, "Round to Even" produces a mathematical result that is closer the mathematical result of its original un-rounded counterparts.

### And to complicate things a little bit...

The .NET string format functions -- such as VB.NET's FormatNumber(), Format() or VB.NET/C# string format characters (as in the NumberFormatInfo class) -- when dealing in numbers that are effectively rounded due to the format string only allowing a limited amount of digits, uses the Common Rounding method.  I know this is the case in .NET 1.X, but I haven't tested in .NET 2.0.

And as far as interoperability is concerned, another Microsoft product you may have heard of, SQL Server, has a Round() function, which unlike .NET's Math.Round(), utilizes the Common Rounding method (at least in versions 2000 & 2005).  So, if you use SQL Stored Procedures to handle some of your business logic, keep this in mind.

### .NET 2.0 gives you a choice

As I alluded to above, the .NET 2.0 Framework's default rounding algorithm is Round-to-Even, however, the are overloaded Math.Round() methods introduced in .NET 2.0 that allow you to specify the rounding algorithm:

Visual Basic
Public Shared Function Round (d As Decimal, mode As MidpointRounding) As Decimal

C#
public static decimal Round (decimal d, MidpointRounding mode)

What is of interest here is the MidpointRounding enum that contains the 2 members:

MidpointRounding.ToEven
MidpointRounding.AwayFromZero

As you probably guessed, they are the "Round to Even" and "Common Rounding" algorithms, respectively.

### But I'm still on .NET 1.x!

But what if you haven't migrated to the .NET 2.0 Framework and you need to implement "Common Rounding"?!  Below is a snippet of code that implements the "CommonRounding" algorithm.  I converted the code to VB.NET from some C# source I found on this forum thread authored by Eric Peng.

Public Shared Function RoundCommon(ByVal val As Decimal, ByVal digits As Integer) As Decimal
Dim sign As Integer = Math.Sign(val)
Dim scale As Decimal = Math.Pow(10, digits)
Dim round As Decimal = Math.Floor(Math.Abs(val) * scale + 0.5D)
Return (sign * round / scale)
End Function

Note: The 8.665 bug mentioned in the forum thread seems to be because the author uses double vs. decimal.  There is an odd behavior in .NET (bug?) regarding this that I will comment on in a future post.