Skip to content

Commit

Permalink
refactor: Optimize NullMath utilities (#1272)
Browse files Browse the repository at this point in the history
  • Loading branch information
DaveSkender authored Nov 11, 2024
1 parent 554b640 commit 531ae8c
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 35 deletions.
104 changes: 89 additions & 15 deletions src/_common/Math/NullMath.cs
Original file line number Diff line number Diff line change
@@ -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.
/// <summary>
/// Nullable <c>System.<see cref="Math"/></c> functions.
/// </summary>
/// <remarks>
/// <c>System.Math</c> 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.
/// </remarks>
public static class NullMath
{
/// <summary>
/// Returns the absolute value of a nullable double.
/// </summary>
/// <param name="value">The nullable double value.</param>
/// <returns>The absolute value, or null if the input is null.</returns>
[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;

/// <summary>
/// Rounds a nullable decimal value to a specified number of fractional digits.
/// </summary>
/// <param name="value">The nullable decimal value.</param>
/// <param name="digits">The number of fractional digits.</param>
/// <returns>The rounded value, or null if the input is null.</returns>
[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;

/// <summary>
/// Rounds a nullable double value to a specified number of fractional digits.
/// </summary>
/// <param name="value">The nullable double value.</param>
/// <param name="digits">The number of fractional digits.</param>
/// <returns>The rounded value, or null if the input is null.</returns>
[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;

/// <summary>
/// Rounds a double value to a specified number of fractional digits.
/// It is an extension alias of <see cref="Math.Round(double, int)"/>
/// </summary>
/// <param name="value">The double value.</param>
/// <param name="digits">The number of fractional digits.</param>
/// <returns>The rounded value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double Round(this double value, int digits)
=> Math.Round(value, digits);

/// <summary>
/// Rounds a decimal value to a specified number of fractional digits.
/// It is an extension alias of <see cref="Math.Round(decimal, int)"/>
/// </summary>
/// <param name="value">The decimal value.</param>
/// <param name="digits">The number of fractional digits.</param>
/// <returns>The rounded value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static decimal Round(this decimal value, int digits)
=> Math.Round(value, digits);

/// <summary>
/// Converts a nullable double value to NaN if it is null.
/// </summary>
/// <param name="value">The nullable double value.</param>
/// <returns>The value, or NaN if the input is null.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double Null2NaN(this double? value)
=> value ?? double.NaN;
=> value.GetValueOrDefault(double.NaN);

/// <summary>
/// Converts a nullable decimal value to NaN if it is null.
/// </summary>
/// <param name="value">The nullable decimal value.</param>
/// <returns>The value as a double, or NaN if the input is null.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double Null2NaN(this decimal? value)
=> value.HasValue
? (double)value.GetValueOrDefault()
: double.NaN;

/// <summary>
/// Converts a nullable double value to null if it is NaN.
/// </summary>
/// <param name="value">The nullable double value.</param>
/// <returns>The value, or null if the input is NaN.</returns>
[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;

/// <summary>
/// Converts a double value to null if it is NaN.
/// </summary>
/// <param name="value">The double value.</param>
/// <returns>The value, or null if the input is NaN.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double? NaN2Null(this double value)
=> double.IsNaN(value)
? null
Expand Down
72 changes: 72 additions & 0 deletions tests/indicators/_common/Math/NullMath.Tests.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
17 changes: 2 additions & 15 deletions tests/performance/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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")]
64 changes: 64 additions & 0 deletions tests/performance/Perf.Utility.NullMath.cs
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -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();
}

0 comments on commit 531ae8c

Please sign in to comment.