From d1e4bad227d6acfbc61ac7552e44b51f539583d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Aug 2026 15:00:39 +0000 Subject: [PATCH 1/5] Initial plan From 8dff6541a4a2d6c245b3d37efcfb988d7dcf3322 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Aug 2026 15:04:44 +0000 Subject: [PATCH 2/5] Add .NET 11 breaking change: Decimal/BigInteger floating-point conversions correctly rounded Co-authored-by: gewarren <24882762+gewarren@users.noreply.github.com> --- docs/core/compatibility/11.md | 1 + ...l-biginteger-floating-point-conversions.md | 173 ++++++++++++++++++ docs/core/compatibility/toc.yml | 2 + .../builtin-types/numeric-conversions.md | 3 + 4 files changed, 179 insertions(+) create mode 100644 docs/core/compatibility/core-libraries/11/decimal-biginteger-floating-point-conversions.md 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..7d718b9389b71 --- /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. + +When a conversion is evaluated as a compile-time constant, a rebuild 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 + +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, in general, the previous result 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 + +- `System.Decimal.Decimal(float)` +- `System.Decimal.Decimal(double)` +- 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 `System.Numerics.BFloat16` +- Explicit conversions from to , , , and `System.Numerics.BFloat16` +- 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..9e857777d5bd8 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 Preview 7, 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): From 296766f4caa721a810b44df989a8cbd527b6a467 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Aug 2026 15:05:39 +0000 Subject: [PATCH 3/5] Use xref links for Decimal constructors and BFloat16 in affected APIs list Co-authored-by: gewarren <24882762+gewarren@users.noreply.github.com> --- .../11/decimal-biginteger-floating-point-conversions.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 index 7d718b9389b71..7c46bc3a42874 100644 --- 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 @@ -156,8 +156,8 @@ There's no compatibility switch to restore the previous conversion algorithms. ## Affected APIs -- `System.Decimal.Decimal(float)` -- `System.Decimal.Decimal(double)` +- +- - Explicit conversions from and to - Explicit conversions from to and - @@ -168,6 +168,6 @@ There's no compatibility switch to restore the previous conversion algorithms. - - , , and when they convert to or from or - Explicit conversion from to -- Explicit conversion from to `System.Numerics.BFloat16` -- Explicit conversions from to , , , and `System.Numerics.BFloat16` +- Explicit conversion from to +- Explicit conversions from to , , , and - Equivalent conversions performed through or the generic math interfaces From c91d643217d80d0478ec2f506db117f5d6ae34bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Aug 2026 15:06:21 +0000 Subject: [PATCH 4/5] Apply writing-style fixes from code review (lead with reason, avoid gerund) Co-authored-by: gewarren <24882762+gewarren@users.noreply.github.com> --- .../11/decimal-biginteger-floating-point-conversions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 7c46bc3a42874..b516275135e60 100644 --- 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 @@ -97,7 +97,7 @@ For the `BigInteger` example, the converted `double` value is now: Conversions from `BigInteger` to `float`, `Half`, or `BFloat16` are also correctly rounded. -When a conversion is evaluated as a compile-time constant, a rebuild 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. +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 @@ -123,7 +123,7 @@ instead of a conversion from a `double` literal: decimal value = (decimal)123.4567; ``` -To restore, in general, the previous result 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. +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: From 0694bade76b93005c4a4a23dd5e9cdac69b9c00d Mon Sep 17 00:00:00 2001 From: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:24:33 -0700 Subject: [PATCH 5/5] Update docs/csharp/language-reference/builtin-types/numeric-conversions.md --- .../language-reference/builtin-types/numeric-conversions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/csharp/language-reference/builtin-types/numeric-conversions.md b/docs/csharp/language-reference/builtin-types/numeric-conversions.md index 9e857777d5bd8..2c32e9c65d095 100644 --- a/docs/csharp/language-reference/builtin-types/numeric-conversions.md +++ b/docs/csharp/language-reference/builtin-types/numeric-conversions.md @@ -92,7 +92,7 @@ Also note that: - 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 Preview 7, 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). + > 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