diff --git a/docs/core/compatibility/11.md b/docs/core/compatibility/11.md index b9a18809ad550..7af24d31fc0ef 100644 --- a/docs/core/compatibility/11.md +++ b/docs/core/compatibility/11.md @@ -27,6 +27,7 @@ See [Breaking changes in ASP.NET Core 11](/aspnet/core/breaking-changes/11/overv | [Complex special-value results now follow C23 Annex G](core-libraries/11/complex-annex-g-special-values.md) | Behavioral change | | [CRC32 validation added when reading ZIP archive entries](core-libraries/11/ziparchive-entry-crc32-validation.md) | Behavioral change | | [DateOnly and TimeOnly TryParse methods throw for invalid input](core-libraries/11/dateonly-timeonly-tryparse-argumentexception.md) | Behavioral change | +| [Decimal and BigInteger floating-point conversions are correctly rounded](core-libraries/11/decimal-biginteger-floating-point-conversions.md) | Behavioral change | | [DeflateStream and GZipStream write headers and footers for empty payload](core-libraries/11/deflatestream-gzipstream-empty-payload.md) | Behavioral change | | [Environment.TickCount made consistent with Windows timeout behavior](core-libraries/11/environment-tickcount-windows-behavior.md) | Behavioral change | | [Math.Round and MathF.Round return correctly rounded results](core-libraries/11/math-round-digits.md) | Behavioral change | diff --git a/docs/core/compatibility/core-libraries/11/decimal-biginteger-floating-point-conversions.md b/docs/core/compatibility/core-libraries/11/decimal-biginteger-floating-point-conversions.md new file mode 100644 index 0000000000000..b516275135e60 --- /dev/null +++ b/docs/core/compatibility/core-libraries/11/decimal-biginteger-floating-point-conversions.md @@ -0,0 +1,173 @@ +--- +title: "Breaking change: Decimal and BigInteger floating-point conversions are correctly rounded" +description: "Learn about the breaking change in .NET 11 where conversions between Decimal and binary floating-point types, and conversions from BigInteger to binary floating-point types, produce correctly rounded results." +ms.date: 08/26/2026 +ai-usage: ai-assisted +--- + +# Decimal and BigInteger floating-point conversions are correctly rounded + +Conversions between and binary floating-point types, and conversions from to binary floating-point types, now produce correctly rounded results. Previously, these conversions could truncate, round through intermediate values, or discard significant digits. + +## Version introduced + +.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: + +```csharp +using System.Globalization; + +double value = 1.23; +decimal converted = (decimal)value; + +Console.WriteLine(converted.ToString("G29", CultureInfo.InvariantCulture)); +``` + +The output was: + +```text +1.23 +``` + +Conversions from `decimal` to `float` or `double` could round more than once. For example: + +```csharp +using System.Globalization; + +decimal value = 10000000000000.099609375m; +double converted = (double)value; + +Console.WriteLine(converted.ToString("G99", CultureInfo.InvariantCulture)); +``` + +The output was: + +```text +10000000000000.09765625 +``` + +Conversions from `decimal` to `Half` or `BFloat16` first converted the value to `float`, which could also produce an incorrectly rounded result. + +Conversions from `BigInteger` to `double` truncated discarded bits instead of rounding to the nearest representable value. For example: + +```csharp +using System.Globalization; +using System.Numerics; + +BigInteger value = long.MaxValue / 2; +double converted = (double)value; + +Console.WriteLine(converted.ToString("G17", CultureInfo.InvariantCulture)); +``` + +The output was: + +```text +4.6116860184273874E+18 +``` + +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 + +Starting in .NET 11, conversions round the exact source value once to the nearest representable destination value. + +For the first example, the `double` value isn't exactly `1.23`. Its exact value is approximately `1.229999999999999982236431605997...`, so the converted `decimal` value is now: + +```text +1.229999999999999982236431606 +``` + +For the second example, the converted `double` value is now: + +```text +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: + +```text +4.6116860184273879E+18 +``` + +Conversions from `BigInteger` to `float`, `Half`, or `BFloat16` are also correctly rounded. + +If a conversion is evaluated as a compile-time constant, a compiler hosted by the .NET 11 Preview 7 SDK or a later SDK can embed the new result in the output assembly when the project is rebuilt, regardless of the project's target framework. + +## Type of breaking change + +This change is a [behavioral change](../../categories.md#behavioral-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 `decimal` conversion and in conversions from `BigInteger`. The new algorithms follow the expected floating-point rule of computation of the conversion as if with exact intermediate precision, followed by a single rounding to the destination type. + +For more information, see [dotnet/runtime#130565](https://github.com/dotnet/runtime/pull/130565) and [dotnet/runtime#130566](https://github.com/dotnet/runtime/pull/130566). + +## Recommended action + +Don't 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: + +```csharp +decimal value = 123.4567m; +``` + +instead of a conversion from a `double` literal: + +```csharp +decimal value = (decimal)123.4567; +``` + +To restore the previous result in general when you convert 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 didn't 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: + +```csharp +using System.Globalization; + +static decimal ConvertToDecimalLikePrevious(float value) +{ + Span text = stackalloc char[32]; + value.TryFormat(text, out int length, "G7", CultureInfo.InvariantCulture); + return decimal.Parse(text[..length], NumberStyles.Float, CultureInfo.InvariantCulture); +} + +static decimal ConvertToDecimalLikePrevious(double value) +{ + Span text = stackalloc char[32]; + value.TryFormat(text, out int length, "G15", CultureInfo.InvariantCulture); + return decimal.Parse(text[..length], NumberStyles.Float, CultureInfo.InvariantCulture); +} + +decimal fromFloat = ConvertToDecimalLikePrevious(14.1f); // 14.1 +decimal fromDouble = ConvertToDecimalLikePrevious(-123.4567); // -123.4567 +``` + +The span-based implementation doesn't 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 truncation 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 reproduce it through a numeric conversion. + +There's no compatibility switch to restore the previous conversion algorithms. + +## Affected APIs + +- +- +- Explicit conversions from and to +- Explicit conversions from to and +- +- +- +- +- +- +- , , and when they convert to or from or +- Explicit conversion from to +- Explicit conversion from to +- Explicit conversions from to , , , and +- Equivalent conversions performed through or the generic math interfaces diff --git a/docs/core/compatibility/toc.yml b/docs/core/compatibility/toc.yml index 0771ca5ae9dbb..5ce6d19f922f4 100644 --- a/docs/core/compatibility/toc.yml +++ b/docs/core/compatibility/toc.yml @@ -20,6 +20,8 @@ items: href: core-libraries/11/ziparchive-entry-crc32-validation.md - name: DateOnly and TimeOnly TryParse methods throw for invalid input href: core-libraries/11/dateonly-timeonly-tryparse-argumentexception.md + - name: Decimal and BigInteger floating-point conversions are correctly rounded + href: core-libraries/11/decimal-biginteger-floating-point-conversions.md - name: DeflateStream and GZipStream write headers and footers for empty payload href: core-libraries/11/deflatestream-gzipstream-empty-payload.md - name: Environment.TickCount made consistent with Windows timeout behavior diff --git a/docs/csharp/language-reference/builtin-types/numeric-conversions.md b/docs/csharp/language-reference/builtin-types/numeric-conversions.md index 722996b7379ed..2c32e9c65d095 100644 --- a/docs/csharp/language-reference/builtin-types/numeric-conversions.md +++ b/docs/csharp/language-reference/builtin-types/numeric-conversions.md @@ -91,6 +91,9 @@ Also note that: - If the source value is NaN (not a number), infinity, or too large to be represented as a `decimal`, an is thrown. - When you convert `decimal` to `float` or `double`, the source value is rounded to the nearest `float` or `double` value, respectively. + > [!NOTE] + > Starting in .NET 11, conversions between `decimal` and `float` or `double` are correctly rounded. Previously, these conversions could round through intermediate values or discard significant digits. For more information, see [Decimal and BigInteger floating-point conversions are correctly rounded](../../../core/compatibility/core-libraries/11/decimal-biginteger-floating-point-conversions.md). + ## C# language specification For more information, see the following sections of the [C# language specification](~/_csharpstandard/standard/README.md):