diff --git a/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs b/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs index e56096d042f9f3..a797d5f90ce2ad 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs @@ -158,6 +158,7 @@ internal interface IDecimalIeee754ParseAndFormatInfo static abstract int NumberBitsSignificand { get; } static abstract TValue NaNMask { get; } static abstract TValue SNaNMask { get; } + static abstract TValue NaNPayloadMask { get; } static abstract TValue SignMask { get; } static abstract TValue G0G1Mask { get; } static abstract TValue G0ToGwPlus1ExponentMask { get; } //G0 to G(w+1) diff --git a/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal128.cs b/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal128.cs index 1f3ac819694c1e..70fb67d41fa3d5 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal128.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal128.cs @@ -51,6 +51,7 @@ public readonly struct Decimal128 private static UInt128 PiValue => new UInt128(0x2FFE_9AE4_7957_96A7, 0xBABE_5564_E6F3_9F8F); // +3.141592653589793238462643383279503 private static UInt128 TauValue => new UInt128(0x2FFF_35C8_F2AF_2D4F, 0x757C_AAC9_CDE7_3F1E); // +6.283185307179586476925286766559006 private static UInt128 QuietNaNValue => new UInt128(0xFC00_0000_0000_0000, 0); + private static UInt128 NaNPayloadMask => new UInt128(0x0000_3FFF_FFFF_FFFF, 0xFFFF_FFFF_FFFF_FFFF); private const ulong SignMaskUpper = 0x8000_0000_0000_0000; private const ulong NaNMaskUpper = 0x7C00_0000_0000_0000; @@ -1254,7 +1255,7 @@ public static Decimal128 ClampNative(Decimal128 value, Decimal128 min, Decimal12 static int INumberBase.Radix => 10; /// - static bool INumberBase.IsCanonical(Decimal128 value) => Number.IsCanonicalDecimalIeee754(new UInt128(value._upper, value._lower), nanReservedMask: new UInt128(0x01FF_C000_0000_0000, 0x0000_0000_0000_0000), nanPayloadMask: new UInt128(0x0000_3FFF_FFFF_FFFF, 0xFFFF_FFFF_FFFF_FFFF), maxNaNPayload: new UInt128(0x0000_314D_C644_8D93, 0x38C1_5B09_FFFF_FFFF)); + static bool INumberBase.IsCanonical(Decimal128 value) => Number.IsCanonicalDecimalIeee754(new UInt128(value._upper, value._lower), nanReservedMask: new UInt128(0x01FF_C000_0000_0000, 0x0000_0000_0000_0000), nanPayloadMask: NaNPayloadMask, maxNaNPayload: new UInt128(0x0000_314D_C644_8D93, 0x38C1_5B09_FFFF_FFFF)); /// static bool INumberBase.IsComplexNumber(Decimal128 value) => false; @@ -1793,6 +1794,8 @@ static unsafe UInt128 IDecimalIeee754ParseAndFormatInfo.Num static UInt128 IDecimalIeee754ParseAndFormatInfo.SNaNMask => new UInt128(SNaNMaskUpper, 0); + static UInt128 IDecimalIeee754ParseAndFormatInfo.NaNPayloadMask => NaNPayloadMask; + static UInt128 IDecimalIeee754ParseAndFormatInfo.SignMask => new UInt128(SignMaskUpper, 0); static UInt128 IDecimalIeee754ParseAndFormatInfo.G0G1Mask => new UInt128(0x6000_0000_0000_0000, 0); diff --git a/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal32.cs b/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal32.cs index 5528036ae3862c..dd5c48c0e98814 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal32.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal32.cs @@ -57,6 +57,7 @@ internal Decimal32(uint value) private const uint MostSignificantBitOfSignificandMask = 0x0080_0000; private const uint NaNMask = 0x7C00_0000; private const uint SNaNMask = 0x7E00_0000; + private const uint NaNPayloadMask = 0x000F_FFFF; private const uint InfinityMask = 0x7800_0000; private const uint MaxSignificand = 9_999_999; private const uint MaxInternalValue = 0x77F8_967F; // +9.999_999 * 10^96; aka +9_999_999 * 10^90 @@ -1277,7 +1278,7 @@ public static Decimal32 ClampNative(Decimal32 value, Decimal32 min, Decimal32 ma static int INumberBase.Radix => 10; /// - static bool INumberBase.IsCanonical(Decimal32 value) => Number.IsCanonicalDecimalIeee754(value._value, nanReservedMask: 0x01F0_0000, nanPayloadMask: 0x000F_FFFF, maxNaNPayload: 999_999); + static bool INumberBase.IsCanonical(Decimal32 value) => Number.IsCanonicalDecimalIeee754(value._value, nanReservedMask: 0x01F0_0000, nanPayloadMask: NaNPayloadMask, maxNaNPayload: 999_999); /// static bool INumberBase.IsComplexNumber(Decimal32 value) => false; @@ -1773,6 +1774,8 @@ static unsafe uint IDecimalIeee754ParseAndFormatInfo.NumberToSi static uint IDecimalIeee754ParseAndFormatInfo.SNaNMask => SNaNMask; + static uint IDecimalIeee754ParseAndFormatInfo.NaNPayloadMask => NaNPayloadMask; + static uint IDecimalIeee754ParseAndFormatInfo.SignMask => SignMask; static uint IDecimalIeee754ParseAndFormatInfo.G0G1Mask => G0G1Mask; diff --git a/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal64.cs b/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal64.cs index 2d0eb03111ad83..760dde42f84402 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal64.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal64.cs @@ -50,6 +50,7 @@ public readonly struct Decimal64 private const ulong MostSignificantBitOfSignificandMask = 0x0020_0000_0000_0000; private const ulong NaNMask = 0x7C00_0000_0000_0000; private const ulong SNaNMask = 0x7E00_0000_0000_0000; + private const ulong NaNPayloadMask = 0x0003_FFFF_FFFF_FFFF; private const ulong InfinityMask = 0x7800_0000_0000_0000; private const ulong MaxSignificand = 9_999_999_999_999_999; private const ulong MaxInternalValue = 0x77FB_86F2_6FC0_FFFF; // 9.999_999_999_999_999 * 10^384; aka 9_999_999_999_999_999 * 10^369 @@ -1268,7 +1269,7 @@ public static Decimal64 ClampNative(Decimal64 value, Decimal64 min, Decimal64 ma static int INumberBase.Radix => 10; /// - static bool INumberBase.IsCanonical(Decimal64 value) => Number.IsCanonicalDecimalIeee754(value._value, nanReservedMask: 0x01FC_0000_0000_0000, nanPayloadMask: 0x0003_FFFF_FFFF_FFFF, maxNaNPayload: 999_999_999_999_999); + static bool INumberBase.IsCanonical(Decimal64 value) => Number.IsCanonicalDecimalIeee754(value._value, nanReservedMask: 0x01FC_0000_0000_0000, nanPayloadMask: NaNPayloadMask, maxNaNPayload: 999_999_999_999_999); /// static bool INumberBase.IsComplexNumber(Decimal64 value) => false; @@ -1765,6 +1766,8 @@ static unsafe ulong IDecimalIeee754ParseAndFormatInfo.NumberTo static ulong IDecimalIeee754ParseAndFormatInfo.SNaNMask => SNaNMask; + static ulong IDecimalIeee754ParseAndFormatInfo.NaNPayloadMask => NaNPayloadMask; + static ulong IDecimalIeee754ParseAndFormatInfo.SignMask => SignMask; static ulong IDecimalIeee754ParseAndFormatInfo.G0G1Mask => G0G1Mask; diff --git a/src/libraries/System.Private.CoreLib/src/System/Numerics/TotalOrderIeee754Comparer.cs b/src/libraries/System.Private.CoreLib/src/System/Numerics/TotalOrderIeee754Comparer.cs index 398acd80b681d2..d3558d18546cf7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Numerics/TotalOrderIeee754Comparer.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Numerics/TotalOrderIeee754Comparer.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace System.Numerics { @@ -62,6 +63,28 @@ public int Compare(T? x, T? y) { return CompareIntegerSemantic(BitConverter.HalfToInt16Bits((Half)(object)x!), BitConverter.HalfToInt16Bits((Half)(object)y!)); } + else if (typeof(T) == typeof(BFloat16)) + { + return CompareIntegerSemantic(Unsafe.BitCast((BFloat16)(object)x!), Unsafe.BitCast((BFloat16)(object)y!)); + } + else if (typeof(T) == typeof(NFloat)) + { + return CompareIntegerSemantic(Unsafe.BitCast((NFloat)(object)x!), Unsafe.BitCast((NFloat)(object)y!)); + } + else if (typeof(T) == typeof(Decimal32)) + { + return CompareDecimal(((Decimal32)(object)x!)._value, ((Decimal32)(object)y!)._value); + } + else if (typeof(T) == typeof(Decimal64)) + { + return CompareDecimal(((Decimal64)(object)x!)._value, ((Decimal64)(object)y!)._value); + } + else if (typeof(T) == typeof(Decimal128)) + { + Decimal128 xValue = (Decimal128)(object)x!; + Decimal128 yValue = (Decimal128)(object)y!; + return CompareDecimal(new UInt128(xValue._upper, xValue._lower), new UInt128(yValue._upper, yValue._lower)); + } else { return CompareGeneric(x, y); @@ -97,6 +120,32 @@ static int CompareGeneric(T? x, T? y) return 1; } + static int CompareExponent(T x, T y) + { + // Equal values with differing representations only differ by their exponent, so the + // unbiased (quantum) exponents are read and compared using signed semantics. For a + // given type both exponents share the same byte count. + + // Prevent stack overflow for huge numbers + const int StackAllocThreshold = 256; + + int xExponentLength = x!.GetExponentByteCount(); + int yExponentLength = y!.GetExponentByteCount(); + + Span exponentX = (uint)xExponentLength <= StackAllocThreshold ? stackalloc byte[xExponentLength] : new byte[xExponentLength]; + Span exponentY = (uint)yExponentLength <= StackAllocThreshold ? stackalloc byte[yExponentLength] : new byte[yExponentLength]; + + x.WriteExponentBigEndian(exponentX); + y.WriteExponentBigEndian(exponentY); + + // The exponents are two's complement big-endian. Flipping the sign bit of the most + // significant byte maps the signed ordering onto an unsigned byte-wise comparison. + exponentX[0] ^= 0x80; + exponentY[0] ^= 0x80; + + return exponentX.SequenceCompareTo(exponentY); + } + // If < or > returns true, the result satisfies definition of totalOrder too if (x < y) @@ -109,27 +158,20 @@ static int CompareGeneric(T? x, T? y) } else if (x == y) { - if (T.IsZero(x)) // only zeros are equal to zeros + // Only zeros are equal to zeros, and totalOrder places -0 before +0. + if (T.IsZero(x) && (T.IsNegative(x) != T.IsNegative(y))) { - // IEEE 754 numbers are either positive or negative. Skip check for the opposite. - - if (T.IsNegative(x)) - { - return T.IsNegative(y) ? 0 : -1; - } - else - { - return T.IsPositive(y) ? 0 : 1; - } + return T.IsNegative(x) ? -1 : 1; } - else - { - // Equivalant values are compared by their exponent parts, - // and the value with smaller exponent is considered closer to zero. - // This only applies to IEEE 754 decimals. Consider to add support if decimals are added into .NET. - return 0; - } + // Values that are equal but have differing representations are IEEE 754 decimal cohort + // members (e.g. 1.0 and 1.00, or same-signed zeros with differing exponents). These are + // ordered by exponent: the value with the smaller exponent is closer to zero for positive + // values, and the ordering is reversed for negative values. Binary types have a unique + // representation per value, so their exponents already match and this returns 0. + + int exponentComparison = CompareExponent(x, y); + return T.IsNegative(x) ? -exponentComparison : exponentComparison; } else { @@ -140,7 +182,6 @@ static unsafe int CompareSignificand(T x, T y) { // IEEE 754 totalOrder only defines the order of NaN type bit (the first bit of significand) // To match the integer semantic comparison above, here we compare all the significand bits - // Revisit this if decimals are added // Leave the space for custom floating-point type that has variable significand length @@ -201,6 +242,76 @@ static unsafe int CompareSignificand(T x, T y) } } + // Fast path for the built-in IEEE 754 decimal types. Decimal bit patterns are not monotonic + // in totalOrder (unlike binary formats), so this replicates the generic totalOrder while + // unpacking each operand at most twice instead of the four-plus unpacks the generic path incurs. + private static int CompareDecimal(TValue xBits, TValue yBits) + where TDecimal : unmanaged, IDecimalIeee754ParseAndFormatInfo + where TValue : unmanaged, IBinaryInteger + { + bool xIsNaN = TDecimal.IsNaN(xBits); + bool yIsNaN = TDecimal.IsNaN(yBits); + + if (xIsNaN || yIsNaN) + { + // totalOrder places NaNs at the extremes ordered by sign: + // -qNaN < -sNaN < -infinite < -finite < -0 < +0 < +finite < +infinite < +sNaN < +qNaN. + bool xIsNegative = TDecimal.IsNegative(xBits); + + if (xIsNaN && yIsNaN) + { + if (xIsNegative != TDecimal.IsNegative(yBits)) + { + return xIsNegative ? -1 : 1; + } + + // totalOrder only fixes the sign and the signaling-before-quiet ordering for NaNs; + // the payload order is implementation-defined, so there is no need to canonicalize. + // The raw payload bits are compared directly (including non-canonical payloads), which + // is cheaper and more deterministic than decoding the coefficient and stays consistent + // across every width (notably Decimal128, whose NaN coefficient decode is non-canonical). + // The signaling bit sits just above the payload but is set for signaling rather than + // quiet, so it is folded in inverted to rank quiet above signaling with a single compare. + TValue signalingMask = TDecimal.SNaNMask ^ TDecimal.NaNMask; + TValue xKey = (xBits & TDecimal.NaNPayloadMask) | ((xBits & signalingMask) ^ signalingMask); + TValue yKey = (yBits & TDecimal.NaNPayloadMask) | ((yBits & signalingMask) ^ signalingMask); + + int payload = xKey.CompareTo(yKey); + return xIsNegative ? -payload : payload; + } + + if (xIsNaN) + { + return xIsNegative ? -1 : 1; + } + + return TDecimal.IsNegative(yBits) ? 1 : -1; + } + + int result = Number.CompareDecimalIeee754(xBits, yBits); + + if (result != 0) + { + return result; + } + + // The values are numerically equal but may have differing representations (decimal cohort + // members such as 1.0 and 1.00, or same-signed zeros with differing exponents). + Number.DecodedDecimalIeee754 x = Number.UnpackDecimalIeee754(xBits); + Number.DecodedDecimalIeee754 y = Number.UnpackDecimalIeee754(yBits); + + // totalOrder places -0 before +0. + if ((x.Significand == TValue.Zero) && (x.Signed != y.Signed)) + { + return x.Signed ? -1 : 1; + } + + // Cohort members differ only by exponent: the value with the smaller exponent is closer to + // zero for positive values, and the ordering is reversed for negative values. + int exponentComparison = x.UnbiasedExponent.CompareTo(y.UnbiasedExponent); + return x.Signed ? -exponentComparison : exponentComparison; + } + /// /// Determines whether the specified numbers are equal. /// diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Numerics/TotalOrderIeee754ComparerTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Numerics/TotalOrderIeee754ComparerTests.cs index 9ec0d213a9113a..1d902c1cbe0737 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Numerics/TotalOrderIeee754ComparerTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Numerics/TotalOrderIeee754ComparerTests.cs @@ -1,9 +1,10 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using System.Globalization; using System.Numerics; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Xunit; @@ -94,6 +95,34 @@ public void TotalOrderTestHalf(Half x, Half y, int result) Assert.Equal(result, Math.Sign(comparer.Compare(x, y))); } + public static IEnumerable BFloat16TestData + { + get + { + yield return new object[] { (BFloat16)0.0f, (BFloat16)0.0f, 0 }; + yield return new object[] { (BFloat16)(-0.0f), (BFloat16)(-0.0f), 0 }; + yield return new object[] { (BFloat16)0.0f, (BFloat16)(-0.0f), 1 }; + yield return new object[] { (BFloat16)(-0.0f), (BFloat16)0.0f, -1 }; + yield return new object[] { (BFloat16)0.0f, (BFloat16)1.0f, -1 }; + yield return new object[] { BFloat16.PositiveInfinity, (BFloat16)1.0f, 1 }; + yield return new object[] { BFloat16.NaN, BFloat16.NegativeInfinity, -1 }; + yield return new object[] { BFloat16.NaN, (BFloat16)(-1.0f), -1 }; + yield return new object[] { -BFloat16.NaN, (BFloat16)1.0f, 1 }; + yield return new object[] { -BFloat16.NaN, BFloat16.PositiveInfinity, 1 }; + yield return new object[] { BFloat16.NaN, BFloat16.NaN, 0 }; + yield return new object[] { BFloat16.NaN, -BFloat16.NaN, -1 }; + yield return new object[] { Unsafe.BitCast(0x7FC0), Unsafe.BitCast(0x7FC1), -1 }; // implementation defined, not part of IEEE 754 totalOrder + } + } + + [Theory] + [MemberData(nameof(BFloat16TestData))] + public void TotalOrderTestBFloat16(BFloat16 x, BFloat16 y, int result) + { + var comparer = new TotalOrderIeee754Comparer(); + Assert.Equal(result, Math.Sign(comparer.Compare(x, y))); + } + public static IEnumerable NFloatTestData { get @@ -134,6 +163,130 @@ public void TotalOrderTestNFloat(NFloat x, NFloat y, int result) Assert.Equal(result, Math.Sign(comparer.Compare(x, y))); } + public static IEnumerable Decimal32TestData + { + get + { + yield return new object[] { Decimal32.Parse("0", CultureInfo.InvariantCulture), Decimal32.Parse("0", CultureInfo.InvariantCulture), 0 }; + yield return new object[] { Decimal32.NegativeZero, Decimal32.NegativeZero, 0 }; + yield return new object[] { Decimal32.Parse("0", CultureInfo.InvariantCulture), Decimal32.NegativeZero, 1 }; + yield return new object[] { Decimal32.NegativeZero, Decimal32.Parse("0", CultureInfo.InvariantCulture), -1 }; + yield return new object[] { Decimal32.Parse("0", CultureInfo.InvariantCulture), Decimal32.Parse("1", CultureInfo.InvariantCulture), -1 }; + yield return new object[] { Decimal32.PositiveInfinity, Decimal32.Parse("1", CultureInfo.InvariantCulture), 1 }; + yield return new object[] { Decimal32.NaN, Decimal32.NegativeInfinity, -1 }; + yield return new object[] { Decimal32.NaN, Decimal32.Parse("-1", CultureInfo.InvariantCulture), -1 }; + yield return new object[] { -Decimal32.NaN, Decimal32.Parse("1", CultureInfo.InvariantCulture), 1 }; + yield return new object[] { -Decimal32.NaN, Decimal32.PositiveInfinity, 1 }; + yield return new object[] { Decimal32.NaN, Decimal32.NaN, 0 }; + yield return new object[] { Decimal32.NaN, -Decimal32.NaN, -1 }; + // Same-signed NaNs are ordered by payload (implementation defined, not part of IEEE 754 totalOrder) + yield return new object[] { Unsafe.BitCast(0x7C00_0000), Unsafe.BitCast(0x7C00_0001), -1 }; + yield return new object[] { Unsafe.BitCast(0xFC00_0000), Unsafe.BitCast(0xFC00_0001), 1 }; + // Signaling NaNs order before quiet NaNs of the same sign (+sNaN < +qNaN) + yield return new object[] { Unsafe.BitCast(0x7E00_0000), Unsafe.BitCast(0x7C00_0000), -1 }; + yield return new object[] { Unsafe.BitCast(0xFE00_0000), Unsafe.BitCast(0xFC00_0000), 1 }; + // Cohort members: equal value, differing quantum exponent (totalOrder orders by exponent) + yield return new object[] { Decimal32.Parse("1.00", CultureInfo.InvariantCulture), Decimal32.Parse("1.0", CultureInfo.InvariantCulture), -1 }; + yield return new object[] { Decimal32.Parse("1.0", CultureInfo.InvariantCulture), Decimal32.Parse("1.00", CultureInfo.InvariantCulture), 1 }; + yield return new object[] { Decimal32.Parse("1e1", CultureInfo.InvariantCulture), Decimal32.Parse("10e0", CultureInfo.InvariantCulture), 1 }; + yield return new object[] { Decimal32.Parse("-1.00", CultureInfo.InvariantCulture), Decimal32.Parse("-1.0", CultureInfo.InvariantCulture), 1 }; + yield return new object[] { Decimal32.Parse("-1.0", CultureInfo.InvariantCulture), Decimal32.Parse("-1.00", CultureInfo.InvariantCulture), -1 }; + yield return new object[] { Decimal32.Parse("0.00", CultureInfo.InvariantCulture), Decimal32.Parse("0", CultureInfo.InvariantCulture), -1 }; + yield return new object[] { Decimal32.Parse("-0.00", CultureInfo.InvariantCulture), Decimal32.NegativeZero, 1 }; + } + } + + [Theory] + [MemberData(nameof(Decimal32TestData))] + public void TotalOrderTestDecimal32(Decimal32 x, Decimal32 y, int result) + { + var comparer = new TotalOrderIeee754Comparer(); + Assert.Equal(result, Math.Sign(comparer.Compare(x, y))); + } + + public static IEnumerable Decimal64TestData + { + get + { + yield return new object[] { Decimal64.Parse("0", CultureInfo.InvariantCulture), Decimal64.Parse("0", CultureInfo.InvariantCulture), 0 }; + yield return new object[] { Decimal64.NegativeZero, Decimal64.NegativeZero, 0 }; + yield return new object[] { Decimal64.Parse("0", CultureInfo.InvariantCulture), Decimal64.NegativeZero, 1 }; + yield return new object[] { Decimal64.NegativeZero, Decimal64.Parse("0", CultureInfo.InvariantCulture), -1 }; + yield return new object[] { Decimal64.Parse("0", CultureInfo.InvariantCulture), Decimal64.Parse("1", CultureInfo.InvariantCulture), -1 }; + yield return new object[] { Decimal64.PositiveInfinity, Decimal64.Parse("1", CultureInfo.InvariantCulture), 1 }; + yield return new object[] { Decimal64.NaN, Decimal64.NegativeInfinity, -1 }; + yield return new object[] { Decimal64.NaN, Decimal64.Parse("-1", CultureInfo.InvariantCulture), -1 }; + yield return new object[] { -Decimal64.NaN, Decimal64.Parse("1", CultureInfo.InvariantCulture), 1 }; + yield return new object[] { -Decimal64.NaN, Decimal64.PositiveInfinity, 1 }; + yield return new object[] { Decimal64.NaN, Decimal64.NaN, 0 }; + yield return new object[] { Decimal64.NaN, -Decimal64.NaN, -1 }; + // Same-signed NaNs are ordered by payload (implementation defined, not part of IEEE 754 totalOrder) + yield return new object[] { Unsafe.BitCast(0x7C00_0000_0000_0000), Unsafe.BitCast(0x7C00_0000_0000_0001), -1 }; + yield return new object[] { Unsafe.BitCast(0xFC00_0000_0000_0000), Unsafe.BitCast(0xFC00_0000_0000_0001), 1 }; + // Signaling NaNs order before quiet NaNs of the same sign (+sNaN < +qNaN) + yield return new object[] { Unsafe.BitCast(0x7E00_0000_0000_0000), Unsafe.BitCast(0x7C00_0000_0000_0000), -1 }; + yield return new object[] { Unsafe.BitCast(0xFE00_0000_0000_0000), Unsafe.BitCast(0xFC00_0000_0000_0000), 1 }; + // Cohort members: equal value, differing quantum exponent (totalOrder orders by exponent) + yield return new object[] { Decimal64.Parse("1.00", CultureInfo.InvariantCulture), Decimal64.Parse("1.0", CultureInfo.InvariantCulture), -1 }; + yield return new object[] { Decimal64.Parse("1.0", CultureInfo.InvariantCulture), Decimal64.Parse("1.00", CultureInfo.InvariantCulture), 1 }; + yield return new object[] { Decimal64.Parse("1e1", CultureInfo.InvariantCulture), Decimal64.Parse("10e0", CultureInfo.InvariantCulture), 1 }; + yield return new object[] { Decimal64.Parse("-1.00", CultureInfo.InvariantCulture), Decimal64.Parse("-1.0", CultureInfo.InvariantCulture), 1 }; + yield return new object[] { Decimal64.Parse("-1.0", CultureInfo.InvariantCulture), Decimal64.Parse("-1.00", CultureInfo.InvariantCulture), -1 }; + yield return new object[] { Decimal64.Parse("0.00", CultureInfo.InvariantCulture), Decimal64.Parse("0", CultureInfo.InvariantCulture), -1 }; + yield return new object[] { Decimal64.Parse("-0.00", CultureInfo.InvariantCulture), Decimal64.NegativeZero, 1 }; + } + } + + [Theory] + [MemberData(nameof(Decimal64TestData))] + public void TotalOrderTestDecimal64(Decimal64 x, Decimal64 y, int result) + { + var comparer = new TotalOrderIeee754Comparer(); + Assert.Equal(result, Math.Sign(comparer.Compare(x, y))); + } + + public static IEnumerable Decimal128TestData + { + get + { + yield return new object[] { Decimal128.Parse("0", CultureInfo.InvariantCulture), Decimal128.Parse("0", CultureInfo.InvariantCulture), 0 }; + yield return new object[] { Decimal128.NegativeZero, Decimal128.NegativeZero, 0 }; + yield return new object[] { Decimal128.Parse("0", CultureInfo.InvariantCulture), Decimal128.NegativeZero, 1 }; + yield return new object[] { Decimal128.NegativeZero, Decimal128.Parse("0", CultureInfo.InvariantCulture), -1 }; + yield return new object[] { Decimal128.Parse("0", CultureInfo.InvariantCulture), Decimal128.Parse("1", CultureInfo.InvariantCulture), -1 }; + yield return new object[] { Decimal128.PositiveInfinity, Decimal128.Parse("1", CultureInfo.InvariantCulture), 1 }; + yield return new object[] { Decimal128.NaN, Decimal128.NegativeInfinity, -1 }; + yield return new object[] { Decimal128.NaN, Decimal128.Parse("-1", CultureInfo.InvariantCulture), -1 }; + yield return new object[] { -Decimal128.NaN, Decimal128.Parse("1", CultureInfo.InvariantCulture), 1 }; + yield return new object[] { -Decimal128.NaN, Decimal128.PositiveInfinity, 1 }; + yield return new object[] { Decimal128.NaN, Decimal128.NaN, 0 }; + yield return new object[] { Decimal128.NaN, -Decimal128.NaN, -1 }; + // Same-signed NaNs are ordered by payload (implementation defined, not part of IEEE 754 totalOrder). + // The payload here lives entirely in the low bits, which the coefficient decode would discard. + yield return new object[] { Unsafe.BitCast(new UInt128(0x7C00_0000_0000_0000, 0x0000_0000_0000_0000)), Unsafe.BitCast(new UInt128(0x7C00_0000_0000_0000, 0x0000_0000_0000_0001)), -1 }; + yield return new object[] { Unsafe.BitCast(new UInt128(0xFC00_0000_0000_0000, 0x0000_0000_0000_0000)), Unsafe.BitCast(new UInt128(0xFC00_0000_0000_0000, 0x0000_0000_0000_0001)), 1 }; + // Signaling NaNs order before quiet NaNs of the same sign (+sNaN < +qNaN) + yield return new object[] { Unsafe.BitCast(new UInt128(0x7E00_0000_0000_0000, 0x0000_0000_0000_0000)), Unsafe.BitCast(new UInt128(0x7C00_0000_0000_0000, 0x0000_0000_0000_0000)), -1 }; + yield return new object[] { Unsafe.BitCast(new UInt128(0xFE00_0000_0000_0000, 0x0000_0000_0000_0000)), Unsafe.BitCast(new UInt128(0xFC00_0000_0000_0000, 0x0000_0000_0000_0000)), 1 }; + // Cohort members: equal value, differing quantum exponent (totalOrder orders by exponent) + yield return new object[] { Decimal128.Parse("1.00", CultureInfo.InvariantCulture), Decimal128.Parse("1.0", CultureInfo.InvariantCulture), -1 }; + yield return new object[] { Decimal128.Parse("1.0", CultureInfo.InvariantCulture), Decimal128.Parse("1.00", CultureInfo.InvariantCulture), 1 }; + yield return new object[] { Decimal128.Parse("1e1", CultureInfo.InvariantCulture), Decimal128.Parse("10e0", CultureInfo.InvariantCulture), 1 }; + yield return new object[] { Decimal128.Parse("-1.00", CultureInfo.InvariantCulture), Decimal128.Parse("-1.0", CultureInfo.InvariantCulture), 1 }; + yield return new object[] { Decimal128.Parse("-1.0", CultureInfo.InvariantCulture), Decimal128.Parse("-1.00", CultureInfo.InvariantCulture), -1 }; + yield return new object[] { Decimal128.Parse("0.00", CultureInfo.InvariantCulture), Decimal128.Parse("0", CultureInfo.InvariantCulture), -1 }; + yield return new object[] { Decimal128.Parse("-0.00", CultureInfo.InvariantCulture), Decimal128.NegativeZero, 1 }; + } + } + + [Theory] + [MemberData(nameof(Decimal128TestData))] + public void TotalOrderTestDecimal128(Decimal128 x, Decimal128 y, int result) + { + var comparer = new TotalOrderIeee754Comparer(); + Assert.Equal(result, Math.Sign(comparer.Compare(x, y))); + } + [Theory] [InlineData(-1)] [InlineData(int.MinValue)]