Skip to content

[Breaking change]: Math.Round/MathF.Round with digits return the correctly rounded result and accept any non-negative digit count #54762

Description

@tannergooding

Description

Math.Round(double, int[, MidpointRounding]) and MathF.Round(float, int[, MidpointRounding]) now return the value that is correctly rounded to the requested number of fractional digits based on the exact value of the input, rather than a close approximation produced by scaling the input by a power of 10. As a result, some inputs now round to a different (correct) result than in previous releases.

Additionally, the historical restriction that limited digits to 0-15 for double and 0-6 for float has been removed. Any non-negative digits value is now accepted; only negative values throw ArgumentOutOfRangeException. Digit counts at or beyond the precision needed to round-trip the type (17 for double, 9 for float) leave the value unchanged, which is the correct result.

Version

.NET 11 Preview 7

Previous behavior

Math.Round(value, digits, mode) computed Round(value * 10^digits, mode) / 10^digits. Because value * 10^digits is generally not exactly representable, values that were slightly below (or above) a decimal midpoint could be scaled into an exact midpoint (or across a rounding boundary), producing an incorrectly rounded result. Large magnitudes could also lose their fractional bits entirely during the scaling step. The digits argument was also capped at 0-15 (double) / 0-6 (float), throwing ArgumentOutOfRangeException outside that range.

Math.Round(655.925, 2, MidpointRounding.AwayFromZero);            // 655.93              (incorrect)
Math.Round(1111111111111111.5, 1, MidpointRounding.AwayFromZero); // 1111111111111111.6  (incorrect)
Math.Round(1.5, 16, MidpointRounding.ToEven);                    // throws ArgumentOutOfRangeException

(655.925 is stored as 655.924999999999954525…, which is below the 655.925 midpoint, so the correct result is 655.92.)

New behavior

The result is computed from the exact value of the input using arbitrary precision arithmetic and is the nearest representable value to the correctly rounded decimal result.

Math.Round(655.925, 2, MidpointRounding.AwayFromZero);            // 655.92              (correct)
Math.Round(1111111111111111.5, 1, MidpointRounding.AwayFromZero); // 1111111111111111.5  (correct)
Math.Round(1.5, 16, MidpointRounding.ToEven);                    // 1.5                 (no longer throws)

Type of breaking change

  • Behavioral change: Existing binaries might behave differently at run time.

Reason for change

The previous results were incorrect for a large fraction of inputs (roughly 5% of random values across the supported digits range differed from the correctly rounded result). The behavior also rejected/mis-handled large finite inputs. The new implementation is IEEE-consistent: it rounds the exact value of the input and returns the nearest representable result, matching the value that value.ToString("F{digits}") already produced.

The 0-15 / 0-6 digits cap was an artificial limitation tied to the old scale-by-10^digits approach; the exact implementation is correct for any digit count, so the cap has been lifted at the same time to avoid a second behavioral break later.

Recommended action

Most code needs no change and benefits from the corrected results.

  • If you depend on the exact prior (incorrect) output, round using the previous approach explicitly, e.g. Math.Round(value * pow10, mode) / pow10, or perform the rounding using decimal when the values represent base-10 quantities such as currency.
  • Reminder: double/float are binary floating-point types and cannot exactly represent most decimal fractions. For exact decimal rounding of decimal quantities, prefer decimal.

Feature area

Core .NET libraries

Affected APIs

  • System.Math.Round(double, int)
  • System.Math.Round(double, int, System.MidpointRounding)
  • System.MathF.Round(float, int)
  • System.MathF.Round(float, int, System.MidpointRounding)

The same corrected behavior and lifted digits range flow through the numeric interface entry points that delegate here, e.g. double.Round, float.Round, Half.Round, and NFloat.Round. Math.Round(decimal, ...) and the single-argument Math.Round(double) / MathF.Round(float) overloads are unaffected. Negative digits still throw ArgumentOutOfRangeException.


Implemented by dotnet/runtime#130574; fixes dotnet/runtime#1643.

Note

This issue was drafted by GitHub Copilot on @tannergooding''s behalf.

Metadata

Metadata

Assignees

Labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions