Rounding to the Whole in .NET

All bearded ku, comrades!

We all know what rounding is. If someone forgot, then rounding is the replacement of a number with its approximate value, written with fewer significant digits. If you ask a person on the fly what happens when you round 6.5 to integers, he will answer “7” without hesitation. We have been taught from school that numbers are rounded to the nearest integer, which is larger in absolute value. That is, if in the rounded number the fractional part is equal to or more than half the discharge of the whole part, then we round the original number to the nearest larger one.

Simply put:
6,4 = 6 6,5 = 7 6,6 = 7 

And so, leaving school and becoming programmers, we often expect the same behavior from our powerful programming languages. Forgetting completely that we were taught “mathematical rounding” at school, but in fact there are much more types of rounding. On Wikipedia alone, you can dig how many rounding options are 0.5 to the nearest integer:

The first type, "mathematical rounding," we all learned from school. You can read about the second and third types at your leisure, they are not interesting to me in this article today.

But “banking rounding” is already interesting. “Why?” You ask. In the subnet we often use the Convert class, which provides a lot of methods for converting one data type to another (not to be confused with the cast, it will be described below). And now, it turns out that when converting floating-point numbers ( double, float, decimal ) into an integer int type through the Convert.ToInt32 method , “banking” rounding works under the hood. It is used here by default!

And it seems like ignorance of this trifle does not greatly affect your work, but as soon as you have to work with statistics and calculating indicators based on a bunch of all kinds of records and numbers this thing will come out sideways. Because we expect (from ignorance) that all our conversions / roundings in the calculations will work according to the rules of "mathematical" rounding. And we look like a ram on a new gate for the result of rounding 6.5 , which is 6 .

The first thought of the programmer who sees this is: “Perhaps rounding works in the opposite direction, and according to the rules it rounds to the smallest number?”, “Maybe I forgot something from school mathematics?” Then he goes to google and understands that they have not forgotten anything, and that some kind of mob is going on. At this step, the lazy developer will decide that this is the standard behavior of the Convert.ToInt32 method, round to the smallest integer, and score for further search. And he will think that if Convert.ToInt32 (6,5) = 6 , then by analogy Convert.ToInt32 (7,5) = 7 . But it was not there. In the future, such developers will be hit on the head with a bunch of bugs from the QA department.

The fact is that “banking” rounding works a little trickier - it rounds a number to the nearest even integer, and not to the nearest integer modulo. This type of rounding is supposedly more honest in case of application in banking operations - banks will not deprive themselves or clients, on the assumption that there are as many operations with an even integer part as there are operations with an odd integer part. But as for me - it’s still unclear :) So that's why Convert.ToInt32 (6.5) will give a result of 6 , and the result for Convert.ToInt32 (7.5) will be 8 , not 7 :)

What to do to get everyone familiar “mathematical” rounding? Convert class methods do not have additional rounding options. It is true, because this class serves primarily not for rounding, but for type conversion. The wonderful Math class with its Round method comes to the rescue. But here also be careful, because by default this method works the same as rounding in Convert.ToInt32 () - according to the “banking” rule. However, this behavior can be changed using the second argument, which is part of the Round method. So, Math.Round (someNumber, MidpointRounding.ToEven ) will give us the default "banking" rounding. But Math.Round (someNumber, MidpointRounding.AwayFromZero ) will work according to the usual rules of "mathematical" rounding.

And by the way, Convert.ToInt32 () does not use System.Math.Round () under the hood. Specially digging on github the implementation of this method - rounding is considered according to the residuals:

 public static int ToInt32(double value) { if (value >= 0) { if (value < 2147483647.5) { int result = (int)value; double dif = value - result; if (dif > 0.5 || dif == 0.5 && (result & 1) != 0) result++; return result; } } else { if (value >= -2147483648.5) { int result = (int)value; double dif = value - result; if (dif < -0.5 || dif == -0.5 && (result & 1) != 0) result--; return result; } } throw new OverflowException(Environment.GetResourceString("Overflow_Int32")); } 

And finally, a few words about type casting :

 var number = 6.9; var intNumber = (int)number; 

In this example, I cast a floating point type ( double in this case) to an integer int . So, when casting to integer types, the whole non-integer part is simply truncated . Accordingly, in this example, the variable " intNumber " will contain the number 6 . There are no rounding rules here, just cutting off everything that comes after the decimal point. Remember this!

Related links:

PS Thanks to Maxim Yakushkin for drawing attention to this implicit moment.

PPS By the way, in python, rounding by default works the same way according to the “banking” principle. Perhaps in your language the same thing, be careful with numbers :)


All Articles