ToString has a different behavior between .NET 462 and .NET Core 3.1

Viewed 208

I am migrating an application from the .NET Framework 4.6.2 to .NET Core 3.1 and my unit tests failed in a place I did not expect.

After some calculations, I ended up with a double with 16 digits. According to the debugger, I got the exact same value whether I am running the .NET462 or the .NETCore31 code. The difference occurs when I "serialize" this value. In the .NETCore31 version, the last digit is lost:

Here is an exemple:

.NET

4.0584789241077042.ToString("R", CultureInfo.InvariantCulture)
// "4.0584789241077042" (the exact same number)

.NET Core

4.0584789241077042.ToString("R", CultureInfo.InvariantCulture);
// "4.058478924107704" (the last digit is gone)

It is not actually an issue, my calculation does not require such precision, but does anyone know why I am getting two different results?

1 Answers

Many changes to floating-point were made in .NET Core 3.0, which Tanner lists in this article.

I think the one that concerns us is:

ToString(), ToString("G"), and ToString("R") will now return the shortest roundtrippable string. This ensures that users end up with something that just works by default. An example of where it was problematic was Math.PI.ToString() where the string that was previously being returned (for ToString() and ToString("G")) was 3.14159265358979; instead, it should have returned 3.1415926535897931. The previous result, when parsed, returned a value which was internally off by 7 ULP (units in last place) from the actual value of Math.PI. This meant that it was very easy for users to get into a scenario where they would accidentally lose some precision on a floating-point value when the needed to serialize/deserialize it.

So your value of 4.0584789241077042 is now round-tripped as the shortest value which can be roundtripped. In other words, even though the resulting string is missing the last decimal place ("4.058478924107704"), parsing that back to a double still gives 4.0584789241077042, due to the fact that the closest value to 4.058478924107704 which can be presented by an IEEE double is 4.0584789241077042.

double original = 4.0584789241077042;
Console.WriteLine("Original: {0:G17}", original);
// Original: 4.0584789241077042

string s = original.ToString("R", CultureInfo.InvariantCulture);
Console.WriteLine("Rouble-trippable: {0}", s);
// Rouble-trippable: 4.058478924107704

double parsed = double.Parse(s, CultureInfo.InvariantCulture);
Console.WriteLine("Parsed: {0:G17}", parsed);
// Parsed: 4.0584789241077042

Console.WriteLine("Original == Parsed: {0}", original == parsed);
// Original == Parsed: True
Related