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();
}