45

Here is my code:

using static System.Console;

namespace ConsoleApp2
{
    internal class Program
    {
        static void Main(string[] args)
        {
            double[] doubles = new[] { 9.05, 9.15, 9.25, 9.35, 9.45, 9.55, 9.65, 9.75, 9.85, 9.95 };
            foreach (double n in doubles)
            {
                WriteLine("{0} ===> {1:F1}", n, n);
            }

        }
    }
}

Output in .NET Framework 4.7.2:

9.05 ===> 9.1
9.15 ===> 9.2
9.25 ===> 9.3
9.35 ===> 9.4
9.45 ===> 9.5
9.55 ===> 9.6
9.65 ===> 9.7
9.75 ===> 9.8
9.85 ===> 9.9
9.95 ===> 10.0

Output in .NET 6 (with same code):

9.05 ===> 9.1
9.15 ===> 9.2
9.25 ===> 9.2
9.35 ===> 9.3
9.45 ===> 9.4
9.55 ===> 9.6
9.65 ===> 9.7
9.75 ===> 9.8
9.85 ===> 9.8
9.95 ===> 9.9

So, in .NET Framework, the numbers are rounded just like we were taught in school. Which is called round half up in Wikipedia.

But in .NET 6, 9.05, 9.15, 9.55, 9.65, 9.75 are rounded up, while 9.25, 9.35, 9.45, 9.85, 9.95 are rounded down.

I know there is a rule called round half to even – rounds to the nearest value; if the number falls midway, it is rounded to the nearest value with an even least significant digit.

But this is obviously not round half to even, some numbers are rounded to odd.

How can we explain the difference in .NET Framework 4.7.2 with .NET 6 and how can I just round the numbers in the same way as .NET Framework in .NET 6?

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
obnews
  • 602
  • 5
  • 13
  • 3
    I very much suspect the difference is in the `{1:F1}` part, but it is too late (as in bed time) for me to investigate for you. – Andrew Morton Jul 31 '22 at 20:26
  • 6
    Maybe the difference in behavior is related to [floating-point formatting "improvements" in .NET Core 3.0](https://devblogs.microsoft.com/dotnet/floating-point-parsing-and-formatting-improvements-in-net-core-3-0/). – Michael Liu Jul 31 '22 at 20:35
  • @AndrewMorton The results follow the `round half up` rule when I change `{1:F1}` to `{1:.#}`. – obnews Jul 31 '22 at 20:39
  • 1
    [This question](https://stackoverflow.com/questions/62748303/is-midpointrounding-awayfromzero-working-right-in-net-core-3-1) seems relevant – stuartd Jul 31 '22 at 20:44
  • 3
    Try changing your test to having a format specifier like `{1:F20}` and see what happens. Skimming through @MichaelLiu's link makes me believe that those changes are the rationale for what you are seeing. Remember: `float` and `double` are inexact representations – Flydog57 Jul 31 '22 at 21:02
  • WARNING: if you are doing any kind of statistical analysis, the "round up" rule puts a bias into your results. That's the reason all statisticians -- and most good stats tools like R, Julia, use the "round to even" rule. – Carl Witthoft Aug 01 '22 at 12:02
  • 2
    @CarlWitthoft It is called `Banker's Rounding`, I think .NET also use this rule if you see the answer from David Browne. – obnews Aug 01 '22 at 12:10
  • 1
    It does not matter what the rounding rule is. When rounding, you should take the accuracy of the value into account anyways, just like for any other floating point comparison. – fishinear Aug 01 '22 at 12:18
  • 3
    [Rounding of last digit changes after Windows .NET update](https://stackoverflow.com/q/57593059/995714), [Rounding issues .Net Core 3.1 vs. .Net Core 2.0/.Net Framework](https://stackoverflow.com/q/60147860/995714), [Rounding in .NET 5 vs. .NET 4.7.2](https://stackoverflow.com/q/67386732/995714) – phuclv Aug 01 '22 at 15:19
  • 1
    You should also be aware of [Is floating point math broken?](https://stackoverflow.com/q/588004/5987) Those numbers may not be exactly what you think they are. – Mark Ransom Aug 02 '22 at 02:11
  • 2
    This is a ***mega*** duplicate. Why isn't it closed instead of answered? There isn't any reason to answer the same basic questions over and over and over again. – Peter Mortensen Aug 02 '22 at 16:07
  • A start (2008)—not necessarily the canonical: *[Why does .NET use banker's rounding as default?](https://stackoverflow.com/questions/311696/)* – Peter Mortensen Aug 02 '22 at 16:10

2 Answers2

53

Use decimal, not double, otherwise you're not starting with the exact values you think you are, and you get the expected results.

9.05 ===> 9.1
9.15 ===> 9.2
9.25 ===> 9.3
9.35 ===> 9.4
9.45 ===> 9.5
9.55 ===> 9.6
9.65 ===> 9.7
9.75 ===> 9.8
9.85 ===> 9.9
9.95 ===> 10.0

With doubles, most of the values are slightly off from the decimal literals in the code, and so are rounded to the nearest number. Only two are actually at the midpoint, and are rounded to even in .NET Core. But as @Traveller points out this isn't general rounding behavior; it's specific to how floating point numbers are printed.

9.05000000000000071E+000 ===> 9.1 <- rounded to nearest
9.15000000000000036E+000 ===> 9.2 <- rounded to nearest
9.25000000000000000E+000 ===> 9.2 <- rounded to even
9.34999999999999964E+000 ===> 9.3 <- rounded to nearest
9.44999999999999929E+000 ===> 9.4 <- rounded to nearest
9.55000000000000071E+000 ===> 9.6 <- rounded to nearest
9.65000000000000036E+000 ===> 9.7 <- rounded to nearest
9.75000000000000000E+000 ===> 9.8 <- rounded to even
9.84999999999999964E+000 ===> 9.8 <- rounded to nearest
9.94999999999999929E+000 ===> 9.9 <- rounded to nearest
David Browne - Microsoft
  • 80,331
  • 6
  • 39
  • 67
  • 10
    That doesn't appear to explain the difference observed in the question. Changing the type of the variable is cheating ;) Is there official documentation to support the change? – Andrew Morton Jul 31 '22 at 20:21
  • 27
    @AndrewMorton I disagree. If the code operates on decimal values, for example 9.25, suggesting changing the type used is the right answer to give. – tymtam Aug 01 '22 at 01:56
  • 6
    @AndrewMorton Using doubles and then expected exact results is always wrong, If you need to round in a controlled fashion use doubles. https://stackoverflow.com/questions/588004/is-floating-point-math-broken – mmmmmm Aug 01 '22 at 16:48
  • 3
    @tymtam: Any of the other numbers would be better examples. `9.25` *is* exactly representable as a binary floating-point `double`, because it can be represented as a fraction with a power-of-2 denominator. (Try it for single-precision float in https://www.h-schmidt.net/FloatConverter/IEEE754.html). It's source values like 9.15 that are a problem, as you can see in David's table of the decimal values represented by the nearest double to 9.15, 9.25 etc. (i.e. the result of round-to-nearest applied at compile time to the source code text, before run-time rounding during conversion to a string.) – Peter Cordes Aug 02 '22 at 01:08
  • 1
    @PeterCordes Haha, what are the chances. I guess, the chances are 20% because 9.75 is the other exactly representative number. – tymtam Aug 02 '22 at 02:21
  • 1
    @tymtam This answers the second of the two questions, on the condition that it's an option to change the data type. I'd argue that's non-trivial in most real-world, brownfield scenarios. – Eric J. Aug 02 '22 at 19:48
  • @EricJ In my experience double->decimal is irrationality feared. (I acknowledge that this fear is a real obstacle to making the change.) – tymtam Aug 02 '22 at 21:34
  • 1
    @tymtam That really depends on the requirements of the program. Decimal is a great choice in many but not all scenarios. Decimal is significantly slower on most hardware because double has CPU support. Decimal takes twice the memory. Try running this code `double foo = Math.Pow(10, 29); decimal bar = (decimal)foo;`. Does the OverflowException matter? Depends. – Eric J. Aug 04 '22 at 17:01
  • *Decimal is significantly slower on most hardware.* The number of projects where this matters is tiny and people who do these understand the differences. And the context here is `ToString`. Also, a faster multiplication that produces an incorrect result is hardly a success. In Summary, I don't disagree with you about the differences and that there are scenarios where decimal doesn't fit (Re: your pow example = badam tsh), but for a .net website project where you send numbers over json, *decimal* is most likely a better choice and should be the default. (You can have the last word here). – tymtam Aug 05 '22 at 06:36
38

The Microsoft documentation have this info carefully hidden in the Standard numeric format strings page (it's probably elsewhere as well, but not in the Double.ToString docs).

Here's the important excerpt, for posterity:

When precision specifier controls the number of fractional digits in the result string, the result string reflects a number that is rounded to a representable result nearest to the infinitely precise result. If there are two equally near representable results:

  • On .NET Framework and .NET Core up to .NET Core 2.0, the runtime selects the result with the greater least significant digit (that is, using MidpointRounding.AwayFromZero).

  • On .NET Core 2.1 and later, the runtime selects the result with an even least significant digit (that is, using MidpointRounding.ToEven).

Since .Net 5 and later mostly continue the Core line despite Microsoft's confusing statements about how they've been merged, that'll pretty clearly fall under the 2nd case.

Traveller
  • 604
  • 6
  • 16
  • When `n=9.05`, `WriteLine("{0} ===> {1:F20}", n, n)` outputs `9.05 ===> 9.05000000000000071054`, how come 9.05 is not rounded to even in 2nd case? – obnews Aug 01 '22 at 00:39
  • 15
    Because `9.05000000000000071054` is _closer_ to `9.1` than it is to `9.0`. MidpointRounding only applies when you're actually at the midpoint, like with `9.25000000000000000` – David Browne - Microsoft Aug 01 '22 at 00:51
  • @DavidBrowne-Microsoft OK, I thought it was only decided by the second digit after the decimal point, regardless what behind this digit. – obnews Aug 01 '22 at 01:02
  • 9
    @obnews - one problem with round away from zero and working with only positive numbers (e.g. rounding you remember from school) is that you don't realise that the rounding rule *only* applies when you're exactly at the midpoint. You don't need to invoke a rounding rule to know that `9.59` is closer to `10` than to `9`. So when you're using a different rounding rule, you still wouldn't round `9.59` down to `9`. – Damien_The_Unbeliever Aug 01 '22 at 06:22
  • 2
    There is more info about .NET rounding on the [Math.Round](https://learn.microsoft.com/en-us/dotnet/api/system.math.round?view=net-6.0#remarks) page. – molnarm Aug 01 '22 at 14:55
  • 1
    @Damien_The_Unbeliever: Does C# let you set a rounding mode like IEEE roundTowardNegative, so 9.99 rounds to 9, and -9.01 rounds to -10? (floor rounding, towards -Infinity). Semi-related: the four rounding modes x86 hardware supports directly (for every math operation and for rounding to integer) are the default roundTiesToEven (C# MidpointRounding.ToEven), roundTowardPositive (ceil), roundTowardNegative (floor), and roundTowardZero (trunc). Anyway, if using a non-midpoint rounding rule for float -> string conversion (where SW has to do the work anyway), it could matter. – Peter Cordes Aug 02 '22 at 01:00
  • @Peter C# supports [the following](https://learn.microsoft.com/en-us/dotnet/api/system.midpointrounding?view=net-6.0) rounding modes. Iirc Banker's rounding is the default. – Voo Aug 02 '22 at 13:51