diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.Round.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.Round.cs index ac210cb2d3a293..78dc033bbfeee5 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.Round.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.Round.cs @@ -113,60 +113,21 @@ public static void Round(ReadOnlySpan x, int digits, MidpointRounding mode return; } - ReadOnlySpan roundPower10; - if (typeof(T) == typeof(float)) - { - ReadOnlySpan roundPower10Single = [1e0f, 1e1f, 1e2f, 1e3f, 1e4f, 1e5f, 1e6f]; - roundPower10 = Rename(roundPower10Single); - } - else if (typeof(T) == typeof(double)) - { - Debug.Assert(typeof(T) == typeof(double)); - ReadOnlySpan roundPower10Double = [1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15]; - roundPower10 = Rename(roundPower10Double); - } - else - { - if ((uint)mode > (uint)MidpointRounding.ToPositiveInfinity) - { - throw new ArgumentException(SR.Format(SR.Argument_InvalidEnumValue, mode, typeof(MidpointRounding)), nameof(mode)); - } - - InvokeSpanIntoSpan(x, new RoundFallbackOperator(digits, mode), destination); - return; - } - - if ((uint)digits >= (uint)roundPower10.Length) + if (digits < 0) { throw new ArgumentOutOfRangeException(nameof(digits)); } - T power10 = roundPower10[digits]; - switch (mode) + if ((uint)mode > (uint)MidpointRounding.ToPositiveInfinity) { - case MidpointRounding.ToEven: - InvokeSpanIntoSpan(x, new MultiplyRoundDivideOperator>(power10), destination); - return; - - case MidpointRounding.AwayFromZero: - InvokeSpanIntoSpan(x, new MultiplyRoundDivideOperator>(power10), destination); - return; - - case MidpointRounding.ToZero: - InvokeSpanIntoSpan(x, new MultiplyRoundDivideOperator>(power10), destination); - return; - - case MidpointRounding.ToNegativeInfinity: - InvokeSpanIntoSpan(x, new MultiplyRoundDivideOperator>(power10), destination); - return; - - case MidpointRounding.ToPositiveInfinity: - InvokeSpanIntoSpan(x, new MultiplyRoundDivideOperator>(power10), destination); - return; - - default: - throw new ArgumentException(SR.Format(SR.Argument_InvalidEnumValue, mode, typeof(MidpointRounding)), nameof(mode)); + throw new ArgumentException(SR.Format(SR.Argument_InvalidEnumValue, mode, typeof(MidpointRounding)), nameof(mode)); } + + // The digit-based rounding defers to the scalar `T.Round` for every element, which accepts any + // non-negative `digits` (matching the scalar API). A correctly-rounded vectorized implementation + // needs the exact (e.g. double-double or arbitrary precision) scaled value to match the scalar + // result at the midpoints, so that acceleration is left as a future improvement. + InvokeSpanIntoSpan(x, new RoundFallbackOperator(digits, mode), destination); } /// T.Round(x) @@ -279,57 +240,6 @@ public static Vector512 Invoke(Vector512 x) } } - /// (T.Round(x * power10, digits, mode)) / power10 - private readonly struct MultiplyRoundDivideOperator : IStatefulUnaryOperator - where T : IFloatingPoint - where TDelegatedRound : IUnaryOperator - { - private readonly T _factor; - - public MultiplyRoundDivideOperator(T factor) - { - Debug.Assert(typeof(T) == typeof(float) || typeof(T) == typeof(double)); - _factor = factor; - } - - public static bool Vectorizable => true; - - private const float Single_RoundLimit = 1e8f; - private const double Double_RoundLimit = 1e16d; - - public T Invoke(T x) - { - T limit = typeof(T) == typeof(float) ? T.CreateTruncating(Single_RoundLimit) : T.CreateTruncating(Double_RoundLimit); - return T.Abs(x) < limit ? - TDelegatedRound.Invoke(x * _factor) / _factor : - x; - } - - public Vector128 Invoke(Vector128 x) - { - Vector128 limit = Vector128.Create(typeof(T) == typeof(float) ? T.CreateTruncating(Single_RoundLimit) : T.CreateTruncating(Double_RoundLimit)); - return Vector128.ConditionalSelect(Vector128.LessThan(Vector128.Abs(x), limit), - TDelegatedRound.Invoke(x * _factor) / _factor, - x); - } - - public Vector256 Invoke(Vector256 x) - { - Vector256 limit = Vector256.Create(typeof(T) == typeof(float) ? T.CreateTruncating(Single_RoundLimit) : T.CreateTruncating(Double_RoundLimit)); - return Vector256.ConditionalSelect(Vector256.LessThan(Vector256.Abs(x), limit), - TDelegatedRound.Invoke(x * _factor) / _factor, - x); - } - - public Vector512 Invoke(Vector512 x) - { - Vector512 limit = Vector512.Create(typeof(T) == typeof(float) ? T.CreateTruncating(Single_RoundLimit) : T.CreateTruncating(Double_RoundLimit)); - return Vector512.ConditionalSelect(Vector512.LessThan(Vector512.Abs(x), limit), - TDelegatedRound.Invoke(x * _factor) / _factor, - x); - } - } - /// T.Round(x, digits, mode) private readonly struct RoundFallbackOperator(int digits, MidpointRounding mode) : IStatefulUnaryOperator where T : IFloatingPoint diff --git a/src/libraries/System.Numerics.Tensors/tests/TensorPrimitives.Generic.cs b/src/libraries/System.Numerics.Tensors/tests/TensorPrimitives.Generic.cs index a9356f84faa04c..856ea2d719690a 100644 --- a/src/libraries/System.Numerics.Tensors/tests/TensorPrimitives.Generic.cs +++ b/src/libraries/System.Numerics.Tensors/tests/TensorPrimitives.Generic.cs @@ -1666,7 +1666,7 @@ public static IEnumerable RoundData() { foreach (MidpointRounding mode in Enum.GetValues(typeof(MidpointRounding))) { - foreach (int digits in new[] { 0, 1, 4 }) + foreach (int digits in new[] { 0, 1, 4, 20 }) { yield return new object[] { mode, digits }; } diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index d05352ecb6fa96..186eda04f629eb 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -1935,10 +1935,7 @@ Valid values are between {0} and {1}, inclusive. - Rounding digits must be between 0 and 15, inclusive. - - - Rounding digits must be between 0 and 6, inclusive. + Rounding digits must be greater than or equal to 0. capacity was less than the current size. diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 11aee01cb8f771..9670007e075ff4 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -617,6 +617,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/System/Math.cs b/src/libraries/System.Private.CoreLib/src/System/Math.cs index ef2da391b440df..2e7340a1c4a7d9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Math.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Math.cs @@ -27,16 +27,13 @@ public static partial class Math public const double Tau = 6.283185307179586476925; - private const int maxRoundingDigits = 15; + // The largest digit count the fast rounding path handles: `10^digits` must fit a `ulong` for the + // integer fallback (10^19 is the last that does); larger counts use the exact routine. + private const int maxFastRoundingDigits = 19; - private const double doubleRoundLimit = 1e16d; - - // This table is required for the Round function which can specify the number of digits to round to - private static ReadOnlySpan RoundPower10Double => - [ - 1E0, 1E1, 1E2, 1E3, 1E4, 1E5, 1E6, 1E7, 1E8, - 1E9, 1E10, 1E11, 1E12, 1E13, 1E14, 1E15 - ]; + // Below this boundary a double may have a fractional portion; at or above it every + // representable value is already an integer (2^52). + private const double doubleIntegerBoundary = 4503599627370496.0; private const double SCALEB_C1 = 8.98846567431158E+307; // 0x1p1023 @@ -1406,15 +1403,35 @@ public static double Round(double value, MidpointRounding mode) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double Round(double value, int digits, MidpointRounding mode) { - if ((uint)digits > maxRoundingDigits) + if (digits < 0) { ThrowHelper.ThrowArgumentOutOfRange_RoundingDigits(nameof(digits)); } - if (Abs(value) < doubleRoundLimit) + if ((uint)mode > (uint)MidpointRounding.ToPositiveInfinity) { - double power10 = RoundPower10Double[digits]; - value = Round(value * power10, mode) / power10; + ThrowHelper.ThrowArgumentException_InvalidEnumValue(mode); + } + + // Rounding to zero fractional digits is just rounding to an integer, which the dedicated + // overload does with a single hardware instruction on most platforms. + if (digits == 0) + { + return Round(value, mode); + } + + // Only finite values with a magnitude below the integer boundary can have a fractional + // portion to round. All other values (including NaN and Infinity) are returned unchanged; + // this comparison is naturally false for those cases. + if (Abs(value) < doubleIntegerBoundary) + { + // The fast path only handles the small-digit range where `10^digits` is exactly + // representable; larger counts fall back to the exact arbitrary-precision routine. + if ((digits > maxFastRoundingDigits) || !Number.TryRoundToDecimalDigitsFast(value, digits, mode, out double rounded)) + { + rounded = Number.RoundToDecimalDigits(value, digits, mode); + } + value = rounded; } return value; diff --git a/src/libraries/System.Private.CoreLib/src/System/MathF.cs b/src/libraries/System.Private.CoreLib/src/System/MathF.cs index 01f0d31f701694..3571a40be1c8fd 100644 --- a/src/libraries/System.Private.CoreLib/src/System/MathF.cs +++ b/src/libraries/System.Private.CoreLib/src/System/MathF.cs @@ -26,15 +26,13 @@ public static partial class MathF public const float Tau = 6.283185307f; - private const int maxRoundingDigits = 6; + // The largest digit count the fast rounding path handles: `10^digits` must be exactly representable + // as a `float` (10^10 is the last that is); larger counts use the exact routine. + private const int maxFastRoundingDigits = 10; - // This table is required for the Round function which can specify the number of digits to round to - private static ReadOnlySpan RoundPower10Single => - [ - 1e0f, 1e1f, 1e2f, 1e3f, 1e4f, 1e5f, 1e6f - ]; - - private const float singleRoundLimit = 1e8f; + // Below this boundary a float may have a fractional portion; at or above it every + // representable value is already an integer (2^23). + private const float singleIntegerBoundary = 8388608.0f; private const float SCALEB_C1 = 1.7014118E+38f; // 0x1p127f @@ -429,15 +427,35 @@ public static float Round(float x, MidpointRounding mode) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float Round(float x, int digits, MidpointRounding mode) { - if ((uint)digits > maxRoundingDigits) + if (digits < 0) { - ThrowHelper.ThrowArgumentOutOfRange_RoundingDigits_MathF(nameof(digits)); + ThrowHelper.ThrowArgumentOutOfRange_RoundingDigits(nameof(digits)); } - if (Abs(x) < singleRoundLimit) + if ((uint)mode > (uint)MidpointRounding.ToPositiveInfinity) { - float power10 = RoundPower10Single[digits]; - x = Round(x * power10, mode) / power10; + ThrowHelper.ThrowArgumentException_InvalidEnumValue(mode); + } + + // Rounding to zero fractional digits is just rounding to an integer, which the dedicated + // overload does with a single hardware instruction on most platforms. + if (digits == 0) + { + return Round(x, mode); + } + + // Only finite values with a magnitude below the integer boundary can have a fractional + // portion to round. All other values (including NaN and Infinity) are returned unchanged; + // this comparison is naturally false for those cases. + if (Abs(x) < singleIntegerBoundary) + { + // The fast path only handles the small-digit range where `10^digits` is exactly + // representable; larger counts fall back to the exact arbitrary-precision routine. + if ((digits > maxFastRoundingDigits) || !Number.TryRoundToDecimalDigitsFast(x, digits, mode, out float rounded)) + { + rounded = Number.RoundToDecimalDigits(x, digits, mode); + } + x = rounded; } return x; diff --git a/src/libraries/System.Private.CoreLib/src/System/Number.Rounding.cs b/src/libraries/System.Private.CoreLib/src/System/Number.Rounding.cs new file mode 100644 index 00000000000000..c6efa31b6c5f29 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Number.Rounding.cs @@ -0,0 +1,433 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Numerics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; + +namespace System +{ + internal static partial class Number + { + // Attempts to round `value` to `digits` fractional decimal digits without resorting to the + // arbitrary precision arithmetic used by `RoundToDecimalDigits`. It produces the exact same + // (correctly rounded) result as that routine when it succeeds, and returns `false` otherwise + // so the caller can fall back. + // + // The double-double `FusedMultiplyAdd` approach is fastest wherever the hardware provides a + // fused-multiply-add (FMA3 on x86, baseline on Arm64); off such hardware it degrades to slow + // software emulation, so we fall back to the exact integer approach there instead. + public static bool TryRoundToDecimalDigitsFast(double value, int digits, MidpointRounding mode, out double result) + { + Debug.Assert(double.IsFinite(value)); + Debug.Assert((uint)digits <= 19); + Debug.Assert((uint)mode <= (uint)MidpointRounding.ToPositiveInfinity); + + return (Fma.IsSupported || AdvSimd.Arm64.IsSupported) + ? TryRoundToDecimalDigitsViaFusedMultiplyAdd(value, digits, mode, out result) + : TryRoundToDecimalDigitsViaInteger(value, digits, mode, out result); + } + + /// + public static bool TryRoundToDecimalDigitsFast(float value, int digits, MidpointRounding mode, out float result) + { + Debug.Assert(float.IsFinite(value)); + Debug.Assert((uint)digits <= 10); + Debug.Assert((uint)mode <= (uint)MidpointRounding.ToPositiveInfinity); + + return (Fma.IsSupported || AdvSimd.Arm64.IsSupported) + ? TryRoundToDecimalDigitsViaFusedMultiplyAdd(value, digits, mode, out result) + : TryRoundToDecimalDigitsViaInteger(value, digits, mode, out result); + } + + // The scaled value `|value| * 10^digits` is computed as an exact `hi + lo` double-double via a + // fused-multiply-add. `10^digits` is exactly representable for the supported `digits` range, so + // the only rounding is the single one folded into `hi`, which `lo` recovers exactly. We can then + // determine the correctly rounded integer part directly, and materialize the result with a single + // correctly rounded division so long as that integer is exactly representable (which is guaranteed + // while `hi` stays below the point where every value is already an integer). + private static bool TryRoundToDecimalDigitsViaFusedMultiplyAdd(TNumber value, int digits, MidpointRounding mode, out TNumber result) + where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo + { + TNumber one = TNumber.One; + + // `10^digits` is exact for every supported `digits` and comes from the shared powers-of-ten table. + TNumber pow10 = TNumber.CreateTruncating(Pow10DoubleTable[digits]); + + TNumber av = TNumber.Abs(value); + TNumber hi = av * pow10; + TNumber lo = TNumber.FusedMultiplyAdd(av, pow10, -hi); + + // `2^(NormalMantissaBits - 1)` is the point at or above which every representable value is already + // an integer. If the scaled value reaches it, the rounded integer would no longer be exactly + // representable and the final division would no longer be correctly rounded. + TNumber integerBoundary = TNumber.ScaleB(one, TNumber.NormalMantissaBits - 1); + + if (hi >= integerBoundary) + { + result = default; + return false; + } + + bool isNegative = TNumber.IsNegative(value); + TNumber zero = TNumber.Zero; + + // The nearest integer to `hi` is exactly representable, and `hi - rn` is exact (its magnitude + // is at most 1/2). The exact fractional part of the scaled value relative to `rn` is therefore + // `(hi - rn) + lo`, which we hold exactly as the double-double `s + e` via a two-sum. + TNumber rn = TNumber.Round(hi, MidpointRounding.ToEven); + TNumber diff = hi - rn; + + TNumber s = diff + lo; + TNumber bb = s - diff; + TNumber e = (diff - (s - bb)) + (lo - bb); + + int signOfResidual = (s > zero) ? 1 : (s < zero) ? -1 : (e > zero) ? 1 : (e < zero) ? -1 : 0; + bool hasRemainder = (s != zero) || (e != zero); + + TNumber half = one / (one + one); + + // `floor` of the scaled value and how its fractional part compares to the midpoint `0.5`. + TNumber floor; + int midpointComparison; + + if (signOfResidual >= 0) + { + floor = rn; + midpointComparison = CompareResidualToThreshold(s, e, half); + } + else + { + floor = rn - one; + midpointComparison = CompareResidualToThreshold(s, e, -half); + } + + bool isFloorOdd = (long.CreateTruncating(floor) & 1L) != 0L; + bool roundUp = ShouldRoundUp(mode, midpointComparison, isFloorOdd, hasRemainder, isNegative); + + TNumber quotient = roundUp ? (floor + one) : floor; + + // `quotient` and `pow10` are both exact, so this division is correctly rounded and yields the + // nearest representable value to the exactly rounded decimal result. + TNumber rounded = quotient / pow10; + result = isNegative ? -rounded : rounded; + return true; + } + + // Rounds by operating on the exact value directly. With `value = mantissa * 2^exponent`, the scaled + // value `|value| * 10^digits` is the integer `mantissa * 10^digits` right-shifted by `-exponent`, + // so the floor and the exact comparison of the discarded fraction to `1/2` are pure integer work. + // The `mantissa * 10^digits` product stays in a `ulong` for the common case and only widens to a + // `UInt128` when it overflows 64 bits, which cannot happen for `float`. + private static bool TryRoundToDecimalDigitsViaInteger(double value, int digits, MidpointRounding mode, out double result) + { + ulong mantissa = ExtractFractionAndBiasedExponent(value, out int exponent); + bool isNegative = double.IsNegative(value); + + // When `exponent + digits >= 0` the scaled value is already an integer, so `value` is an exact + // multiple of `10^-digits` and rounding leaves it unchanged. + if ((exponent + digits) >= 0) + { + result = value; + return true; + } + + int shift = -exponent; + ulong high = Math.BigMul(mantissa, (ulong)Pow10DoubleTable[digits], out ulong low); + + ulong floor; + int midpointComparison; + bool hasRemainder; + + bool inRange = (high == 0) + ? TryGetFloorAndMidpoint(low, shift, DoubleIntegerBoundaryLog2, out floor, out midpointComparison, out hasRemainder) + : TryGetFloorAndMidpoint(new UInt128(high, low), shift, DoubleIntegerBoundaryLog2, out floor, out midpointComparison, out hasRemainder); + + if (!inRange) + { + result = default; + return false; + } + + bool isFloorOdd = (floor & 1) != 0; + bool roundUp = ShouldRoundUp(mode, midpointComparison, isFloorOdd, hasRemainder, isNegative); + + ulong quotient = roundUp ? (floor + 1) : floor; + + // `quotient` (<= 2^52) and `10^digits` are both exact, so this division is correctly rounded. + double rounded = quotient / Pow10DoubleTable[digits]; + result = isNegative ? -rounded : rounded; + return true; + } + + /// + private static bool TryRoundToDecimalDigitsViaInteger(float value, int digits, MidpointRounding mode, out float result) + { + ulong mantissa = ExtractFractionAndBiasedExponent(value, out int exponent); + bool isNegative = float.IsNegative(value); + + if ((exponent + digits) >= 0) + { + result = value; + return true; + } + + int shift = -exponent; + + // `mantissa` (< 2^24) times `10^digits` (<= 10^10) is at most ~2^57, so it never overflows a ulong. + ulong scaled = mantissa * (ulong)Pow10DoubleTable[digits]; + + if (!TryGetFloorAndMidpoint(scaled, shift, SingleIntegerBoundaryLog2, out ulong floor, out int midpointComparison, out bool hasRemainder)) + { + result = default; + return false; + } + + bool isFloorOdd = (floor & 1) != 0; + bool roundUp = ShouldRoundUp(mode, midpointComparison, isFloorOdd, hasRemainder, isNegative); + + ulong quotient = roundUp ? (floor + 1) : floor; + + // `quotient` (<= 2^23) and `10^digits` are both exact in `float`, so this division is correctly rounded. + float rounded = quotient / (float)Pow10DoubleTable[digits]; + result = isNegative ? -rounded : rounded; + return true; + } + + // `2^IntegerBoundaryLog2` is the point at or above which every representable value is already an + // integer; a floor that reaches it could not be materialized exactly by the final division. + private const int DoubleIntegerBoundaryLog2 = 52; + private const int SingleIntegerBoundaryLog2 = 23; + + // Computes `floor(scaled / 2^shift)` and how the discarded fraction `(scaled mod 2^shift) / 2^shift` + // compares to the `1/2` midpoint, where `scaled` is the exact `mantissa * 10^digits`. Returns false + // when the integer part would not be exactly representable so the caller can fall back. + private static unsafe bool TryGetFloorAndMidpoint(TUInt scaled, int shift, int integerBoundaryLog2, out ulong floor, out int midpointComparison, out bool hasRemainder) + where TUInt : unmanaged, IBinaryInteger, IUnsignedNumber + { + int bitWidth = sizeof(TUInt) * 8; + + if (shift >= bitWidth) + { + // `scaled < 2^bitWidth <= 2^shift`, so the integer part is zero. The midpoint `2^(shift-1)` + // only overlaps the value's range when `shift == bitWidth`; when `shift > bitWidth` the + // fraction is strictly below `1/2`, even when `scaled` is itself zero. + floor = 0; + hasRemainder = scaled != TUInt.Zero; + + if (shift == bitWidth) + { + TUInt half = TUInt.One << (bitWidth - 1); + midpointComparison = (scaled < half) ? -1 : (scaled > half) ? 1 : 0; + } + else + { + midpointComparison = -1; + } + return true; + } + + TUInt integerPart = scaled >> shift; + + if (integerPart >= (TUInt.One << integerBoundaryLog2)) + { + floor = 0; + midpointComparison = 0; + hasRemainder = false; + return false; + } + + TUInt remainder = scaled - (integerPart << shift); + hasRemainder = remainder != TUInt.Zero; + + TUInt half2 = TUInt.One << (shift - 1); + midpointComparison = (remainder < half2) ? -1 : (remainder > half2) ? 1 : 0; + + floor = ulong.CreateTruncating(integerPart); + return true; + } + + // Resolves whether the floor should be incremented for the given rounding `mode`, using the sign of + // the fractional part relative to the `1/2` midpoint (`midpointComparison`) and whether the floor is + // odd. `mode` is validated by the caller, so the final arm is unreachable. + private static bool ShouldRoundUp(MidpointRounding mode, int midpointComparison, bool isFloorOdd, bool hasRemainder, bool isNegative) + { + return mode switch + { + MidpointRounding.ToEven => (midpointComparison > 0) || ((midpointComparison == 0) && isFloorOdd), + MidpointRounding.AwayFromZero => midpointComparison >= 0, + MidpointRounding.ToZero => false, + MidpointRounding.ToNegativeInfinity => isNegative && hasRemainder, + MidpointRounding.ToPositiveInfinity => !isNegative && hasRemainder, + _ => throw new UnreachableException(), + }; + } + + // Compares the exact residual `s + e` against `threshold` (one of `+/-0.5`), returning the sign + // of the difference. `s - threshold` is exact for these inputs, so it decides the result unless + // it is zero, in which case the low part `e` breaks the tie exactly. + private static int CompareResidualToThreshold(TNumber s, TNumber e, TNumber threshold) + where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo + { + TNumber zero = TNumber.Zero; + TNumber g = s - threshold; + + if (g != zero) + { + return (g > zero) ? 1 : -1; + } + + return (e > zero) ? 1 : (e < zero) ? -1 : 0; + } + + // Rounds the exact value represented by `value` to `digits` fractional decimal digits + // using the specified `mode`, returning the nearest representable result. + // + // Unlike scaling the input by a power of 10 and rounding, this operates on the exact + // value of the input using arbitrary precision arithmetic and so produces the correctly + // rounded result for all finite inputs, including those that would otherwise appear to be + // a midpoint after an inexact scaling (e.g. `655.924999999999954525...` scaling to exactly + // `65592.5` when multiplied by `100`). + // + // The caller is responsible for handling values which cannot have a fractional portion at + // the requested number of digits (namely non-finite values and values whose magnitude is at + // or above the point where all representable values are integers). + public static unsafe TNumber RoundToDecimalDigits(TNumber value, int digits, MidpointRounding mode) + where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo + { + Debug.Assert(TNumber.IsFinite(value)); + Debug.Assert(digits >= 0); + + bool isNegative = TNumber.IsNegative(value); + + // Decompose the input into `mantissa * 2^exponent`, giving us the exact value. + ulong mantissa = ExtractFractionAndBiasedExponent(value, out int exponent); + + // When `exponent + digits >= 0` the scaled value is already an integer, so `value` is an + // exact multiple of `10^-digits` and rounding leaves it unchanged. This also bounds the + // work below: only `digits < -exponent` reaches the arbitrary-precision arithmetic, keeping + // both the `BigInteger` and the digit buffer within their fixed capacities. The widening to + // `long` avoids overflow for pathologically large `digits`. + if (((long)exponent + digits) >= 0) + { + return value; + } + + // We want the nearest integer to `|value| * 10^digits`, which is `numerator / denominator` + // where both are computed exactly. The `2^exponent` term stays in the numerator when the + // exponent is non-negative and moves to the denominator otherwise. + + BigInteger.SetUInt64(out BigInteger numerator, mantissa); + BigInteger denominator; + + if (exponent >= 0) + { + numerator.ShiftLeft(exponent); + numerator.MultiplyPow10((uint)digits); + BigInteger.SetUInt32(out denominator, 1); + } + else + { + numerator.MultiplyPow10((uint)digits); + BigInteger.Pow2(-exponent, out denominator); + } + + BigInteger.DivRem(ref numerator, ref denominator, out BigInteger quotient, out BigInteger remainder); + + // The fractional portion we are rounding is `remainder / denominator`. Comparing + // `2 * remainder` against `denominator` tells us whether it is below, exactly at, or + // above the midpoint without any loss of precision. + + bool hasRemainder = !remainder.IsZero(); + remainder.Multiply(2); + int midpointComparison = BigInteger.Compare(ref remainder, ref denominator); + + bool roundUp; + + switch (mode) + { + // Rounds to the nearest value; if the number falls midway, it is rounded to the + // nearest value with an even least significant digit. + case MidpointRounding.ToEven: + roundUp = (midpointComparison > 0) || ((midpointComparison == 0) && !quotient.IsZero() && ((quotient.GetBlock(0) & 1) != 0)); + break; + + // Rounds to the nearest value; if the number falls midway, it is rounded to the + // nearest value away from zero. + case MidpointRounding.AwayFromZero: + roundUp = midpointComparison >= 0; + break; + + // Directed rounding: round toward zero. + case MidpointRounding.ToZero: + roundUp = false; + break; + + // Directed rounding: round toward negative infinity. + case MidpointRounding.ToNegativeInfinity: + roundUp = isNegative && hasRemainder; + break; + + // Directed rounding: round toward positive infinity. + case MidpointRounding.ToPositiveInfinity: + roundUp = !isNegative && hasRemainder; + break; + + default: + ThrowHelper.ThrowArgumentException_InvalidEnumValue(mode); + return default; + } + + if (roundUp) + { + quotient.Add(1); + } + + // `quotient` is now the exactly rounded integer value of `|value| * 10^digits`. The final + // result is the nearest representable value to `quotient * 10^-digits`, which we obtain by + // materializing the decimal digits and letting the shared conversion perform the correctly + // rounded decimal-to-binary step. + + byte* pDigits = stackalloc byte[TNumber.NumberBufferLength]; + NumberBuffer number = new NumberBuffer(NumberBufferKind.FloatingPoint, pDigits, TNumber.NumberBufferLength); + number.IsNegative = isNegative; + + Span buffer = number.Digits; + int digitCount = 0; + + if (!quotient.IsZero()) + { + BigInteger.SetUInt32(out BigInteger ten, 10); + + // Extract the digits least-significant first. + do + { + BigInteger.DivRem(ref quotient, ref ten, out BigInteger newQuotient, out BigInteger digit); + uint digitValue = digit.IsZero() ? 0 : digit.GetBlock(0); + buffer[digitCount++] = (byte)('0' + digitValue); + BigInteger.SetValue(out quotient, ref newQuotient); + } + while (!quotient.IsZero()); + + // The decimal point sits `digits` places to the left of the least significant digit, + // so the scale (number of digits to the left of the decimal point) is `digitCount - digits`. + number.Scale = digitCount - digits; + + // Reorder to most-significant first, as expected by NumberBuffer. + buffer.Slice(0, digitCount).Reverse(); + + // Trailing zeros carry no value and are not stored in a NumberBuffer. + while ((digitCount > 0) && (buffer[digitCount - 1] == '0')) + { + digitCount--; + } + } + + buffer[digitCount] = (byte)'\0'; + number.DigitsCount = digitCount; + + return NumberToFloat(ref number); + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs b/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs index 79b1e6e9aba57a..a353d6b294f498 100644 --- a/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs @@ -248,12 +248,6 @@ internal static void ThrowArgumentOutOfRange_RoundingDigits(string name) throw new ArgumentOutOfRangeException(name, SR.ArgumentOutOfRange_RoundingDigits); } - [DoesNotReturn] - internal static void ThrowArgumentOutOfRange_RoundingDigits_MathF(string name) - { - throw new ArgumentOutOfRangeException(name, SR.ArgumentOutOfRange_RoundingDigits_MathF); - } - [DoesNotReturn] internal static void ThrowArgumentOutOfRange_Range(string parameterName, T value, T minInclusive, T maxInclusive) { diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/Math.cs b/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/Math.cs index 316650591fd64f..c185c2baf57d87 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/Math.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/Math.cs @@ -3,7 +3,9 @@ using Xunit; using System.Collections.Generic; +using System.Diagnostics; using System.Runtime.CompilerServices; +using Microsoft.DotNet.RemoteExecutor; #pragma warning disable xUnit1025 // reporting duplicate test cases due to not distinguishing 0.0 from -0.0 @@ -1248,6 +1250,140 @@ public static void Round_Double_Digits_SpecificCases() Assert.Equal(double.NegativeInfinity, Math.Round(double.NegativeInfinity, 3, MidpointRounding.AwayFromZero)); } + public static IEnumerable Round_Double_Digits_ExactValue_TestData() + { + // 655.925 is not exactly representable; the nearest double is 655.924999999999954525..., + // which is just below the 655.925 decimal midpoint. Scaling by 100 produces exactly 65592.5 + // and so the value used to incorrectly round as if it were a midpoint. + yield return new object[] { 655.925, 2, MidpointRounding.ToEven, 655.92 }; + yield return new object[] { 655.925, 2, MidpointRounding.AwayFromZero, 655.92 }; + yield return new object[] { 655.925, 2, MidpointRounding.ToZero, 655.92 }; + yield return new object[] { 655.925, 2, MidpointRounding.ToNegativeInfinity, 655.92 }; + yield return new object[] { 655.925, 2, MidpointRounding.ToPositiveInfinity, 655.93 }; + yield return new object[] { -655.925, 2, MidpointRounding.ToEven, -655.92 }; + yield return new object[] { -655.925, 2, MidpointRounding.AwayFromZero, -655.92 }; + yield return new object[] { -655.925, 2, MidpointRounding.ToZero, -655.92 }; + yield return new object[] { -655.925, 2, MidpointRounding.ToNegativeInfinity, -655.93 }; + yield return new object[] { -655.925, 2, MidpointRounding.ToPositiveInfinity, -655.92 }; + + // Large magnitudes below the integer boundary previously lost their fractional bits when + // scaled by a power of 10 (1111111111111111.5 * 10 is not exactly representable). + yield return new object[] { 1111111111111111.5, 1, MidpointRounding.ToEven, 1111111111111111.5 }; + yield return new object[] { 1111111111111111.5, 1, MidpointRounding.AwayFromZero, 1111111111111111.5 }; + + // 0.25 is an exactly representable decimal midpoint at one fractional digit. + yield return new object[] { 0.25, 1, MidpointRounding.ToEven, 0.2 }; + yield return new object[] { 0.25, 1, MidpointRounding.AwayFromZero, 0.3 }; + yield return new object[] { 0.35, 1, MidpointRounding.ToEven, 0.3 }; // 0.35 -> 0.34999999999999997... + yield return new object[] { 0.35, 1, MidpointRounding.AwayFromZero, 0.3 }; + + // Values at or above the integer boundary (2^52) are already integers and are unchanged. + yield return new object[] { 4503599627370496.0, 5, MidpointRounding.AwayFromZero, 4503599627370496.0 }; + yield return new object[] { 2e16, 3, MidpointRounding.AwayFromZero, 2e16 }; + + // Subnormals and other tiny magnitudes have an enormous binary exponent, so the fraction sits + // far below the midpoint and rounds to zero except when directed rounding forces it up. + yield return new object[] { double.Epsilon, 15, MidpointRounding.ToEven, 0.0 }; + yield return new object[] { double.Epsilon, 15, MidpointRounding.AwayFromZero, 0.0 }; + yield return new object[] { double.Epsilon, 15, MidpointRounding.ToPositiveInfinity, 1e-15 }; + yield return new object[] { -double.Epsilon, 15, MidpointRounding.ToNegativeInfinity, -1e-15 }; + yield return new object[] { 5e-320, 10, MidpointRounding.ToEven, 0.0 }; + + // The historical 0-15 digit cap has been lifted; any non-negative digit count is now accepted. + // Rounding to a digit count at or beyond the precision needed to round-trip a double (17) is a + // no-op, since the exactly rounded decimal converts back to the original value. + yield return new object[] { 0.1, 16, MidpointRounding.ToEven, 0.1 }; + yield return new object[] { 0.1, 17, MidpointRounding.ToEven, 0.1 }; + yield return new object[] { 0.1, 20, MidpointRounding.AwayFromZero, 0.1 }; + yield return new object[] { 655.925, 30, MidpointRounding.AwayFromZero, 655.925 }; + yield return new object[] { 0.1, 1000, MidpointRounding.ToEven, 0.1 }; + + // A fractional part first appears beyond the 15th digit here, so rounding at 16 digits is + // meaningful and was unreachable under the old cap. + yield return new object[] { 2.5e-16, 16, MidpointRounding.ToEven, 3e-16 }; + yield return new object[] { 2.5e-16, 16, MidpointRounding.AwayFromZero, 3e-16 }; + + // The fast rounding path is exact through 19 fractional digits for double; exercise the boundary + // (18, 19) and confirm the exact routine takes over just beyond it (20) while staying correct. + yield return new object[] { 2.5e-18, 18, MidpointRounding.ToEven, 3e-18 }; + yield return new object[] { 2.5e-18, 18, MidpointRounding.AwayFromZero, 3e-18 }; + yield return new object[] { 2.5e-19, 19, MidpointRounding.ToEven, 3e-19 }; + yield return new object[] { 2.5e-19, 19, MidpointRounding.AwayFromZero, 3e-19 }; + yield return new object[] { 2.5e-20, 20, MidpointRounding.ToEven, 2e-20 }; + yield return new object[] { 2.5e-20, 20, MidpointRounding.AwayFromZero, 2e-20 }; + + // The smallest subnormals combined with very large digit counts exercise the deepest part of the + // exact arbitrary-precision path (near its worst-case buffer size), which must stay correct. + yield return new object[] { double.Epsilon, 324, MidpointRounding.AwayFromZero, double.Epsilon }; + yield return new object[] { double.Epsilon, 323, MidpointRounding.AwayFromZero, 0.0 }; + yield return new object[] { 2.2250738585072014e-308, 1074, MidpointRounding.AwayFromZero, 2.2250738585072014e-308 }; + } + + [Theory] + [MemberData(nameof(Round_Double_Digits_ExactValue_TestData))] + public static void Round_Double_Digits_ExactValue(double value, int digits, MidpointRounding mode, double expected) + { + double actual = Math.Round(value, digits, mode); + Assert.Equal(BitConverter.DoubleToInt64Bits(expected), BitConverter.DoubleToInt64Bits(actual)); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void Round_Double_Digits_ExactValue_IntegerFallback() + { + // The digit-rounding fast path uses a double-double FusedMultiplyAdd implementation where the + // hardware accelerates it (FMA3 on x86, baseline on Arm64) and an exact integer implementation + // otherwise. On accelerated hardware the theory above only covers the FusedMultiplyAdd path, so + // re-run the same vectors with hardware intrinsics disabled to deterministically exercise the + // integer fallback and confirm it produces bit-for-bit identical results. + var psi = new ProcessStartInfo(); + psi.Environment["DOTNET_EnableHWIntrinsic"] = "0"; + + RemoteExecutor.Invoke(static () => + { + // The integer fallback is only consulted for finite magnitudes below the integer boundary + // (2^52) with a digit count in the fast-path range; every other vector takes the dedicated + // integer-rounding overload or the arbitrary-precision routine regardless of the intrinsic + // switch, so skip them to keep the remote run focused and avoid redundant work. + const int MaxFastRoundingDigits = 19; + const double IntegerBoundary = 4503599627370496.0; // 2^52 + + foreach (object[] testData in Round_Double_Digits_ExactValue_TestData()) + { + double value = (double)testData[0]; + int digits = (int)testData[1]; + MidpointRounding mode = (MidpointRounding)testData[2]; + double expected = (double)testData[3]; + + if (digits is < 1 or > MaxFastRoundingDigits || Math.Abs(value) >= IntegerBoundary) + { + continue; + } + + double actual = Math.Round(value, digits, mode); + Assert.Equal(BitConverter.DoubleToInt64Bits(expected), BitConverter.DoubleToInt64Bits(actual)); + } + }, new RemoteInvokeOptions { StartInfo = psi }).Dispose(); + } + + [Theory] + [InlineData(0.5)] // below the integer boundary + [InlineData(9e15)] // between the integer boundary (2^52) and the old 1e16 limit + [InlineData(double.NaN)] + [InlineData(double.PositiveInfinity)] + public static void Round_Double_Digits_InvalidMidpointRounding_ThrowsArgumentException(double value) + { + AssertExtensions.Throws("mode", () => Math.Round(value, 3, (MidpointRounding)(-1))); + } + + [Theory] + [InlineData(-1)] + [InlineData(int.MinValue)] + public static void Round_Double_Digits_NegativeDigits_ThrowsArgumentOutOfRangeException(int digits) + { + AssertExtensions.Throws("digits", () => Math.Round(1.5, digits)); + AssertExtensions.Throws("digits", () => Math.Round(1.5, digits, MidpointRounding.ToEven)); + } + [Fact] public static void Sign_Decimal() { diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/MathF.cs b/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/MathF.cs index e5d2b224734b13..a1b67b9ca66efd 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/MathF.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/MathF.cs @@ -3,6 +3,8 @@ using Xunit; using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.DotNet.RemoteExecutor; #pragma warning disable xUnit1025 // reporting duplicate test cases due to not distinguishing 0.0 from -0.0 @@ -1235,6 +1237,126 @@ public static void Round_Digits(float x, float expected, int digits, MidpointRou AssertExtensions.Equal(expected, MathF.Round(x, digits, mode), CrossPlatformMachineEpsilon * 10); } + public static IEnumerable Round_Digits_ExactValue_TestData() + { + // Cases where scaling the input by a power of 10 produced an inexact intermediate and so + // rounded incorrectly. Expected values are the correctly rounded results of the exact input. + yield return new object[] { 55392.164f, 2, MidpointRounding.AwayFromZero, 55392.16f }; + yield return new object[] { -2052.5215f, 3, MidpointRounding.AwayFromZero, -2052.521f }; + yield return new object[] { 7379.389f, 4, MidpointRounding.ToPositiveInfinity, 7379.389f }; + yield return new object[] { 87101.12f, 5, MidpointRounding.ToZero, 87101.12f }; + yield return new object[] { 20.932291f, 6, MidpointRounding.ToEven, 20.932291f }; + yield return new object[] { -422486.8f, 5, MidpointRounding.ToEven, -422486.8f }; + yield return new object[] { -122072.92f, 2, MidpointRounding.ToNegativeInfinity, -122072.93f }; + + // 0.25 is an exactly representable decimal midpoint at one fractional digit. + yield return new object[] { 0.25f, 1, MidpointRounding.ToEven, 0.2f }; + yield return new object[] { 0.25f, 1, MidpointRounding.AwayFromZero, 0.3f }; + + // Values at or above the integer boundary (2^23) are already integers and are unchanged. + yield return new object[] { 8388608.0f, 5, MidpointRounding.AwayFromZero, 8388608.0f }; + yield return new object[] { 2e7f, 3, MidpointRounding.AwayFromZero, 2e7f }; + + // Subnormals and other tiny magnitudes have an enormous binary exponent, so the fraction sits + // far below the midpoint and rounds to zero except when directed rounding forces it up. + yield return new object[] { float.Epsilon, 6, MidpointRounding.ToEven, 0.0f }; + yield return new object[] { float.Epsilon, 6, MidpointRounding.AwayFromZero, 0.0f }; + yield return new object[] { float.Epsilon, 6, MidpointRounding.ToPositiveInfinity, 1e-6f }; + yield return new object[] { -float.Epsilon, 6, MidpointRounding.ToNegativeInfinity, -1e-6f }; + yield return new object[] { 1.5e-40f, 6, MidpointRounding.ToEven, 0.0f }; + + // The historical 0-6 digit cap has been lifted; any non-negative digit count is now accepted. + // Rounding to a digit count at or beyond the precision needed to round-trip a float (9) is a + // no-op, since the exactly rounded decimal converts back to the original value. + yield return new object[] { 0.1f, 8, MidpointRounding.ToEven, 0.1f }; + yield return new object[] { 0.1f, 10, MidpointRounding.ToEven, 0.1f }; + yield return new object[] { 0.1f, 20, MidpointRounding.AwayFromZero, 0.1f }; + yield return new object[] { 0.1f, 100, MidpointRounding.ToEven, 0.1f }; + + // A fractional part first appears beyond the 6th digit here, so rounding at 8 digits is + // meaningful and was unreachable under the old cap. + yield return new object[] { 2.5e-8f, 8, MidpointRounding.AwayFromZero, 3e-8f }; + + // The fast rounding path is exact through 10 fractional digits for float; exercise the boundary + // (9, 10) and confirm the exact routine takes over just beyond it (11) while staying correct. + yield return new object[] { 2.5e-9f, 9, MidpointRounding.ToEven, 2e-9f }; + yield return new object[] { 2.5e-9f, 9, MidpointRounding.AwayFromZero, 2e-9f }; + yield return new object[] { 2.5e-10f, 10, MidpointRounding.ToEven, 2e-10f }; + yield return new object[] { 2.5e-10f, 10, MidpointRounding.AwayFromZero, 2e-10f }; + yield return new object[] { 2.5e-11f, 11, MidpointRounding.ToEven, 3e-11f }; + yield return new object[] { 2.5e-11f, 11, MidpointRounding.AwayFromZero, 3e-11f }; + + // The smallest subnormals combined with very large digit counts exercise the deepest part of the + // exact arbitrary-precision path (near its worst-case buffer size), which must stay correct. + yield return new object[] { float.Epsilon, 45, MidpointRounding.AwayFromZero, float.Epsilon }; + yield return new object[] { float.Epsilon, 44, MidpointRounding.AwayFromZero, 0.0f }; + } + + [Theory] + [MemberData(nameof(Round_Digits_ExactValue_TestData))] + public static void Round_Digits_ExactValue(float value, int digits, MidpointRounding mode, float expected) + { + float actual = MathF.Round(value, digits, mode); + Assert.Equal(BitConverter.SingleToInt32Bits(expected), BitConverter.SingleToInt32Bits(actual)); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void Round_Digits_ExactValue_IntegerFallback() + { + // The digit-rounding fast path uses a double-double FusedMultiplyAdd implementation where the + // hardware accelerates it (FMA3 on x86, baseline on Arm64) and an exact integer implementation + // otherwise. On accelerated hardware the theory above only covers the FusedMultiplyAdd path, so + // re-run the same vectors with hardware intrinsics disabled to deterministically exercise the + // integer fallback and confirm it produces bit-for-bit identical results. + var psi = new ProcessStartInfo(); + psi.Environment["DOTNET_EnableHWIntrinsic"] = "0"; + + RemoteExecutor.Invoke(static () => + { + // The integer fallback is only consulted for finite magnitudes below the integer boundary + // (2^23) with a digit count in the fast-path range; every other vector takes the dedicated + // integer-rounding overload or the arbitrary-precision routine regardless of the intrinsic + // switch, so skip them to keep the remote run focused and avoid redundant work. + const int MaxFastRoundingDigits = 10; + const float IntegerBoundary = 8388608.0f; // 2^23 + + foreach (object[] testData in Round_Digits_ExactValue_TestData()) + { + float value = (float)testData[0]; + int digits = (int)testData[1]; + MidpointRounding mode = (MidpointRounding)testData[2]; + float expected = (float)testData[3]; + + if (digits is < 1 or > MaxFastRoundingDigits || MathF.Abs(value) >= IntegerBoundary) + { + continue; + } + + float actual = MathF.Round(value, digits, mode); + Assert.Equal(BitConverter.SingleToInt32Bits(expected), BitConverter.SingleToInt32Bits(actual)); + } + }, new RemoteInvokeOptions { StartInfo = psi }).Dispose(); + } + + [Theory] + [InlineData(0.5f)] // below the integer boundary + [InlineData(9e6f)] // between the integer boundary (2^23) and the old 1e8 limit + [InlineData(float.NaN)] + [InlineData(float.PositiveInfinity)] + public static void Round_Digits_InvalidMidpointRounding_ThrowsArgumentException(float value) + { + AssertExtensions.Throws("mode", () => MathF.Round(value, 3, (MidpointRounding)(-1))); + } + + [Theory] + [InlineData(-1)] + [InlineData(int.MinValue)] + public static void Round_Digits_NegativeDigits_ThrowsArgumentOutOfRangeException(int digits) + { + AssertExtensions.Throws("digits", () => MathF.Round(1.5f, digits)); + AssertExtensions.Throws("digits", () => MathF.Round(1.5f, digits, MidpointRounding.ToEven)); + } + [Theory] [InlineData(float.NegativeInfinity, unchecked((int)(0x7FFFFFFF)), float.NegativeInfinity, 0)] [InlineData(float.PositiveInfinity, unchecked((int)(0x7FFFFFFF)), float.PositiveInfinity, 0)] diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/TimeSpanTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/TimeSpanTests.cs index 0de8d33c4d3565..98e0638c426c69 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/TimeSpanTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/TimeSpanTests.cs @@ -1775,7 +1775,7 @@ public static void NaNMultiplication() [Theory, MemberData(nameof(MultiplicationTestData))] public static void Division(TimeSpan timeSpan, double factor, TimeSpan expected) { - Assert.Equal(factor, expected / timeSpan, 14); + AssertExtensions.Equal(factor, expected / timeSpan, 1e-14); double divisor = 1.0 / factor; Assert.Equal(expected, timeSpan / divisor); } @@ -1818,7 +1818,7 @@ public static void NamedNaNMultiplication() [Theory, MemberData(nameof(MultiplicationTestData))] public static void NamedDivision(TimeSpan timeSpan, double factor, TimeSpan expected) { - Assert.Equal(factor, expected.Divide(timeSpan), 14); + AssertExtensions.Equal(factor, expected.Divide(timeSpan), 1e-14); double divisor = 1.0 / factor; Assert.Equal(expected, timeSpan.Divide(divisor)); }