From 531ae8ceb3da0051413679708efdbc6b1fef58db Mon Sep 17 00:00:00 2001 From: Dave Skender <8432125+DaveSkender@users.noreply.github.com> Date: Mon, 11 Nov 2024 10:18:35 -0500 Subject: [PATCH] refactor: Optimize NullMath utilities (#1272) --- src/_common/Math/NullMath.cs | 104 +++++++++++++++--- .../indicators/_common/Math/NullMath.Tests.cs | 72 ++++++++++++ tests/performance/GlobalSuppressions.cs | 17 +-- tests/performance/Perf.Utility.NullMath.cs | 64 +++++++++++ ...rf.Internals.cs => Perf.Utility.StdDev.cs} | 10 +- 5 files changed, 232 insertions(+), 35 deletions(-) create mode 100644 tests/indicators/_common/Math/NullMath.Tests.cs create mode 100644 tests/performance/Perf.Utility.NullMath.cs rename tests/performance/{Perf.Internals.cs => Perf.Utility.StdDev.cs} (61%) diff --git a/src/_common/Math/NullMath.cs b/src/_common/Math/NullMath.cs index 71aad8c1c..7004f0408 100644 --- a/src/_common/Math/NullMath.cs +++ b/src/_common/Math/NullMath.cs @@ -1,40 +1,114 @@ +using System.Runtime.CompilerServices; + namespace Skender.Stock.Indicators; -// NULLABLE SYSTEM.MATH -// System.Math does not allow or handle null input values. -// Instead of putting a lot of inline defensive code -// we're building nullable equivalents here. +/// +/// Nullable System. functions. +/// +/// +/// System.Math infamously does not allow +/// or handle nullable input values. +/// Instead of adding repetitive inline defensive code, +/// we're using these equivalents. Most are simple wrappers. +/// public static class NullMath { + /// + /// Returns the absolute value of a nullable double. + /// + /// 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. + /// + /// 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. + /// + /// 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); + /// + /// Converts a nullable double value to NaN if it is null. + /// + /// 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.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 not null and double.NaN) + => value.HasValue && double.IsNaN(value.GetValueOrDefault()) ? null : value; + /// + /// Converts a double value to null if it is NaN. + /// + /// 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/GlobalSuppressions.cs b/tests/performance/GlobalSuppressions.cs index 0d913b5dc..a4a01ae24 100644 --- a/tests/performance/GlobalSuppressions.cs +++ b/tests/performance/GlobalSuppressions.cs @@ -1,8 +1,3 @@ -// This file is used by Code Analysis to maintain SuppressMessage -// attributes that are applied to this project. -// Project-level suppressions either have no target or are given -// a specific target and scoped to a namespace, type, member, etc. - using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage( @@ -11,14 +6,6 @@ Justification = "Required for BenchmarkDotNet")] [assembly: SuppressMessage( - "StyleCop.CSharp.MaintainabilityRules", - "SA1401:Fields should be private", - Justification = "Required for BenchmarkDotNet", - Scope = "member", - Target = "~F:Tests.Performance.InternalsPerformance.Periods")] - -[assembly: SuppressMessage("Design", + "Design", "CA1051:Do not declare visible instance fields", - Justification = "Required for BenchmarkDotNet", - Scope = "member", - Target = "~F:Tests.Performance.InternalsPerformance.Periods")] + Justification = "Required for BenchmarkDotNet")] 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.Internals.cs b/tests/performance/Perf.Utility.StdDev.cs similarity index 61% rename from tests/performance/Perf.Internals.cs rename to tests/performance/Perf.Utility.StdDev.cs index 461dbab29..71a0d5753 100644 --- a/tests/performance/Perf.Internals.cs +++ b/tests/performance/Perf.Utility.StdDev.cs @@ -1,22 +1,22 @@ namespace Tests.Performance; -// INTERNAL FUNCTIONS +// INTERNAL UTILITIES [ShortRunJob] -public class InternalsPerformance +public class UtilityStdDev { [Params(20, 50, 250, 1000)] public int Periods; - private double[] values; + private double[] _values; // standard deviation [GlobalSetup(Targets = [nameof(StdDev)])] public void Setup() - => values = TestData.GetLongish(Periods) + => _values = TestData.GetLongish(Periods) .Select(x => (double)x.Close) .ToArray(); [Benchmark] - public object StdDev() => values.StdDev(); + public object StdDev() => _values.StdDev(); }