You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Starting in .NET 11 Preview 7, conversions between decimal and binary floating-point types, and conversions from BigInteger to binary floating-point types, produce correctly rounded results. Previously, these conversions could truncate, round through intermediate values, or discard significant digits.
The change affects conversions between decimal and float or double, and conversions from decimal or BigInteger to Half or BFloat16. It was introduced by dotnet/runtime#130565 and dotnet/runtime#130566.
The change can also affect compile-time constant folding. For example, a C# compiler running on the .NET 11 Preview 7 SDK can emit a different decimal value for a constant conversion from a double literal, even when the project targets an earlier version of .NET.
Version
.NET 11 Preview 7
Previous behavior
Conversions from float and double to decimal retained only 7 and 15 significant decimal digits, respectively. This could hide the difference between a binary floating-point literal and a decimal literal:
Conversions from BigInteger to float, Half, or BFloat16 first converted the value to double, which could round twice and produce a result one unit in the last place away from the nearest representable value.
New behavior
Conversions round the exact source value once to the nearest representable destination value.
For the first example, the double value is not exactly 1.23. Its exact value is approximately 1.229999999999999982236431605997..., so the converted decimal value is now:
1.229999999999999982236431606
For the second example, the converted double value is now:
10000000000000.099609375
Conversions from decimal to Half or BFloat16 are also correctly rounded and can produce a different destination bit pattern than in previous versions.
For the BigInteger example, the converted double value is now:
4.6116860184273879E+18
Conversions from BigInteger to float, Half, or BFloat16 are also correctly rounded.
When a conversion is evaluated as a compile-time constant, rebuilding with a compiler hosted by the .NET 11 Preview 7 SDK or a later SDK can embed the new result in the output assembly regardless of the project's target framework.
Type of breaking change
Binary incompatible: Existing binaries might encounter a breaking change in behavior, such as failure to load or execute, and if so, require recompilation.
Source incompatible: When recompiled using the new SDK or component or to target the new runtime, existing source code might require source changes to compile successfully.
Behavioral change: Existing binaries might behave differently at run time.
Reason for change
The previous conversion algorithms lost information that the destination type could represent and sometimes selected a value other than the nearest representable result. This caused precision errors in both directions of decimal conversion and in conversions from BigInteger. The new algorithms follow the expected floating-point rule of computing the conversion as if with exact intermediate precision and then rounding once to the destination type.
Recommended action
Do not assume that a binary floating-point literal and a decimal literal with the same source text represent the same value. If a value is intended to be decimal, use a decimal literal:
decimalvalue=123.4567m;
instead of converting a double literal:
decimalvalue=(decimal)123.4567;
To generally restore the previous result when converting to decimal, round the converted value to 7 significant decimal digits for a float source or 15 significant decimal digits for a double source. The previous conversion rounded to nearest with ties to even; it did not truncate. Positive and negative values were handled symmetrically.
For example, formatting the source value with the G7 or G15 standard numeric format string and parsing the result performs the corresponding significant-digit rounding for arbitrary finite values:
The span-based implementation does not allocate. Values outside the range of decimal continue to throw an exception during parsing, as they did during conversion.
Update tests and serialized expected values that encoded the previous, incorrectly rounded result. This includes code that depended on BigInteger conversion truncating toward zero. If an application requires a specific legacy float, double, Half, or BFloat16 bit pattern for a protocol or file format, encode that bit pattern explicitly rather than reproducing it through a numeric conversion.
There is no compatibility switch to restore the previous conversion algorithms.
Feature area
Core .NET libraries
Affected APIs
System.Decimal.Decimal(float)
System.Decimal.Decimal(double)
Explicit conversions from System.Single and System.Double to System.Decimal
Explicit conversions from System.Decimal to System.Single and System.Double
System.Decimal.ToSingle(decimal)
System.Decimal.ToDouble(decimal)
System.Convert.ToDecimal(float)
System.Convert.ToDecimal(double)
System.Convert.ToSingle(decimal)
System.Convert.ToDouble(decimal)
System.Decimal.CreateChecked<TOther>(TOther), System.Decimal.CreateSaturating<TOther>(TOther), and System.Decimal.CreateTruncating<TOther>(TOther) when converting to or from System.Single or System.Double
Explicit conversion from System.Decimal to System.Half
Explicit conversion from System.Decimal to System.Numerics.BFloat16
Explicit conversions from System.Numerics.BigInteger to System.Double, System.Single, System.Half, and System.Numerics.BFloat16
Equivalent conversions performed through System.IConvertible or the generic math interfaces
Note
This issue was drafted with AI assistance from GitHub Copilot.
Description
Starting in .NET 11 Preview 7, conversions between
decimaland binary floating-point types, and conversions fromBigIntegerto binary floating-point types, produce correctly rounded results. Previously, these conversions could truncate, round through intermediate values, or discard significant digits.The change affects conversions between
decimalandfloatordouble, and conversions fromdecimalorBigIntegertoHalforBFloat16. It was introduced by dotnet/runtime#130565 and dotnet/runtime#130566.The change can also affect compile-time constant folding. For example, a C# compiler running on the .NET 11 Preview 7 SDK can emit a different
decimalvalue for a constant conversion from adoubleliteral, even when the project targets an earlier version of .NET.Version
.NET 11 Preview 7
Previous behavior
Conversions from
floatanddoubletodecimalretained only 7 and 15 significant decimal digits, respectively. This could hide the difference between a binary floating-point literal and a decimal literal:The output was:
Conversions from
decimaltofloatordoublecould round more than once. For example:The output was:
Conversions from
decimaltoHalforBFloat16first converted the value tofloat, which could also produce an incorrectly rounded result.Conversions from
BigIntegertodoubletruncated discarded bits instead of rounding to the nearest representable value. For example:The output was:
Conversions from
BigIntegertofloat,Half, orBFloat16first converted the value todouble, which could round twice and produce a result one unit in the last place away from the nearest representable value.New behavior
Conversions round the exact source value once to the nearest representable destination value.
For the first example, the
doublevalue is not exactly1.23. Its exact value is approximately1.229999999999999982236431605997..., so the converteddecimalvalue is now:For the second example, the converted
doublevalue is now:Conversions from
decimaltoHalforBFloat16are also correctly rounded and can produce a different destination bit pattern than in previous versions.For the
BigIntegerexample, the converteddoublevalue is now:Conversions from
BigIntegertofloat,Half, orBFloat16are also correctly rounded.When a conversion is evaluated as a compile-time constant, rebuilding with a compiler hosted by the .NET 11 Preview 7 SDK or a later SDK can embed the new result in the output assembly regardless of the project's target framework.
Type of breaking change
Reason for change
The previous conversion algorithms lost information that the destination type could represent and sometimes selected a value other than the nearest representable result. This caused precision errors in both directions of
decimalconversion and in conversions fromBigInteger. The new algorithms follow the expected floating-point rule of computing the conversion as if with exact intermediate precision and then rounding once to the destination type.Recommended action
Do not assume that a binary floating-point literal and a decimal literal with the same source text represent the same value. If a value is intended to be decimal, use a decimal literal:
instead of converting a
doubleliteral:To generally restore the previous result when converting to
decimal, round the converted value to 7 significant decimal digits for afloatsource or 15 significant decimal digits for adoublesource. The previous conversion rounded to nearest with ties to even; it did not truncate. Positive and negative values were handled symmetrically.For example, formatting the source value with the
G7orG15standard numeric format string and parsing the result performs the corresponding significant-digit rounding for arbitrary finite values:The span-based implementation does not allocate. Values outside the range of
decimalcontinue to throw an exception during parsing, as they did during conversion.Update tests and serialized expected values that encoded the previous, incorrectly rounded result. This includes code that depended on
BigIntegerconversion truncating toward zero. If an application requires a specific legacyfloat,double,Half, orBFloat16bit pattern for a protocol or file format, encode that bit pattern explicitly rather than reproducing it through a numeric conversion.There is no compatibility switch to restore the previous conversion algorithms.
Feature area
Core .NET libraries
Affected APIs
System.Decimal.Decimal(float)System.Decimal.Decimal(double)System.SingleandSystem.DoubletoSystem.DecimalSystem.DecimaltoSystem.SingleandSystem.DoubleSystem.Decimal.ToSingle(decimal)System.Decimal.ToDouble(decimal)System.Convert.ToDecimal(float)System.Convert.ToDecimal(double)System.Convert.ToSingle(decimal)System.Convert.ToDouble(decimal)System.Decimal.CreateChecked<TOther>(TOther),System.Decimal.CreateSaturating<TOther>(TOther), andSystem.Decimal.CreateTruncating<TOther>(TOther)when converting to or fromSystem.SingleorSystem.DoubleSystem.DecimaltoSystem.HalfSystem.DecimaltoSystem.Numerics.BFloat16System.Numerics.BigIntegertoSystem.Double,System.Single,System.Half, andSystem.Numerics.BFloat16System.IConvertibleor the generic math interfacesNote
This issue was drafted with AI assistance from GitHub Copilot.
Associated WorkItem - 628410