Click here to Skip to main content
15,879,326 members
Articles / Programming Languages / C#
Tip/Trick

How .NET's Math.Round has Nothing to do with Maths. And That's OK!

Rate me:
Please Sign up or sign in to vote.
4.97/5 (84 votes)
18 May 2022CPOL6 min read 64.8K   28   39
About the unexpected behaviour of .NET's Math.Round

Introduction

Before we start, imagine what values "a" and "b" would have after running the following:

C#
a = Math.Round(1.5);
b = Math.Round(2.5);

If your answer is "a = 2 and b = 3", you are, just like I was, wrong. The correct answer is that both a and b end up becoming 2. Confused? You might want to stick around for the reason why.

Background

About a week ago, I was working on some code that was moving two entities along the x-axis. Inside the library, their x value was a double, but the public interface only allowed for meters in the form of an integer. I used Math.Round to translate the double value into that integer.

One of the requirements for the code was that the entities could never be closer than one meter to each other. I used the rounded value to check this.

As you might have guessed by now, the code kept failing. Even though both entities started out exactly one meter apart, had the same starting speed and were accelerating with the same speed, using the same algorithm, the integer value kept returning the same x value once in a while and thus the code kept failing.

At first, I thought it was some kind of precision error as a result of using double values. But this wouldn't make much sense, as the algorithm would result in the same precision loss in both values. After some debugging, I eventually found out it was not the double but Math.Round that was behaving differently than I had anticipated.

Bankers Rounding

After I had figured out the problem was with the use of Math.Round, I quickly figured out the problem: the default[1] implementation for rounding in .NET is "Round half to even"[2] a.k.a. "Bankers Rounding". This means that mid point values are rounded towards the nearest even number. In the example I provided in the introduction, this means both values are being round towards 2, the nearest even number.

So why was it implemented like this in .NET? Is it a bug?

Well, when first implemented, Microsoft followed the IEEE 754 standard. This is the reason of the default Math.Round implementation.[3] (Note: The current IEEE 754 standard contains five rounding rules[4])

Another good reason is that bankers rounding does not suffer from negative or positive bias as much as the round half away from zero method over most reasonable distributions.

Round Half Away From Zero

But all is not lost.

If you expect 1.5 to be rounded to 2 and 2.5 to be rounded to 3 (like I expected), the Math.Round method has an override that allows you to use the "Round half away from zero"[5] method instead.

C#
a = Math.Round(1.5, MidpointRounding.AwayFromZero);
b = Math.Round(2.5, MidpointRounding.AwayFromZero);

In the code snippet above, 1.5 is rounded to 2 and 2.5 is rounded to 3.

Round Half Up

Interestingly however, after doing some research, it looks like "Round half away from zero" is also not the method commonly used in maths. Instead, the "Round half up"[6] method is usually used[7] in maths. For positive numbers, there is no difference, but for negative numbers, mid point values are rounded towards +∞ instead of away from 0.

This way, there are the same amount of fractions being rounded to zero, while in the "Round half away from zero" method, both 0.5 and -0.5 are not rounded to zero, making zero an exception to all other numbers.

Math ≠ maths

So it turns out that the Math.Round method does not even support the actual rounding method that is commonly used in maths.

When I first discovered this, I was quite disappointed. After all, even with the good reasons that were provided, the library is still called Math. It does seem to communicate a certain intent.

But writing this article did make me think: before looking into it, I was not even aware there were so many rounding methods. Each with its own pros and cons. Each with its own consequences. If I didn't know about all these methods, I certainly did not know about those consequences. So maybe it is not such a bad thing that someone else did this for me, and made the appropriate "default" decision.

After all, if the consequences of your rounding are that critical to your code, I do hope that you don't make the assumptions I did, or at least discover they were wrong with some good old fashion tests.

Wrapping Up

Even though this subject isn't really brain surgery (or rocket science for that matter), I do hope you learned something new, or at least enjoyed the read.

In the end, I think this is a good reminder of how complex even the seemingly simple things we use every day can be. And there is probably a lesson about assumptions in here as well. ;)

Happy rounding!

Other Programming Languages

After discussing this subject with others, I got curious how other languages handle the mid point values. I was happily surprised that almost every language did have this subject covered. However, there are a lot of differences between languages.

To hopefully help someone in the future, I have mapped the rounding methods in a table. X's in bold are default implementations, other X's are optional parameters or separate methods.

In an attempt to not clutter the references list, the language names in the table link to the documentation I used.

Without further ado, the big "programming language/rounding method table":

  Java C Python* C++ VB .NET Java
Script
C# PHP Objective C MATLAB R Perl Swift Go Delphi Ruby**
Round half up X X       X   X                
Round half down   X           X                
Round half towards zero   X                           X
Round half away from zero   X   X X   X   X X   X X X   X
Round half to even   X X   X   X X     X X   X X X
Round half to odd               X       X        
Random tie-breaking                       X        

* The table references Python 3.7 but Python 2 actually has a different round implementation. It will round half away form zero.
** The mode keywords in Ruby are :up for "round half away from zero" and :down for "round half towards zero", making them quite confusing in my opinion.

As I have no experience with most of these languages, feel free to point out any mistakes. I will correct the table accordingly.

References

  1. https://docs.microsoft.com/en-us/dotnet/api/system.math.round?view=netframework-4.7.2#midpoint-values-and-rounding-conventions
  2. https://en.wikipedia.org/wiki/Rounding#Round_half_to_even
  3. http://stackoverflow.com/questions/311696/why-does-net-use-bankers-rounding-as-default/6562018#6562018
  4. https://en.wikipedia.org/wiki/IEEE_754#Rounding_rules
  5. https://en.wikipedia.org/wiki/Rounding#Round_half_away_from_zero
  6. https://en.wikipedia.org/wiki/Rounding#Round_half_up
  7. https://www.mathsisfun.com/numbers/rounding-methods.html (not sure about the reliability of this reference, but I couldn't find a lot about rounding methods in maths)

History

  • 23-12-2018 - Version 1
  • 23-12-2018 - Version 1.1
    • Minor textual changes
  • 24-12-2018 - Version 1.2
    • Clarified example
  • 26-12-2018 - Version 1.3
    • Minor textual change
  • 05-01-2018 - Version 2.0

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Netherlands Netherlands
Enthusiastic Senior Software Engineer.

Experience working in a multinational and multidisciplinary environment with involvement in all stages of the software development process. Supportive and reliable team player with a strong aim towards software quality and customer satisfaction. Fast and eager learner.

His passion for programming can be traced back to his pre-professional days. Where, even as an elementary school student he could be found on the computer creating computer games. The reason? There is just no feeling like being able to think something up, create it, and then see others enjoy it.

Outside the office, he's a contributor to the Code Project and there is always a project he's working on. When he's not coding he likes to make and edit video’s, can discuss theoretical physics for hours and if you challenge him to a board game, he won’t say no. He can also frequently be found in the gym and travels when he can.

Comments and Discussions

 
GeneralRe: "Maths" LOL Pin
DaveBoltman7-Jan-19 21:09
DaveBoltman7-Jan-19 21:09 
GeneralRe: "Maths" LOL Pin
Frank Malcolm21-May-22 19:37
Frank Malcolm21-May-22 19:37 
PraiseExcellent Pin
Thanks787225-Dec-18 7:34
professionalThanks787225-Dec-18 7:34 
QuestionSimilar discovery on rounding Pin
nedcove25-Dec-18 7:24
nedcove25-Dec-18 7:24 
AnswerRe: Similar discovery on rounding Pin
Jasper Lammers25-Dec-18 20:31
Jasper Lammers25-Dec-18 20:31 
Praise.Net uses the wrong (unexpected) default parameter Pin
snoopy00125-Dec-18 1:57
snoopy00125-Dec-18 1:57 
GeneralRe: .Net uses the wrong (unexpected) default parameter Pin
Jasper Lammers12-Jan-19 11:04
Jasper Lammers12-Jan-19 11:04 
GeneralMy vote of 5 Pin
Robert_Dyball23-Dec-18 23:50
professionalRobert_Dyball23-Dec-18 23:50 
Hey thanks Jasper for that one, it'll be very useful to know - one of those gotcha's that could easily eat time debugging.
GeneralRe: My vote of 5 Pin
Jasper Lammers24-Dec-18 23:18
Jasper Lammers24-Dec-18 23:18 
Questionrounding error in round Pin
Paul Tait23-Dec-18 15:44
Paul Tait23-Dec-18 15:44 
AnswerRe: rounding error in round Pin
Jasper Lammers24-Dec-18 23:21
Jasper Lammers24-Dec-18 23:21 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.