diff --git a/src/_common/Math/NullMath.cs b/src/_common/Math/NullMath.cs index f82656be5..7004f0408 100644 --- a/src/_common/Math/NullMath.cs +++ b/src/_common/Math/NullMath.cs @@ -1,7 +1,9 @@ +using System.Runtime.CompilerServices; + namespace Skender.Stock.Indicators; /// -/// Nullable System. functions. +/// Nullable System. functions. /// /// /// System.Math infamously does not allow @@ -16,10 +18,13 @@ public static class NullMath /// /// The nullable double value. /// The absolute value, or null if the input is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double? Abs(this double? value) - => value is null - ? null - : value < 0 ? (double)-value : (double)value; + => value.HasValue + ? (value.GetValueOrDefault() < 0 + ? -value.GetValueOrDefault() + : value) + : null; /// /// Rounds a nullable decimal value to a specified number of fractional digits. @@ -27,10 +32,11 @@ public static class NullMath /// The nullable decimal value. /// The number of fractional digits. /// The rounded value, or null if the input is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static decimal? Round(this decimal? value, int digits) - => value is null - ? null - : Math.Round((decimal)value, digits); + => value.HasValue + ? Math.Round(value.GetValueOrDefault(), digits) + : null; /// /// Rounds a nullable double value to a specified number of fractional digits. @@ -38,26 +44,31 @@ public static class NullMath /// The nullable double value. /// The number of fractional digits. /// The rounded value, or null if the input is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double? Round(this double? value, int digits) - => value is null - ? null - : Math.Round((double)value, digits); + => value.HasValue + ? Math.Round(value.GetValueOrDefault(), digits) + : null; /// /// Rounds a double value to a specified number of fractional digits. + /// It is an extension alias of /// /// The double value. /// The number of fractional digits. /// The rounded value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double Round(this double value, int digits) => Math.Round(value, digits); /// /// Rounds a decimal value to a specified number of fractional digits. + /// It is an extension alias of /// /// The decimal value. /// The number of fractional digits. /// The rounded value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static decimal Round(this decimal value, int digits) => Math.Round(value, digits); @@ -66,26 +77,29 @@ public static decimal Round(this decimal value, int digits) /// /// The nullable double value. /// The value, or NaN if the input is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double Null2NaN(this double? value) - => value ?? double.NaN; + => value.GetValueOrDefault(double.NaN); /// /// Converts a nullable decimal value to NaN if it is null. /// /// The nullable decimal value. /// The value as a double, or NaN if the input is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double Null2NaN(this decimal? value) - => value is null - ? double.NaN - : (double)value; + => value.HasValue + ? (double)value.GetValueOrDefault() + : double.NaN; /// /// Converts a nullable double value to null if it is NaN. /// /// The nullable double value. /// The value, or null if the input is NaN. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double? NaN2Null(this double? value) - => value is double.NaN + => value.HasValue && double.IsNaN(value.GetValueOrDefault()) ? null : value; @@ -94,6 +108,7 @@ public static double Null2NaN(this decimal? value) /// /// The double value. /// The value, or null if the input is NaN. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double? NaN2Null(this double value) => double.IsNaN(value) ? null diff --git a/tests/indicators/_common/Math/NullMath.Tests.cs b/tests/indicators/_common/Math/NullMath.Tests.cs new file mode 100644 index 000000000..86b72ab03 --- /dev/null +++ b/tests/indicators/_common/Math/NullMath.Tests.cs @@ -0,0 +1,72 @@ +namespace Utilities; + +#pragma warning disable CA1805 // Do not initialize unnecessarily + +[TestClass] +public class NullMathTests : TestBase +{ + private readonly double? dblPos = 100.12345; + private readonly double? dblNeg = -200.98765; + private readonly double? dblNul = null; + private readonly decimal? decPos = 10.12345m; + private readonly decimal? decNeg = -20.98765m; + private readonly decimal? decNul = null; + + [TestMethod] + public void AbsDouble() + { + dblPos.Abs().Should().Be(100.12345d); + dblNeg.Abs().Should().Be(200.98765d); + dblNul.Abs().Should().BeNull(); + } + + [TestMethod] + public void RoundDecimal() + { + decPos.Round(2).Should().Be(10.12m); + decNeg.Round(2).Should().Be(-20.99m); + decNul.Round(2).Should().BeNull(); + + 10.12345m.Round(2).Should().Be(10.12m); + } + + [TestMethod] + public void RoundDouble() + { + dblPos.Round(2).Should().Be(100.12d); + dblNeg.Round(2).Should().Be(-200.99d); + dblNul.Round(2).Should().BeNull(); + + 100.12345d.Round(2).Should().Be(100.12d); + } + + [TestMethod] + public void Null2NaN() + { + // doubles + dblPos.Null2NaN().Should().Be(100.12345d); + dblNeg.Null2NaN().Should().Be(-200.98765d); + dblNul.Null2NaN().Should().Be(double.NaN); + + // decimals ยป doubles + decPos.Null2NaN().Should().Be(10.12345d); + decNeg.Null2NaN().Should().Be(-20.98765d); + decNul.Null2NaN().Should().Be(double.NaN); + } + + [TestMethod] + public void NaN2Null() + { + // double (nullable) + double? dblNaNul = double.NaN; + dblNaNul.NaN2Null().Should().BeNull(); + dblPos.NaN2Null().Should().Be(100.12345d); + dblNeg.NaN2Null().Should().Be(-200.98765d); + + // double (non-nullable) + double dblNaN = double.NaN; + dblNaN.NaN2Null().Should().BeNull(); + 100.12345d.NaN2Null().Should().Be(100.12345d); + (-200.98765d).NaN2Null().Should().Be(-200.98765d); + } +} diff --git a/tests/performance/Perf.Utility.NullMath.cs b/tests/performance/Perf.Utility.NullMath.cs new file mode 100644 index 000000000..2096d1af3 --- /dev/null +++ b/tests/performance/Perf.Utility.NullMath.cs @@ -0,0 +1,64 @@ +namespace Tests.Performance; + +#pragma warning disable CA1805 // Do not initialize unnecessarily + +[ShortRunJob] +public class UtilityNullMath +{ + private static readonly double? dblVal = 54321.0123456789d; + private static readonly double? dblNul = null; + private static readonly decimal? decVal = 54321.0123456789m; + private static readonly decimal? decNul = null; + private static readonly double? nulNaN = double.NaN; + private const double dblNaN = double.NaN; + + // Abs() + + [Benchmark] + public double? AbsDblVal() => dblVal.Abs(); + + [Benchmark] + public double? AbsDblNul() => dblNul.Abs(); + + // Round() + + [Benchmark] + public decimal? RoundDecVal() => decVal.Round(2); + + [Benchmark] + public decimal? RoundDecNul() => decNul.Round(2); + + [Benchmark] + public double? RoundDblVal() => dblVal.Round(2); + + [Benchmark] + public double? RoundDblNul() => dblNul.Round(2); + + // Null2NaN() + + [Benchmark] + public double Null2NaNDecVal() => decVal.Null2NaN(); + + [Benchmark] + public double Null2NaNDecNul() => decNul.Null2NaN(); + + [Benchmark] + public double Null2NaNDblVal() => dblVal.Null2NaN(); + + [Benchmark] + public double Null2NaNDblNul() => dblNul.Null2NaN(); + + // Nan2Null() + + [Benchmark] + public double? NaN2NullDblVal() => dblVal.NaN2Null(); + + [Benchmark] + public double? NaN2NullDblNul() => dblNul.NaN2Null(); + + [Benchmark] + public double? NaN2NullNaNVal() => dblNaN.NaN2Null(); + + [Benchmark] + public double? NaN2NullNanNul() => nulNaN.NaN2Null(); +} diff --git a/tests/performance/Perf.Utility.Maths.cs b/tests/performance/Perf.Utility.StdDev.cs similarity index 94% rename from tests/performance/Perf.Utility.Maths.cs rename to tests/performance/Perf.Utility.StdDev.cs index a8f4acab7..09e763653 100644 --- a/tests/performance/Perf.Utility.Maths.cs +++ b/tests/performance/Perf.Utility.StdDev.cs @@ -3,7 +3,7 @@ namespace Performance; // INTERNAL UTILITIES [ShortRunJob] -public class UtilityMaths +public class UtilityStdDev { [Params(20, 50, 250, 1000)] public int Periods;