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 d527a4cf583216..7e8c3aefe04104 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 @@ -530,6 +530,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/System/Numerics/TotalOrderIeee754Comparer.cs b/src/libraries/System.Private.CoreLib/src/System/Numerics/TotalOrderIeee754Comparer.cs new file mode 100644 index 00000000000000..d461168433634a --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Numerics/TotalOrderIeee754Comparer.cs @@ -0,0 +1,233 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System.Numerics +{ + /// + /// Represents a comparison operation that compares floating-point numbers + /// with IEEE 754 totalOrder semantic. + /// + /// The type of the numbers to be compared, must be an IEEE 754 floating-point type. + public readonly struct TotalOrderIeee754Comparer : IComparer, IEqualityComparer, IEquatable> + where T : IFloatingPointIeee754? + { + /// + /// Compares two numbers with IEEE 754 totalOrder semantic and returns + /// a value indicating whether one is less than, equal to, or greater than the other. + /// + /// The first number to compare. + /// The second number to compare. + /// + /// A signed integer that indicates the relative + /// values of and , as shown in the following table. + /// + /// + /// Value + /// Meaning + /// + /// + /// Less than zero + /// is less than + /// + /// + /// Zero + /// equals + /// + /// + /// Greater than zero + /// is greater than + /// + /// + /// + /// + /// IEEE 754 specification defines totalOrder as <= semantic. + /// totalOrder(x,y) is when the result of this method is less than or equal to 0. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int Compare(T? x, T? y) + { + if (typeof(T) == typeof(float)) + { + return CompareIntegerSemantic(BitConverter.SingleToInt32Bits((float)(object)x!), BitConverter.SingleToInt32Bits((float)(object)y!)); + } + else if (typeof(T) == typeof(double)) + { + return CompareIntegerSemantic(BitConverter.DoubleToInt64Bits((double)(object)x!), BitConverter.DoubleToInt64Bits((double)(object)y!)); + } + else if (typeof(T) == typeof(Half)) + { + return CompareIntegerSemantic(BitConverter.HalfToInt16Bits((Half)(object)x!), BitConverter.HalfToInt16Bits((Half)(object)y!)); + } + else + { + return CompareGeneric(x, y); + } + + static int CompareIntegerSemantic(TInteger x, TInteger y) + where TInteger : struct, IBinaryInteger, ISignedNumber + { + // In IEEE 754 binary floating-point representation, a number is represented as Sign|Exponent|Significand + // Normal numbers has an implicit 1. in front of the significand, so value with larger exponent will have larger absolute value + // Inf and NaN are defined as Exponent=All 1s, while Inf has Significand=0, sNaN has Significand=0xxx and qNaN has Significand=1xxx + // This also satisfies totalOrder definition which is +x < +Inf < +sNaN < +qNaN + + // The order of NaNs of same category and same sign is implementation defined, + // here we define it as the order of exponent bits to simplify comparison + + // Negative values are represented in sign-magnitude, instead of two's complement like integers + // Just negating the comparison result when both numbers are negative is enough + + return (TInteger.IsNegative(x) && TInteger.IsNegative(y)) ? y.CompareTo(x) : x.CompareTo(y); + } + + static int CompareGeneric(T? x, T? y) + { + // IComparer contract is null < value + + if (x is null) + { + return (y is null) ? 0 : -1; + } + else if (y is null) + { + return 1; + } + + // If < or > returns true, the result satisfies definition of totalOrder too + + if (x < y) + { + return -1; + } + else if (x > y) + { + return 1; + } + else if (x == y) + { + if (T.IsZero(x)) // only zeros are equal to zeros + { + // 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; + } + } + 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; + } + } + else + { + // One or two of the values are NaN + // totalOrder defines that -qNaN < -sNaN < x < +sNaN < + qNaN + + static 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 + + int xSignificandBits = x!.GetSignificandBitLength(); + int ySignificandBits = y!.GetSignificandBitLength(); + + if (xSignificandBits == ySignificandBits) + { + // Prevent stack overflow for huge numbers + const int StackAllocThreshold = 256; + + int xSignificandLength = x.GetSignificandByteCount(); + int ySignificandLength = y.GetSignificandByteCount(); + + Span significandX = xSignificandLength <= StackAllocThreshold ? stackalloc byte[xSignificandLength] : new byte[xSignificandLength]; + Span significandY = ySignificandLength <= StackAllocThreshold ? stackalloc byte[ySignificandLength] : new byte[ySignificandLength]; + + x.WriteSignificandBigEndian(significandX); + y.WriteSignificandBigEndian(significandY); + + return significandX.SequenceCompareTo(significandY); + } + else + { + return xSignificandBits.CompareTo(ySignificandBits); + } + } + + if (T.IsNaN(x)) + { + if (T.IsNaN(y)) + { + if (T.IsNegative(x)) + { + return T.IsPositive(y) ? -1 : CompareSignificand(y, x); + } + else + { + return T.IsNegative(y) ? 1 : CompareSignificand(x, y); + } + } + else + { + return T.IsPositive(x) ? 1 : -1; + } + } + else if (T.IsNaN(y)) + { + return T.IsPositive(y) ? -1 : 1; + } + else + { + // T does not correctly implement IEEE754 semantics + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidArgumentForComparison); + return 0; // unreachable + } + } + } + } + + /// + /// Determines whether the specified numbers are equal. + /// + /// The first number of type to compare. + /// The second number of type to compare. + /// if the specified numbers are equal; otherwise, . + /// + /// There is no corresponding equals semantic with totalOrder defined by IEEE 754 specification. + /// This method returns when returns 0. + /// + public bool Equals(T? x, T? y) => Compare(x, y) == 0; + + /// + /// Returns a hash code for the specified number. + /// + /// The number for which a hash code is to be returned. + /// A hash code for the specified number. + public int GetHashCode([DisallowNull] T obj) + { + ArgumentNullException.ThrowIfNull(obj, nameof(obj)); + return obj.GetHashCode(); + } + + public bool Equals(TotalOrderIeee754Comparer other) => true; + + public override bool Equals([NotNullWhen(true)] object? obj) => obj is TotalOrderIeee754Comparer; + + public override int GetHashCode() => EqualityComparer.Default.GetHashCode(); + } +} diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 6ad2debb088229..d52f990054fcb2 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -10530,6 +10530,15 @@ public partial interface IUnaryPlusOperators where TSelf : Syste public partial interface IUnsignedNumber : System.Numerics.INumberBase where TSelf : System.Numerics.IUnsignedNumber? { } + public readonly partial struct TotalOrderIeee754Comparer : System.Collections.Generic.IComparer, System.Collections.Generic.IEqualityComparer, System.IEquatable> where T : System.Numerics.IFloatingPointIeee754? + { + public int Compare(T? x, T? y) { throw null; } + public bool Equals(System.Numerics.TotalOrderIeee754Comparer other) { throw null; } + public override bool Equals([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] object? obj) { throw null; } + public bool Equals(T? x, T? y) { throw null; } + public override int GetHashCode() { throw null; } + public int GetHashCode([System.Diagnostics.CodeAnalysis.DisallowNullAttribute] T obj) { throw null; } + } } namespace System.Reflection { diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj index fc6859854dc2e6..91d560c81a8d8c 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj @@ -113,6 +113,7 @@ + diff --git a/src/libraries/System.Runtime/tests/System/Numerics/TotalOrderIeee754ComparerTests.cs b/src/libraries/System.Runtime/tests/System/Numerics/TotalOrderIeee754ComparerTests.cs new file mode 100644 index 00000000000000..454a60dff8e641 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System/Numerics/TotalOrderIeee754ComparerTests.cs @@ -0,0 +1,104 @@ +// 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.Numerics; +using System.Runtime.InteropServices; +using Xunit; + +namespace System.Runtime.Tests +{ + public sealed class TotalOrderIeee754ComparerTests + { + public static IEnumerable SingleTestData + { + get + { + yield return new object[] { 0.0f, 0.0f, 0 }; + yield return new object[] { -0.0f, -0.0f, 0 }; + yield return new object[] { 0.0f, -0.0f, 1 }; + yield return new object[] { -0.0f, 0.0f, -1 }; + yield return new object[] { 0.0f, 1.0f, -1 }; + yield return new object[] { float.PositiveInfinity, 1.0f, 1 }; + yield return new object[] { BitConverter.UInt32BitsToSingle(0xFFC00000), float.NegativeInfinity, -1 }; + yield return new object[] { BitConverter.UInt32BitsToSingle(0xFFC00000), -1.0f, -1 }; + yield return new object[] { BitConverter.UInt32BitsToSingle(0x7FC00000), 1.0f, 1 }; + yield return new object[] { BitConverter.UInt32BitsToSingle(0x7FC00000), float.PositiveInfinity, 1 }; + yield return new object[] { float.NaN, float.NaN, 0 }; + yield return new object[] { BitConverter.UInt32BitsToSingle(0xFFC00000), BitConverter.UInt32BitsToSingle(0x7FC00000), -1 }; + yield return new object[] { BitConverter.UInt32BitsToSingle(0x7FC00000), BitConverter.UInt32BitsToSingle(0x7FC00001), -1 }; // implementation defined, not part of IEEE 754 totalOrder + } + } + + [Theory] + [MemberData(nameof(SingleTestData))] + public void TotalOrderTestSingle(float x, float y, int result) + { + var comparer = new TotalOrderIeee754Comparer(); + Assert.Equal(result, Math.Sign(comparer.Compare(x, y))); + } + + public static IEnumerable DoubleTestData + { + get + { + yield return new object[] { 0.0, 0.0, 0 }; + yield return new object[] { -0.0, -0.0, 0 }; + yield return new object[] { 0.0, -0.0, 1 }; + yield return new object[] { -0.0, 0.0, -1 }; + yield return new object[] { 0.0, 1.0, -1 }; + yield return new object[] { double.PositiveInfinity, 1.0, 1 }; + yield return new object[] { BitConverter.UInt64BitsToDouble(0xFFF80000_00000000), double.NegativeInfinity, -1 }; + yield return new object[] { BitConverter.UInt64BitsToDouble(0xFFF80000_00000000), -1.0, -1 }; + yield return new object[] { BitConverter.UInt64BitsToDouble(0x7FF80000_00000000), 1.0, 1 }; + yield return new object[] { BitConverter.UInt64BitsToDouble(0x7FF80000_00000000), double.PositiveInfinity, 1 }; + yield return new object[] { double.NaN, double.NaN, 0 }; + yield return new object[] { BitConverter.UInt64BitsToDouble(0xFFF80000_00000000), BitConverter.UInt64BitsToDouble(0x7FF80000_00000000), -1 }; + yield return new object[] { BitConverter.UInt64BitsToDouble(0x7FF80000_00000000), BitConverter.UInt64BitsToDouble(0x7FF80000_00000001), -1 }; // implementation defined, not part of IEEE 754 totalOrder + } + } + + [Theory] + [MemberData(nameof(DoubleTestData))] + public void TotalOrderTestDouble(double x, double y, int result) + { + var comparer = new TotalOrderIeee754Comparer(); + Assert.Equal(result, Math.Sign(comparer.Compare(x, y))); + } + public static IEnumerable HalfTestData + { + get + { + yield return new object[] { (Half)0.0, (Half)0.0, 0 }; + yield return new object[] { (Half)(-0.0), (Half)(-0.0), 0 }; + yield return new object[] { (Half)0.0, (Half)(-0.0), 1 }; + yield return new object[] { (Half)(-0.0), (Half)0.0, -1 }; + yield return new object[] { (Half)0.0, (Half)1.0, -1 }; + yield return new object[] { Half.PositiveInfinity, (Half)1.0, 1 }; + yield return new object[] { BitConverter.UInt16BitsToHalf(0xFE00), Half.NegativeInfinity, -1 }; + yield return new object[] { BitConverter.UInt16BitsToHalf(0xFE00), (Half)(-1.0), -1 }; + yield return new object[] { BitConverter.UInt16BitsToHalf(0x7E00), (Half)1.0, 1 }; + yield return new object[] { BitConverter.UInt16BitsToHalf(0x7E00), Half.PositiveInfinity, 1 }; + yield return new object[] { Half.NaN, Half.NaN, 0 }; + yield return new object[] { BitConverter.UInt16BitsToHalf(0xFE00), BitConverter.UInt16BitsToHalf(0x7E00), -1 }; + yield return new object[] { BitConverter.UInt16BitsToHalf(0x7E00), BitConverter.UInt16BitsToHalf(0x7E01), -1 }; // implementation defined, not part of IEEE 754 totalOrder + } + } + + [Theory] + [MemberData(nameof(HalfTestData))] + public void TotalOrderTestHalf(Half x, Half y, int result) + { + var comparer = new TotalOrderIeee754Comparer(); + Assert.Equal(result, Math.Sign(comparer.Compare(x, y))); + } + + [Theory] + [MemberData(nameof(SingleTestData))] + public void TotalOrderTestNFloat(float x, float y, int result) + { + var comparer = new TotalOrderIeee754Comparer(); + Assert.Equal(result, Math.Sign(comparer.Compare(x, y))); + } + } +}