Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/core/compatibility/11.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <xref:System.Decimal> and binary floating-point types, and conversions from <xref:System.Numerics.BigInteger> 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<char> 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<char> 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

- <xref:System.Decimal.%23ctor(System.Single)?displayProperty=nameWithType>
- <xref:System.Decimal.%23ctor(System.Double)?displayProperty=nameWithType>
Comment on lines +159 to +160

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- <xref:System.Decimal.%23ctor(System.Single)?displayProperty=nameWithType>
- <xref:System.Decimal.%23ctor(System.Double)?displayProperty=nameWithType>
- <xref:System.Decimal.%23ctor(System.Single)>
- <xref:System.Decimal.%23ctor(System.Double)>

- Explicit conversions from <xref:System.Single> and <xref:System.Double> to <xref:System.Decimal>
- Explicit conversions from <xref:System.Decimal> to <xref:System.Single> and <xref:System.Double>
- <xref:System.Decimal.ToSingle(System.Decimal)?displayProperty=nameWithType>
- <xref:System.Decimal.ToDouble(System.Decimal)?displayProperty=nameWithType>
- <xref:System.Convert.ToDecimal(System.Single)?displayProperty=nameWithType>
- <xref:System.Convert.ToDecimal(System.Double)?displayProperty=nameWithType>
- <xref:System.Convert.ToSingle(System.Decimal)?displayProperty=nameWithType>
- <xref:System.Convert.ToDouble(System.Decimal)?displayProperty=nameWithType>
- <xref:System.Decimal.CreateChecked``1(``0)?displayProperty=nameWithType>, <xref:System.Decimal.CreateSaturating``1(``0)?displayProperty=nameWithType>, and <xref:System.Decimal.CreateTruncating``1(``0)?displayProperty=nameWithType> when they convert to or from <xref:System.Single> or <xref:System.Double>
- Explicit conversion from <xref:System.Decimal> to <xref:System.Half>
- Explicit conversion from <xref:System.Decimal> to <xref:System.Numerics.BFloat16>
- Explicit conversions from <xref:System.Numerics.BigInteger> to <xref:System.Double>, <xref:System.Single>, <xref:System.Half>, and <xref:System.Numerics.BFloat16>
- Equivalent conversions performed through <xref:System.IConvertible> or the generic math interfaces
2 changes: 2 additions & 0 deletions docs/core/compatibility/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <xref:System.OverflowException> 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):
Expand Down
Loading