From ab41ccfaa092b1d35d5abf4e80ec826ecdc87deb Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Thu, 13 Jul 2023 14:22:18 -0700 Subject: [PATCH 1/4] Expose the DegreesToRadians and RadiansToDegrees APIs --- .../src/System/Double.cs | 12 ++ .../System.Private.CoreLib/src/System/Half.cs | 6 + .../Numerics/ITrigonometricFunctions.cs | 16 +++ .../System/Runtime/InteropServices/NFloat.cs | 6 + .../src/System/Single.cs | 12 ++ .../ref/System.Runtime.InteropServices.cs | 2 + .../Runtime/InteropServices/NFloatTests.cs | 120 ++++++++++++++++++ .../System.Runtime/ref/System.Runtime.cs | 8 ++ .../tests/System/DoubleTests.cs | 60 +++++++++ .../System.Runtime/tests/System/HalfTests.cs | 72 +++++++++++ .../tests/System/SingleTests.cs | 60 +++++++++ 11 files changed, 374 insertions(+) diff --git a/src/libraries/System.Private.CoreLib/src/System/Double.cs b/src/libraries/System.Private.CoreLib/src/System/Double.cs index ba621512477f4..962a837fec00a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Double.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Double.cs @@ -1871,6 +1871,18 @@ public static double CosPi(double x) return result; } + /// + public static double DegreesToRadians(double degrees) + { + return (degrees * Pi) / 180.0; + } + + /// + public static double RadiansToDegrees(double radians) + { + return (radians * 180.0) / Pi; + } + /// [Intrinsic] public static double Sin(double x) => Math.Sin(x); diff --git a/src/libraries/System.Private.CoreLib/src/System/Half.cs b/src/libraries/System.Private.CoreLib/src/System/Half.cs index 58a7b7865ef82..255569672fcbe 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Half.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Half.cs @@ -2146,6 +2146,12 @@ private static bool TryConvertTo(Half value, [MaybeNullWhen(false)] out /// public static Half CosPi(Half x) => (Half)float.CosPi((float)x); + /// + public static Half DegreesToRadians(Half degrees) => (Half)float.DegreesToRadians((float)degrees); + + /// + public static Half RadiansToDegrees(Half radians) => (Half)float.RadiansToDegrees((float)radians); + /// public static Half Sin(Half x) => (Half)MathF.Sin((float)x); diff --git a/src/libraries/System.Private.CoreLib/src/System/Numerics/ITrigonometricFunctions.cs b/src/libraries/System.Private.CoreLib/src/System/Numerics/ITrigonometricFunctions.cs index 3231901ab6812..8892e327cdd5e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Numerics/ITrigonometricFunctions.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Numerics/ITrigonometricFunctions.cs @@ -57,6 +57,22 @@ public interface ITrigonometricFunctions /// This computes cos(x * PI). static abstract TSelf CosPi(TSelf x); + /// Converts a given value from degrees to radians. + /// The value to convert to radians. + /// The value of converted to radians. + static virtual TSelf DegreesToRadians(TSelf degrees) + { + return (degrees * TSelf.Pi) / TSelf.CreateChecked(180); + } + + /// Converts a given value from radians to degrees. + /// The value to convert to degrees. + /// The value of converted to degrees. + static virtual TSelf RadiansToDegrees(TSelf radians) + { + return (radians * TSelf.CreateChecked(180)) / TSelf.Pi; + } + /// Computes the sine of a value. /// The value, in radians, whose sine is to be computed. /// The sine of . diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/NFloat.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/NFloat.cs index f61091f19e932..f6efe3aff1bfa 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/NFloat.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/NFloat.cs @@ -1851,6 +1851,12 @@ private static bool TryConvertTo(NFloat value, [MaybeNullWhen(false)] ou /// public static NFloat CosPi(NFloat x) => new NFloat(NativeType.CosPi(x._value)); + /// + public static NFloat DegreesToRadians(NFloat degrees) => new NFloat(NativeType.DegreesToRadians(degrees._value)); + + /// + public static NFloat RadiansToDegrees(NFloat radians) => new NFloat(NativeType.RadiansToDegrees(radians._value)); + /// public static NFloat Sin(NFloat x) => new NFloat(NativeType.Sin(x._value)); diff --git a/src/libraries/System.Private.CoreLib/src/System/Single.cs b/src/libraries/System.Private.CoreLib/src/System/Single.cs index a4fa4fa4101c9..214652374a756 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Single.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Single.cs @@ -1752,6 +1752,18 @@ public static float CosPi(float x) return result; } + /// + public static float DegreesToRadians(float degrees) + { + return (degrees * Pi) / 180.0f; + } + + /// + public static float RadiansToDegrees(float radians) + { + return (radians * 180.0f) / Pi; + } + /// [Intrinsic] public static float Sin(float x) => MathF.Sin(x); diff --git a/src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.cs b/src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.cs index 1744a5f234b81..0d80a901d93ad 100644 --- a/src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.cs +++ b/src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.cs @@ -1331,6 +1331,7 @@ public static void Free(void* ptr) { } public static System.Runtime.InteropServices.NFloat CreateChecked(TOther value) where TOther : System.Numerics.INumberBase { throw null; } public static System.Runtime.InteropServices.NFloat CreateSaturating(TOther value) where TOther : System.Numerics.INumberBase { throw null; } public static System.Runtime.InteropServices.NFloat CreateTruncating(TOther value) where TOther : System.Numerics.INumberBase { throw null; } + public static System.Runtime.InteropServices.NFloat DegreesToRadians(System.Runtime.InteropServices.NFloat degrees) { throw null; } public override bool Equals([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] object? obj) { throw null; } public bool Equals(System.Runtime.InteropServices.NFloat other) { throw null; } public static System.Runtime.InteropServices.NFloat Exp(System.Runtime.InteropServices.NFloat x) { throw null; } @@ -1464,6 +1465,7 @@ public static void Free(void* ptr) { } public static System.Runtime.InteropServices.NFloat Parse(string s, System.Globalization.NumberStyles style, System.IFormatProvider? provider) { throw null; } public static System.Runtime.InteropServices.NFloat Parse(string s, System.IFormatProvider? provider) { throw null; } public static System.Runtime.InteropServices.NFloat Pow(System.Runtime.InteropServices.NFloat x, System.Runtime.InteropServices.NFloat y) { throw null; } + public static System.Runtime.InteropServices.NFloat RadiansToDegrees(System.Runtime.InteropServices.NFloat radians) { throw null; } public static System.Runtime.InteropServices.NFloat ReciprocalEstimate(System.Runtime.InteropServices.NFloat x) { throw null; } public static System.Runtime.InteropServices.NFloat ReciprocalSqrtEstimate(System.Runtime.InteropServices.NFloat x) { throw null; } public static System.Runtime.InteropServices.NFloat RootN(System.Runtime.InteropServices.NFloat x, int n) { throw null; } diff --git a/src/libraries/System.Runtime.InteropServices/tests/System.Runtime.InteropServices.UnitTests/System/Runtime/InteropServices/NFloatTests.cs b/src/libraries/System.Runtime.InteropServices/tests/System.Runtime.InteropServices.UnitTests/System/Runtime/InteropServices/NFloatTests.cs index 8a68e89a332c6..bd14d939f1671 100644 --- a/src/libraries/System.Runtime.InteropServices/tests/System.Runtime.InteropServices.UnitTests/System/Runtime/InteropServices/NFloatTests.cs +++ b/src/libraries/System.Runtime.InteropServices/tests/System.Runtime.InteropServices.UnitTests/System/Runtime/InteropServices/NFloatTests.cs @@ -2545,5 +2545,125 @@ public static void LerpTest64(double value1, double value2, double amount, doubl AssertExtensions.Equal(+expectedResult, NFloat.Lerp((NFloat)(+value1), (NFloat)(+value2), (NFloat)(amount)), 0); AssertExtensions.Equal((expectedResult == 0.0) ? expectedResult : -expectedResult, NFloat.Lerp((NFloat)(-value1), (NFloat)(-value2), (NFloat)(amount)), 0); } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.Is32BitProcess))] + [InlineData(float.NaN, float.NaN, 0.0f)] + [InlineData(0.0f, 0.0f, 0.0f)] + [InlineData(0.318309886f, 0.0055555557f, CrossPlatformMachineEpsilon32)] // value: (1 / pi) + [InlineData(0.434294482f, 0.007579869f, CrossPlatformMachineEpsilon32)] // value: (log10(e)) + [InlineData(0.5f, 0.008726646f, CrossPlatformMachineEpsilon32)] + [InlineData(0.636619772f, 0.011111111f, CrossPlatformMachineEpsilon32)] // value: (2 / pi) + [InlineData(0.693147181f, 0.0120977005f, CrossPlatformMachineEpsilon32)] // value: (ln(2)) + [InlineData(0.707106781f, 0.012341342f, CrossPlatformMachineEpsilon32)] // value: (1 / sqrt(2)) + [InlineData(0.785398163f, 0.013707785f, CrossPlatformMachineEpsilon32)] // value: (pi / 4) + [InlineData(1.0f, 0.017453292f, CrossPlatformMachineEpsilon32)] + [InlineData(1.12837917f, 0.019693933f, CrossPlatformMachineEpsilon32)] // value: (2 / sqrt(pi)) + [InlineData(1.41421356f, 0.024682684f, CrossPlatformMachineEpsilon32)] // value: (sqrt(2)) + [InlineData(1.44269504f, 0.025179777f, CrossPlatformMachineEpsilon32)] // value: (log2(e)) + [InlineData(1.5f, 0.02617994f, CrossPlatformMachineEpsilon32)] + [InlineData(1.57079633f, 0.02741557f, CrossPlatformMachineEpsilon32)] // value: (pi / 2) + [InlineData(2.0f, 0.034906585f, CrossPlatformMachineEpsilon32)] + [InlineData(2.30258509f, 0.040187694f, CrossPlatformMachineEpsilon32)] // value: (ln(10)) + [InlineData(2.5f, 0.043633234f, CrossPlatformMachineEpsilon32)] + [InlineData(2.71828183f, 0.047442965f, CrossPlatformMachineEpsilon32)] // value: (e) + [InlineData(3.0f, 0.05235988f, CrossPlatformMachineEpsilon32)] + [InlineData(3.14159265f, 0.05483114f, CrossPlatformMachineEpsilon32)] // value: (pi) + [InlineData(3.5f, 0.061086528f, CrossPlatformMachineEpsilon32)] + [InlineData(float.PositiveInfinity, float.PositiveInfinity, 0.0f)] + public static void DegreesToRadiansTest32(float value, float expectedResult, float allowedVariance) + { + AssertExtensions.Equal(-expectedResult, (float)NFloat.DegreesToRadians(-value), allowedVariance); + AssertExtensions.Equal(+expectedResult, (float)NFloat.DegreesToRadians(+value), allowedVariance); + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.Is64BitProcess))] + [InlineData(double.NaN, double.NaN, 0.0)] + [InlineData(0.0, 0.0, 0.0)] + [InlineData(0.31830988618379067, 0.005555555555555556, CrossPlatformMachineEpsilon64)] // value: (1 / pi) + [InlineData(0.43429448190325183, 0.007579868632454674, CrossPlatformMachineEpsilon64)] // value: (log10(e)) + [InlineData(0.5, 0.008726646259971648, CrossPlatformMachineEpsilon64)] + [InlineData(0.63661977236758134, 0.011111111111111112, CrossPlatformMachineEpsilon64)] // value: (2 / pi) + [InlineData(0.69314718055994531, 0.01209770050168668, CrossPlatformMachineEpsilon64)] // value: (ln(2)) + [InlineData(0.70710678118654752, 0.012341341494884351, CrossPlatformMachineEpsilon64)] // value: (1 / sqrt(2)) + [InlineData(0.78539816339744831, 0.013707783890401885, CrossPlatformMachineEpsilon64)] // value: (pi / 4) + [InlineData(1.0, 0.017453292519943295, CrossPlatformMachineEpsilon64)] + [InlineData(1.1283791670955126, 0.019693931676727953, CrossPlatformMachineEpsilon64)] // value: (2 / sqrt(pi)) + [InlineData(1.4142135623730950, 0.024682682989768702, CrossPlatformMachineEpsilon64)] // value: (sqrt(2)) + [InlineData(1.4426950408889634, 0.02517977856570663, CrossPlatformMachineEpsilon64)] // value: (log2(e)) + [InlineData(1.5, 0.02617993877991494, CrossPlatformMachineEpsilon64)] + [InlineData(1.5707963267948966, 0.02741556778080377, CrossPlatformMachineEpsilon64)] // value: (pi / 2) + [InlineData(2.0, 0.03490658503988659, CrossPlatformMachineEpsilon64)] + [InlineData(2.3025850929940457, 0.040187691180085916, CrossPlatformMachineEpsilon64)] // value: (ln(10)) + [InlineData(2.5, 0.04363323129985824, CrossPlatformMachineEpsilon64)] + [InlineData(2.7182818284590452, 0.047442967903742035, CrossPlatformMachineEpsilon64)] // value: (e) + [InlineData(3.0, 0.05235987755982988, CrossPlatformMachineEpsilon64)] + [InlineData(3.1415926535897932, 0.05483113556160754, CrossPlatformMachineEpsilon64)] // value: (pi) + [InlineData(3.5, 0.061086523819801536, CrossPlatformMachineEpsilon64)] + [InlineData(double.PositiveInfinity, double.PositiveInfinity, 0.0)] + public static void DegreesToRadiansTest64(double value, double expectedResult, double allowedVariance) + { + AssertExtensions.Equal(-expectedResult, NFloat.DegreesToRadians((NFloat)(-value)), allowedVariance); + AssertExtensions.Equal(+expectedResult, NFloat.DegreesToRadians((NFloat)(+value)), allowedVariance); + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.Is32BitProcess))] + [InlineData(float.NaN, float.NaN, 0.0)] + [InlineData(0.0f, 0.0f, 0.0)] + [InlineData(0.0055555557f, 0.318309886f, CrossPlatformMachineEpsilon32)] // expected: (1 / pi) + [InlineData(0.007579869f, 0.434294482f, CrossPlatformMachineEpsilon32)] // expected: (log10(e)) + [InlineData(0.008726646f, 0.5f, CrossPlatformMachineEpsilon32)] + [InlineData(0.011111111f, 0.636619772f, CrossPlatformMachineEpsilon32)] // expected: (2 / pi) + [InlineData(0.0120977005f, 0.693147181f, CrossPlatformMachineEpsilon32)] // expected: (ln(2)) + [InlineData(0.012341342f, 0.707106781f, CrossPlatformMachineEpsilon32)] // expected: (1 / sqrt(2)) + [InlineData(0.013707785f, 0.785398163f, CrossPlatformMachineEpsilon32)] // expected: (pi / 4) + [InlineData(0.017453292f, 1.0f, CrossPlatformMachineEpsilon32)] + [InlineData(0.019693933f, 1.12837917f, CrossPlatformMachineEpsilon32)] // expected: (2 / sqrt(pi)) + [InlineData(0.024682684f, 1.41421356f, CrossPlatformMachineEpsilon32)] // expected: (sqrt(2)) + [InlineData(0.025179777f, 1.44269504f, CrossPlatformMachineEpsilon32)] // expected: (log2(e)) + [InlineData(0.02617994f, 1.5f, CrossPlatformMachineEpsilon32)] + [InlineData(0.02741557f, 1.57079633f, CrossPlatformMachineEpsilon32)] // expected: (pi / 2) + [InlineData(0.034906585f, 2.0f, CrossPlatformMachineEpsilon32)] + [InlineData(0.040187694f, 2.30258509f, CrossPlatformMachineEpsilon32)] // expected: (ln(10)) + [InlineData(0.043633234f, 2.5f, CrossPlatformMachineEpsilon32)] + [InlineData(0.047442965f, 2.71828183f, CrossPlatformMachineEpsilon32)] // expected: (e) + [InlineData(0.05235988f, 3.0f, CrossPlatformMachineEpsilon32)] + [InlineData(0.05483114f, 3.14159265f, CrossPlatformMachineEpsilon32)] // expected: (pi) + [InlineData(0.061086528f, 3.5f, CrossPlatformMachineEpsilon32)] + [InlineData(float.PositiveInfinity, float.PositiveInfinity, 0.0)] + public static void RadiansToDegreesTest32(float value, float expectedResult, float allowedVariance) + { + AssertExtensions.Equal(-expectedResult, (float)NFloat.RadiansToDegrees(-value), allowedVariance); + AssertExtensions.Equal(+expectedResult, (float)NFloat.RadiansToDegrees(+value), allowedVariance); + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.Is64BitProcess))] + [InlineData(double.NaN, double.NaN, 0.0)] + [InlineData(0.0, 0.0, 0.0)] + [InlineData(0.0055555555555555567, 0.3183098861837906, CrossPlatformMachineEpsilon64)] // expected: (1 / pi) + [InlineData(0.0075798686324546743, 0.4342944819032518, CrossPlatformMachineEpsilon64)] // expected: (log10(e)) + [InlineData(0.008726646259971648, 0.5, CrossPlatformMachineEpsilon64)] + [InlineData(0.0111111111111111124, 0.6366197723675813, CrossPlatformMachineEpsilon64)] // expected: (2 / pi) + [InlineData(0.0120977005016866801, 0.6931471805599453, CrossPlatformMachineEpsilon64)] // expected: (ln(2)) + [InlineData(0.0123413414948843512, 0.7071067811865475, CrossPlatformMachineEpsilon64)] // expected: (1 / sqrt(2)) + [InlineData(0.0137077838904018851, 0.7853981633974483, CrossPlatformMachineEpsilon64)] // expected: (pi / 4) + [InlineData(0.017453292519943295, 1.0, CrossPlatformMachineEpsilon64)] + [InlineData(0.019693931676727953, 1.1283791670955126, CrossPlatformMachineEpsilon64)] // expected: (2 / sqrt(pi)) + [InlineData(0.024682682989768702, 1.4142135623730950, CrossPlatformMachineEpsilon64)] // expected: (sqrt(2)) + [InlineData(0.025179778565706630, 1.4426950408889634, CrossPlatformMachineEpsilon64)] // expected: (log2(e)) + [InlineData(0.026179938779914940, 1.5, CrossPlatformMachineEpsilon64)] + [InlineData(0.027415567780803770, 1.5707963267948966, CrossPlatformMachineEpsilon64)] // expected: (pi / 2) + [InlineData(0.034906585039886590, 2.0, CrossPlatformMachineEpsilon64)] + [InlineData(0.040187691180085916, 2.3025850929940457, CrossPlatformMachineEpsilon64)] // expected: (ln(10)) + [InlineData(0.043633231299858240, 2.5, CrossPlatformMachineEpsilon64)] + [InlineData(0.047442967903742035, 2.7182818284590452, CrossPlatformMachineEpsilon64)] // expected: (e) + [InlineData(0.052359877559829880, 3.0, CrossPlatformMachineEpsilon64)] + [InlineData(0.054831135561607540, 3.1415926535897932, CrossPlatformMachineEpsilon64)] // expected: (pi) + [InlineData(0.061086523819801536, 3.5, CrossPlatformMachineEpsilon64)] + [InlineData(double.PositiveInfinity, double.PositiveInfinity, 0.0)] + public static void RadiansToDegreesTest64(double value, double expectedResult, double allowedVariance) + { + AssertExtensions.Equal(-expectedResult, NFloat.RadiansToDegrees((NFloat)(-value)), allowedVariance); + AssertExtensions.Equal(+expectedResult, NFloat.RadiansToDegrees((NFloat)(+value)), allowedVariance); + } } } diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 08e6907f7e80e..f6975bc73592c 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -2247,6 +2247,7 @@ public DivideByZeroException(string? message, System.Exception? innerException) public static double CreateChecked(TOther value) where TOther : System.Numerics.INumberBase { throw null; } public static double CreateSaturating(TOther value) where TOther : System.Numerics.INumberBase { throw null; } public static double CreateTruncating(TOther value) where TOther : System.Numerics.INumberBase { throw null; } + public static double DegreesToRadians(double degrees) { throw null; } public bool Equals(double obj) { throw null; } public override bool Equals([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] object? obj) { throw null; } public static double Exp(double x) { throw null; } @@ -2307,6 +2308,7 @@ public DivideByZeroException(string? message, System.Exception? innerException) public static double Parse(string s, System.Globalization.NumberStyles style, System.IFormatProvider? provider) { throw null; } public static double Parse(string s, System.IFormatProvider? provider) { throw null; } public static double Pow(double x, double y) { throw null; } + public static double RadiansToDegrees(double radians) { throw null; } public static double ReciprocalEstimate(double x) { throw null; } public static double ReciprocalSqrtEstimate(double x) { throw null; } public static double RootN(double x, int n) { throw null; } @@ -2871,6 +2873,7 @@ public enum GCNotificationStatus public static System.Half CreateChecked(TOther value) where TOther : System.Numerics.INumberBase { throw null; } public static System.Half CreateSaturating(TOther value) where TOther : System.Numerics.INumberBase { throw null; } public static System.Half CreateTruncating(TOther value) where TOther : System.Numerics.INumberBase { throw null; } + public static System.Half DegreesToRadians(System.Half degrees) { throw null; } public bool Equals(System.Half other) { throw null; } public override bool Equals([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] object? obj) { throw null; } public static System.Half Exp(System.Half x) { throw null; } @@ -2999,6 +3002,7 @@ public enum GCNotificationStatus public static System.Half Parse(string s, System.Globalization.NumberStyles style = System.Globalization.NumberStyles.AllowDecimalPoint | System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowLeadingSign | System.Globalization.NumberStyles.AllowLeadingWhite | System.Globalization.NumberStyles.AllowThousands | System.Globalization.NumberStyles.AllowTrailingWhite, System.IFormatProvider? provider = null) { throw null; } public static System.Half Parse(string s, System.IFormatProvider? provider) { throw null; } public static System.Half Pow(System.Half x, System.Half y) { throw null; } + public static System.Half RadiansToDegrees(System.Half radians) { throw null; } public static System.Half ReciprocalEstimate(System.Half x) { throw null; } public static System.Half ReciprocalSqrtEstimate(System.Half x) { throw null; } public static System.Half RootN(System.Half x, int n) { throw null; } @@ -4985,6 +4989,7 @@ public SerializableAttribute() { } public static float CreateChecked(TOther value) where TOther : System.Numerics.INumberBase { throw null; } public static float CreateSaturating(TOther value) where TOther : System.Numerics.INumberBase { throw null; } public static float CreateTruncating(TOther value) where TOther : System.Numerics.INumberBase { throw null; } + public static float DegreesToRadians(float degrees) { throw null; } public override bool Equals([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] object? obj) { throw null; } public bool Equals(float obj) { throw null; } public static float Exp(float x) { throw null; } @@ -5045,6 +5050,7 @@ public SerializableAttribute() { } public static float Parse(string s, System.Globalization.NumberStyles style, System.IFormatProvider? provider) { throw null; } public static float Parse(string s, System.IFormatProvider? provider) { throw null; } public static float Pow(float x, float y) { throw null; } + public static float RadiansToDegrees(float radians) { throw null; } public static float ReciprocalEstimate(float x) { throw null; } public static float ReciprocalSqrtEstimate(float x) { throw null; } public static float RootN(float x, int n) { throw null; } @@ -10911,6 +10917,8 @@ public partial interface ITrigonometricFunctions : System.Numerics.IFloat static abstract TSelf AtanPi(TSelf x); static abstract TSelf Cos(TSelf x); static abstract TSelf CosPi(TSelf x); + static virtual TSelf DegreesToRadians(TSelf degrees) { throw null; } + static virtual TSelf RadiansToDegrees(TSelf radians) { throw null; } static abstract TSelf Sin(TSelf x); static abstract (TSelf Sin, TSelf Cos) SinCos(TSelf x); static abstract (TSelf SinPi, TSelf CosPi) SinCosPi(TSelf x); diff --git a/src/libraries/System.Runtime/tests/System/DoubleTests.cs b/src/libraries/System.Runtime/tests/System/DoubleTests.cs index 6c8cfda43d565..3c159c0534e7e 100644 --- a/src/libraries/System.Runtime/tests/System/DoubleTests.cs +++ b/src/libraries/System.Runtime/tests/System/DoubleTests.cs @@ -1685,5 +1685,65 @@ public static void LerpTest(double value1, double value2, double amount, double AssertExtensions.Equal(+expectedResult, double.Lerp(+value1, +value2, amount), 0); AssertExtensions.Equal((expectedResult == 0.0) ? expectedResult : -expectedResult, double.Lerp(-value1, -value2, amount), 0); } + + [Theory] + [InlineData(double.NaN, double.NaN, 0.0)] + [InlineData(0.0, 0.0, 0.0)] + [InlineData(0.31830988618379067, 0.005555555555555556, CrossPlatformMachineEpsilon)] // value: (1 / pi) + [InlineData(0.43429448190325183, 0.007579868632454674, CrossPlatformMachineEpsilon)] // value: (log10(e)) + [InlineData(0.5, 0.008726646259971648, CrossPlatformMachineEpsilon)] + [InlineData(0.63661977236758134, 0.011111111111111112, CrossPlatformMachineEpsilon)] // value: (2 / pi) + [InlineData(0.69314718055994531, 0.01209770050168668, CrossPlatformMachineEpsilon)] // value: (ln(2)) + [InlineData(0.70710678118654752, 0.012341341494884351, CrossPlatformMachineEpsilon)] // value: (1 / sqrt(2)) + [InlineData(0.78539816339744831, 0.013707783890401885, CrossPlatformMachineEpsilon)] // value: (pi / 4) + [InlineData(1.0, 0.017453292519943295, CrossPlatformMachineEpsilon)] + [InlineData(1.1283791670955126, 0.019693931676727953, CrossPlatformMachineEpsilon)] // value: (2 / sqrt(pi)) + [InlineData(1.4142135623730950, 0.024682682989768702, CrossPlatformMachineEpsilon)] // value: (sqrt(2)) + [InlineData(1.4426950408889634, 0.02517977856570663, CrossPlatformMachineEpsilon)] // value: (log2(e)) + [InlineData(1.5, 0.02617993877991494, CrossPlatformMachineEpsilon)] + [InlineData(1.5707963267948966, 0.02741556778080377, CrossPlatformMachineEpsilon)] // value: (pi / 2) + [InlineData(2.0, 0.03490658503988659, CrossPlatformMachineEpsilon)] + [InlineData(2.3025850929940457, 0.040187691180085916, CrossPlatformMachineEpsilon)] // value: (ln(10)) + [InlineData(2.5, 0.04363323129985824, CrossPlatformMachineEpsilon)] + [InlineData(2.7182818284590452, 0.047442967903742035, CrossPlatformMachineEpsilon)] // value: (e) + [InlineData(3.0, 0.05235987755982988, CrossPlatformMachineEpsilon)] + [InlineData(3.1415926535897932, 0.05483113556160754, CrossPlatformMachineEpsilon)] // value: (pi) + [InlineData(3.5, 0.061086523819801536, CrossPlatformMachineEpsilon)] + [InlineData(double.PositiveInfinity, double.PositiveInfinity, 0.0)] + public static void DegreesToRadiansTest(double value, double expectedResult, double allowedVariance) + { + AssertExtensions.Equal(-expectedResult, double.DegreesToRadians(-value), allowedVariance); + AssertExtensions.Equal(+expectedResult, double.DegreesToRadians(+value), allowedVariance); + } + + [Theory] + [InlineData(double.NaN, double.NaN, 0.0)] + [InlineData(0.0, 0.0, 0.0)] + [InlineData(0.0055555555555555567, 0.3183098861837906, CrossPlatformMachineEpsilon)] // expected: (1 / pi) + [InlineData(0.0075798686324546743, 0.4342944819032518, CrossPlatformMachineEpsilon)] // expected: (log10(e)) + [InlineData(0.008726646259971648, 0.5, CrossPlatformMachineEpsilon)] + [InlineData(0.0111111111111111124, 0.6366197723675813, CrossPlatformMachineEpsilon)] // expected: (2 / pi) + [InlineData(0.0120977005016866801, 0.6931471805599453, CrossPlatformMachineEpsilon)] // expected: (ln(2)) + [InlineData(0.0123413414948843512, 0.7071067811865475, CrossPlatformMachineEpsilon)] // expected: (1 / sqrt(2)) + [InlineData(0.0137077838904018851, 0.7853981633974483, CrossPlatformMachineEpsilon)] // expected: (pi / 4) + [InlineData(0.017453292519943295, 1.0, CrossPlatformMachineEpsilon)] + [InlineData(0.019693931676727953, 1.1283791670955126, CrossPlatformMachineEpsilon)] // expected: (2 / sqrt(pi)) + [InlineData(0.024682682989768702, 1.4142135623730950, CrossPlatformMachineEpsilon)] // expected: (sqrt(2)) + [InlineData(0.025179778565706630, 1.4426950408889634, CrossPlatformMachineEpsilon)] // expected: (log2(e)) + [InlineData(0.026179938779914940, 1.5, CrossPlatformMachineEpsilon)] + [InlineData(0.027415567780803770, 1.5707963267948966, CrossPlatformMachineEpsilon)] // expected: (pi / 2) + [InlineData(0.034906585039886590, 2.0, CrossPlatformMachineEpsilon)] + [InlineData(0.040187691180085916, 2.3025850929940457, CrossPlatformMachineEpsilon)] // expected: (ln(10)) + [InlineData(0.043633231299858240, 2.5, CrossPlatformMachineEpsilon)] + [InlineData(0.047442967903742035, 2.7182818284590452, CrossPlatformMachineEpsilon)] // expected: (e) + [InlineData(0.052359877559829880, 3.0, CrossPlatformMachineEpsilon)] + [InlineData(0.054831135561607540, 3.1415926535897932, CrossPlatformMachineEpsilon)] // expected: (pi) + [InlineData(0.061086523819801536, 3.5, CrossPlatformMachineEpsilon)] + [InlineData(double.PositiveInfinity, double.PositiveInfinity, 0.0)] + public static void RadiansToDegreesTest(double value, double expectedResult, double allowedVariance) + { + AssertExtensions.Equal(-expectedResult, double.RadiansToDegrees(-value), allowedVariance); + AssertExtensions.Equal(+expectedResult, double.RadiansToDegrees(+value), allowedVariance); + } } } diff --git a/src/libraries/System.Runtime/tests/System/HalfTests.cs b/src/libraries/System.Runtime/tests/System/HalfTests.cs index b37476cb9ae63..e4212859334c4 100644 --- a/src/libraries/System.Runtime/tests/System/HalfTests.cs +++ b/src/libraries/System.Runtime/tests/System/HalfTests.cs @@ -2143,5 +2143,77 @@ public static void LerpTest(Half value1, Half value2, Half amount, Half expected AssertExtensions.Equal(+expectedResult, Half.Lerp(+value1, +value2, amount), Half.Zero); AssertExtensions.Equal((expectedResult == Half.Zero) ? expectedResult : -expectedResult, Half.Lerp(-value1, -value2, amount), Half.Zero); } + + public static IEnumerable DegreesToRadians_TestData() + { + yield return new object[] { Half.PositiveInfinity, Half.NaN, Half.Zero }; + yield return new object[] { Half.NaN, Half.NaN, Half.Zero }; + yield return new object[] { Half.Zero, Half.Zero, Half.Zero }; + yield return new object[] { (Half)(0.3184f), (Half)(0.005554f), CrossPlatformMachineEpsilon }; // value: (1 / pi) + yield return new object[] { (Half)(0.4343f), (Half)(0.00758f), CrossPlatformMachineEpsilon }; // value: (log10(e)) + yield return new object[] { (Half)(0.5f), (Half)(0.00872f), CrossPlatformMachineEpsilon }; + yield return new object[] { (Half)(0.6367f), (Half)(0.01111f), CrossPlatformMachineEpsilon }; // value: (2 / pi) + yield return new object[] { (Half)(0.6934f), (Half)(0.0121f), CrossPlatformMachineEpsilon }; // value: (ln(2)) + yield return new object[] { (Half)(0.707f), (Half)(0.01234f), CrossPlatformMachineEpsilon }; // value: (1 / sqrt(2)) + yield return new object[] { (Half)(0.785f), (Half)(0.0137f), CrossPlatformMachineEpsilon }; // value: (pi / 4) + yield return new object[] { (Half)(1.0f), (Half)(0.01744f), CrossPlatformMachineEpsilon }; + yield return new object[] { (Half)(1.128f), (Half)(0.01968f), CrossPlatformMachineEpsilon }; // value: (2 / sqrt(pi)) + yield return new object[] { (Half)(1.414f), (Half)(0.02467f), CrossPlatformMachineEpsilon }; // value: (sqrt(2)) + yield return new object[] { (Half)(1.442f), (Half)(0.02518f), CrossPlatformMachineEpsilon }; // value: (log2(e)) + yield return new object[] { (Half)(1.5f), (Half)(0.02617f), CrossPlatformMachineEpsilon }; + yield return new object[] { (Half)(1.57f), (Half)(0.0274f), CrossPlatformMachineEpsilon }; // value: (pi / 2) + yield return new object[] { (Half)(2.0f), (Half)(0.03488f), CrossPlatformMachineEpsilon }; + yield return new object[] { (Half)(2.303f), (Half)(0.04016f), CrossPlatformMachineEpsilon }; // value: (ln(10)) + yield return new object[] { (Half)(2.5f), (Half)(0.0436f), CrossPlatformMachineEpsilon }; + yield return new object[] { (Half)(2.719f), (Half)(0.04742f), CrossPlatformMachineEpsilon }; // value: (e) + yield return new object[] { (Half)(3.0f), (Half)(0.05234f), CrossPlatformMachineEpsilon }; + yield return new object[] { (Half)(3.14f), (Half)(0.0548f), CrossPlatformMachineEpsilon }; // value: (pi) + yield return new object[] { (Half)(3.5f), (Half)(0.06107f), CrossPlatformMachineEpsilon }; + yield return new object[] { Half.PositiveInfinity, Half.PositiveInfinity, Half.Zero }; + } + + [Theory] + [MemberData(nameof(DegreesToRadians_TestData))] + public static void DegreesToRadiansTest(Half value, Half expectedResult, Half allowedVariance) + { + AssertExtensions.Equal(-expectedResult, Half.DegreesToRadians(-value), allowedVariance); + AssertExtensions.Equal(+expectedResult, Half.DegreesToRadians(+value), allowedVariance); + } + + public static IEnumerable RadiansToDegrees_TestData() + { + yield return new object[] { Half.NaN, Half.PositiveInfinity, Half.Zero }; + yield return new object[] { Half.NaN, Half.NaN, Half.Zero }; + yield return new object[] { Half.Zero, Half.Zero, Half.Zero }; + yield return new object[] { (Half)(0.005554f), (Half)(0.3184f), CrossPlatformMachineEpsilon }; // value: (1 / pi) + yield return new object[] { (Half)(0.00758f), (Half)(0.4343f), CrossPlatformMachineEpsilon }; // value: (log10(e)) + yield return new object[] { (Half)(0.00872f), (Half)(0.5f), CrossPlatformMachineEpsilon }; + yield return new object[] { (Half)(0.01111f), (Half)(0.6367f), CrossPlatformMachineEpsilon }; // value: (2 / pi) + yield return new object[] { (Half)(0.0121f), (Half)(0.6934f), CrossPlatformMachineEpsilon }; // value: (ln(2)) + yield return new object[] { (Half)(0.01234f), (Half)(0.707f), CrossPlatformMachineEpsilon }; // value: (1 / sqrt(2)) + yield return new object[] { (Half)(0.0137f), (Half)(0.785f), CrossPlatformMachineEpsilon }; // value: (pi / 4) + yield return new object[] { (Half)(0.01744f), (Half)(1.0f), CrossPlatformMachineEpsilon }; + yield return new object[] { (Half)(0.01968f), (Half)(1.128f), CrossPlatformMachineEpsilon }; // value: (2 / sqrt(pi)) + yield return new object[] { (Half)(0.02467f), (Half)(1.414f), CrossPlatformMachineEpsilon }; // value: (sqrt(2)) + yield return new object[] { (Half)(0.02518f), (Half)(1.442f), CrossPlatformMachineEpsilon }; // value: (log2(e)) + yield return new object[] { (Half)(0.02617f), (Half)(1.5f), CrossPlatformMachineEpsilon }; + yield return new object[] { (Half)(0.0274f), (Half)(1.57f), CrossPlatformMachineEpsilon }; // value: (pi / 2) + yield return new object[] { (Half)(0.03488f), (Half)(2.0f), CrossPlatformMachineEpsilon }; + yield return new object[] { (Half)(0.04016f), (Half)(2.303f), CrossPlatformMachineEpsilon }; // value: (ln(10)) + yield return new object[] { (Half)(0.0436f), (Half)(2.5f), CrossPlatformMachineEpsilon }; + yield return new object[] { (Half)(0.04742f), (Half)(2.719f), CrossPlatformMachineEpsilon }; // value: (e) + yield return new object[] { (Half)(0.05234f), (Half)(3.0f), CrossPlatformMachineEpsilon }; + yield return new object[] { (Half)(0.0548f), (Half)(3.14f), CrossPlatformMachineEpsilon }; // value: (pi) + yield return new object[] { (Half)(0.06107f), (Half)(3.5f), CrossPlatformMachineEpsilon }; + yield return new object[] { Half.PositiveInfinity, Half.PositiveInfinity, Half.Zero }; + } + + [Theory] + [MemberData(nameof(RadiansToDegrees_TestData))] + public static void RadiansToDegreesTest(Half value, Half expectedResult, Half allowedVariance) + { + AssertExtensions.Equal(-expectedResult, Half.RadiansToDegrees(-value), allowedVariance); + AssertExtensions.Equal(+expectedResult, Half.RadiansToDegrees(+value), allowedVariance); + } } } diff --git a/src/libraries/System.Runtime/tests/System/SingleTests.cs b/src/libraries/System.Runtime/tests/System/SingleTests.cs index 176ffc0993cc9..459bdd843f5a9 100644 --- a/src/libraries/System.Runtime/tests/System/SingleTests.cs +++ b/src/libraries/System.Runtime/tests/System/SingleTests.cs @@ -1597,5 +1597,65 @@ public static void LerpTest(float value1, float value2, float amount, float expe AssertExtensions.Equal(+expectedResult, float.Lerp(+value1, +value2, amount), 0); AssertExtensions.Equal((expectedResult == 0.0f) ? expectedResult : -expectedResult, float.Lerp(-value1, -value2, amount), 0); } + + [Theory] + [InlineData(float.NaN, float.NaN, 0.0f)] + [InlineData(0.0f, 0.0f, 0.0f)] + [InlineData(0.318309886f, 0.0055555557f, CrossPlatformMachineEpsilon)] // value: (1 / pi) + [InlineData(0.434294482f, 0.007579869f, CrossPlatformMachineEpsilon)] // value: (log10(e)) + [InlineData(0.5f, 0.008726646f, CrossPlatformMachineEpsilon)] + [InlineData(0.636619772f, 0.011111111f, CrossPlatformMachineEpsilon)] // value: (2 / pi) + [InlineData(0.693147181f, 0.0120977005f, CrossPlatformMachineEpsilon)] // value: (ln(2)) + [InlineData(0.707106781f, 0.012341342f, CrossPlatformMachineEpsilon)] // value: (1 / sqrt(2)) + [InlineData(0.785398163f, 0.013707785f, CrossPlatformMachineEpsilon)] // value: (pi / 4) + [InlineData(1.0f, 0.017453292f, CrossPlatformMachineEpsilon)] + [InlineData(1.12837917f, 0.019693933f, CrossPlatformMachineEpsilon)] // value: (2 / sqrt(pi)) + [InlineData(1.41421356f, 0.024682684f, CrossPlatformMachineEpsilon)] // value: (sqrt(2)) + [InlineData(1.44269504f, 0.025179777f, CrossPlatformMachineEpsilon)] // value: (log2(e)) + [InlineData(1.5f, 0.02617994f, CrossPlatformMachineEpsilon)] + [InlineData(1.57079633f, 0.02741557f, CrossPlatformMachineEpsilon)] // value: (pi / 2) + [InlineData(2.0f, 0.034906585f, CrossPlatformMachineEpsilon)] + [InlineData(2.30258509f, 0.040187694f, CrossPlatformMachineEpsilon)] // value: (ln(10)) + [InlineData(2.5f, 0.043633234f, CrossPlatformMachineEpsilon)] + [InlineData(2.71828183f, 0.047442965f, CrossPlatformMachineEpsilon)] // value: (e) + [InlineData(3.0f, 0.05235988f, CrossPlatformMachineEpsilon)] + [InlineData(3.14159265f, 0.05483114f, CrossPlatformMachineEpsilon)] // value: (pi) + [InlineData(3.5f, 0.061086528f, CrossPlatformMachineEpsilon)] + [InlineData(float.PositiveInfinity, float.PositiveInfinity, 0.0f)] + public static void DegreesToRadiansTest(float value, float expectedResult, float allowedVariance) + { + AssertExtensions.Equal(-expectedResult, float.DegreesToRadians(-value), allowedVariance); + AssertExtensions.Equal(+expectedResult, float.DegreesToRadians(+value), allowedVariance); + } + + [Theory] + [InlineData(float.NaN, float.NaN, 0.0)] + [InlineData(0.0f, 0.0f, 0.0)] + [InlineData(0.0055555557f, 0.318309886f, CrossPlatformMachineEpsilon)] // expected: (1 / pi) + [InlineData(0.007579869f, 0.434294482f, CrossPlatformMachineEpsilon)] // expected: (log10(e)) + [InlineData(0.008726646f, 0.5f, CrossPlatformMachineEpsilon)] + [InlineData(0.011111111f, 0.636619772f, CrossPlatformMachineEpsilon)] // expected: (2 / pi) + [InlineData(0.0120977005f, 0.693147181f, CrossPlatformMachineEpsilon)] // expected: (ln(2)) + [InlineData(0.012341342f, 0.707106781f, CrossPlatformMachineEpsilon)] // expected: (1 / sqrt(2)) + [InlineData(0.013707785f, 0.785398163f, CrossPlatformMachineEpsilon)] // expected: (pi / 4) + [InlineData(0.017453292f, 1.0f, CrossPlatformMachineEpsilon)] + [InlineData(0.019693933f, 1.12837917f, CrossPlatformMachineEpsilon)] // expected: (2 / sqrt(pi)) + [InlineData(0.024682684f, 1.41421356f, CrossPlatformMachineEpsilon)] // expected: (sqrt(2)) + [InlineData(0.025179777f, 1.44269504f, CrossPlatformMachineEpsilon)] // expected: (log2(e)) + [InlineData(0.02617994f, 1.5f, CrossPlatformMachineEpsilon)] + [InlineData(0.02741557f, 1.57079633f, CrossPlatformMachineEpsilon)] // expected: (pi / 2) + [InlineData(0.034906585f, 2.0f, CrossPlatformMachineEpsilon)] + [InlineData(0.040187694f, 2.30258509f, CrossPlatformMachineEpsilon)] // expected: (ln(10)) + [InlineData(0.043633234f, 2.5f, CrossPlatformMachineEpsilon)] + [InlineData(0.047442965f, 2.71828183f, CrossPlatformMachineEpsilon)] // expected: (e) + [InlineData(0.05235988f, 3.0f, CrossPlatformMachineEpsilon)] + [InlineData(0.05483114f, 3.14159265f, CrossPlatformMachineEpsilon)] // expected: (pi) + [InlineData(0.061086528f, 3.5f, CrossPlatformMachineEpsilon)] + [InlineData(float.PositiveInfinity, float.PositiveInfinity, 0.0)] + public static void RadiansToDegreesTest(float value, float expectedResult, float allowedVariance) + { + AssertExtensions.Equal(-expectedResult, float.RadiansToDegrees(-value), allowedVariance); + AssertExtensions.Equal(+expectedResult, float.RadiansToDegrees(+value), allowedVariance); + } } } From 5cf47deaf73a14035806901265167cc63d9cf0a8 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Thu, 13 Jul 2023 15:59:11 -0700 Subject: [PATCH 2/4] Adding some comments elaborating on the Degrees/Radians conversion APIs --- .../src/System/Double.cs | 6 +++ .../System.Private.CoreLib/src/System/Half.cs | 16 +++++- .../Numerics/ITrigonometricFunctions.cs | 51 +++++++++++++++++++ .../System/Runtime/InteropServices/NFloat.cs | 16 +++++- .../src/System/Single.cs | 6 +++ 5 files changed, 91 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Double.cs b/src/libraries/System.Private.CoreLib/src/System/Double.cs index 962a837fec00a..3731aaf995987 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Double.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Double.cs @@ -1874,12 +1874,18 @@ public static double CosPi(double x) /// public static double DegreesToRadians(double degrees) { + // NOTE: Don't change the algorithm without consulting the DIM + // which elaborates on why this implementation was chosen + return (degrees * Pi) / 180.0; } /// public static double RadiansToDegrees(double radians) { + // NOTE: Don't change the algorithm without consulting the DIM + // which elaborates on why this implementation was chosen + return (radians * 180.0) / Pi; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Half.cs b/src/libraries/System.Private.CoreLib/src/System/Half.cs index 255569672fcbe..d8dd5dcdb96f8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Half.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Half.cs @@ -2147,10 +2147,22 @@ private static bool TryConvertTo(Half value, [MaybeNullWhen(false)] out public static Half CosPi(Half x) => (Half)float.CosPi((float)x); /// - public static Half DegreesToRadians(Half degrees) => (Half)float.DegreesToRadians((float)degrees); + public static Half DegreesToRadians(Half degrees) + { + // NOTE: Don't change the algorithm without consulting the DIM + // which elaborates on why this implementation was chosen + + return (Half)float.DegreesToRadians((float)degrees); + } /// - public static Half RadiansToDegrees(Half radians) => (Half)float.RadiansToDegrees((float)radians); + public static Half RadiansToDegrees(Half radians) + { + // NOTE: Don't change the algorithm without consulting the DIM + // which elaborates on why this implementation was chosen + + return (Half)float.RadiansToDegrees((float)radians); + } /// public static Half Sin(Half x) => (Half)MathF.Sin((float)x); diff --git a/src/libraries/System.Private.CoreLib/src/System/Numerics/ITrigonometricFunctions.cs b/src/libraries/System.Private.CoreLib/src/System/Numerics/ITrigonometricFunctions.cs index 8892e327cdd5e..7b6d250e2f4b5 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Numerics/ITrigonometricFunctions.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Numerics/ITrigonometricFunctions.cs @@ -62,6 +62,54 @@ public interface ITrigonometricFunctions /// The value of converted to radians. static virtual TSelf DegreesToRadians(TSelf degrees) { + // We don't want to simplify this to: degrees * (Pi / 180) + // + // Doing so will change the result and in many cases will + // cause a loss of precision. The main exception to this + // is when the initial multiplication causes overflow, but + // if we decide that should be handled in the future it needs + // to be more explicit around how its done. + // + // Floating-point operations are natural imprecise due to + // rounding required to fit the "infinitely-precise result" + // into the limits of the underlying representation. Because + // of this, every operation can introduce some amount of rounding + // error. + // + // For integers, the IEEE 754 binary floating-point types can + // exactly represent them up to the 2^n, where n is the number + // of bits in the significand. This is 10 for Half, 23 for Single, + // and 52 for Double. As you approach this limit, the number of + // digits available to represent the fractional portion decreases. + // + // For Half, you get around 3.311 total decimal digits of precision. + // For Single, this is around 7.225 and around 15.955 for Double. + // + // The actual number of digits can be slightly more or less, depending. + // + // This means that values such as `Pi` are not exactly `Pi`, instead: + // * Half: 3.14 0625 + // * Single: 3.14 1592 741012573 2421875 + // * Double: 3.14 1592 653589793 115997963468544185161590576171875 + // * Actual: 3.14 1592 653589793 2384626433832795028841971693993751058209749445923... + // + // If we were to simplify this to simply multiply by (Pi / 180), we get: + // * Half: 0.01745 6054 6875 + // * Single: 0.01745 3292 384743690 49072265625 + // * Double: 0.01745 3292 519943295 4743716805978692718781530857086181640625 + // * Actual: 0.01745 3292 519943295 7692369076848861271344287188854172545609719144... + // + // Neither of these end up "perfect". There will be some cases where they will trade + // in terms of closeness to the "infinitely precise result". Over the entire domain + // however, doing the separate multiplications tends to produce overall more accurate + // results. It helps ensure the implementation can be trivial for the DIM case, and + // covers the vast majority of typical inputs more efficiently largely only pessimizing + // the case where the first multiplication results in overflow. + // + // This is particularly true for `RadiansToDegrees` where 180 is exactly representable + // and so allows an exactly representable intermediate value to be computed when overflow + // doesn't occur. + return (degrees * TSelf.Pi) / TSelf.CreateChecked(180); } @@ -70,6 +118,9 @@ static virtual TSelf DegreesToRadians(TSelf degrees) /// The value of converted to degrees. static virtual TSelf RadiansToDegrees(TSelf radians) { + // We don't want to simplify this to: radians * (180 / Pi) + // See DegreesToRadians for a longer explanation as to why + return (radians * TSelf.CreateChecked(180)) / TSelf.Pi; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/NFloat.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/NFloat.cs index f6efe3aff1bfa..3f3312b2c46b6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/NFloat.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/NFloat.cs @@ -1852,10 +1852,22 @@ private static bool TryConvertTo(NFloat value, [MaybeNullWhen(false)] ou public static NFloat CosPi(NFloat x) => new NFloat(NativeType.CosPi(x._value)); /// - public static NFloat DegreesToRadians(NFloat degrees) => new NFloat(NativeType.DegreesToRadians(degrees._value)); + public static NFloat DegreesToRadians(NFloat degrees) + { + // NOTE: Don't change the algorithm without consulting the DIM + // which elaborates on why this implementation was chosen + + return new NFloat(NativeType.DegreesToRadians(degrees._value)); + } /// - public static NFloat RadiansToDegrees(NFloat radians) => new NFloat(NativeType.RadiansToDegrees(radians._value)); + public static NFloat RadiansToDegrees(NFloat radians) + { + // NOTE: Don't change the algorithm without consulting the DIM + // which elaborates on why this implementation was chosen + + return new NFloat(NativeType.RadiansToDegrees(radians._value)); + } /// public static NFloat Sin(NFloat x) => new NFloat(NativeType.Sin(x._value)); diff --git a/src/libraries/System.Private.CoreLib/src/System/Single.cs b/src/libraries/System.Private.CoreLib/src/System/Single.cs index 214652374a756..4d6b873e4285f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Single.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Single.cs @@ -1755,12 +1755,18 @@ public static float CosPi(float x) /// public static float DegreesToRadians(float degrees) { + // NOTE: Don't change the algorithm without consulting the DIM + // which elaborates on why this implementation was chosen + return (degrees * Pi) / 180.0f; } /// public static float RadiansToDegrees(float radians) { + // NOTE: Don't change the algorithm without consulting the DIM + // which elaborates on why this implementation was chosen + return (radians * 180.0f) / Pi; } From ea116b8df6bd2b877ebc9a36a392fd3f19ae9db9 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Thu, 13 Jul 2023 17:29:47 -0700 Subject: [PATCH 3/4] Update src/libraries/System.Private.CoreLib/src/System/Numerics/ITrigonometricFunctions.cs Co-authored-by: Joe4evr --- .../src/System/Numerics/ITrigonometricFunctions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Numerics/ITrigonometricFunctions.cs b/src/libraries/System.Private.CoreLib/src/System/Numerics/ITrigonometricFunctions.cs index 7b6d250e2f4b5..eedb795255154 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Numerics/ITrigonometricFunctions.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Numerics/ITrigonometricFunctions.cs @@ -70,7 +70,7 @@ static virtual TSelf DegreesToRadians(TSelf degrees) // if we decide that should be handled in the future it needs // to be more explicit around how its done. // - // Floating-point operations are natural imprecise due to + // Floating-point operations are naturally imprecise due to // rounding required to fit the "infinitely-precise result" // into the limits of the underlying representation. Because // of this, every operation can introduce some amount of rounding From 84df029df128efd8e197e89a91b48289f6044e01 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Thu, 13 Jul 2023 20:48:28 -0700 Subject: [PATCH 4/4] Fix a copy/paste error for the Half degree to radians tests --- src/libraries/System.Runtime/tests/System/HalfTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libraries/System.Runtime/tests/System/HalfTests.cs b/src/libraries/System.Runtime/tests/System/HalfTests.cs index e4212859334c4..e5b68dfd0bc0d 100644 --- a/src/libraries/System.Runtime/tests/System/HalfTests.cs +++ b/src/libraries/System.Runtime/tests/System/HalfTests.cs @@ -2146,7 +2146,6 @@ public static void LerpTest(Half value1, Half value2, Half amount, Half expected public static IEnumerable DegreesToRadians_TestData() { - yield return new object[] { Half.PositiveInfinity, Half.NaN, Half.Zero }; yield return new object[] { Half.NaN, Half.NaN, Half.Zero }; yield return new object[] { Half.Zero, Half.Zero, Half.Zero }; yield return new object[] { (Half)(0.3184f), (Half)(0.005554f), CrossPlatformMachineEpsilon }; // value: (1 / pi) @@ -2182,7 +2181,6 @@ public static void DegreesToRadiansTest(Half value, Half expectedResult, Half al public static IEnumerable RadiansToDegrees_TestData() { - yield return new object[] { Half.NaN, Half.PositiveInfinity, Half.Zero }; yield return new object[] { Half.NaN, Half.NaN, Half.Zero }; yield return new object[] { Half.Zero, Half.Zero, Half.Zero }; yield return new object[] { (Half)(0.005554f), (Half)(0.3184f), CrossPlatformMachineEpsilon }; // value: (1 / pi)