mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 20:48:04 +00:00
Refactor tests and implementations for various indicators
- Updated RsiIndicatorTests to ensure proper initialization and state checks. - Added new tests for Rsx, Vel, and Adosc indicators to validate behavior under iterative corrections and edge cases (NaN, Infinity). - Enhanced Bessel indicator tests and implementation with consistent formatting. - Improved Ema and Pwma implementations by ensuring proper handling of values. - Introduced mock classes for charting to facilitate testing without dependencies. - Ensured all indicators produce consistent results across different modes of operation. - Cleaned up code formatting and added missing commas for better readability.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
@@ -36,6 +37,7 @@ public abstract class AbstractBase : ITValuePublisher
|
||||
/// <summary>
|
||||
/// Helper to invoke the Pub event.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected void PubEvent(TValue value, bool isNew = true)
|
||||
{
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew });
|
||||
|
||||
@@ -719,4 +719,175 @@ public class RingBufferTests
|
||||
Assert.Equal(21.666666666666668, buffer.Average, 1e-10);
|
||||
Assert.NotEqual(avgBeforeCorrection, buffer.Average);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Snapshot_CapturesCurrentState()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
buffer.Snapshot();
|
||||
|
||||
// Modify buffer after snapshot
|
||||
buffer.Add(40.0);
|
||||
|
||||
Assert.Equal(4, buffer.Count);
|
||||
Assert.Equal(100.0, buffer.Sum); // 10 + 20 + 30 + 40
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Restore_ReturnsToSnapshotState()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
buffer.Snapshot();
|
||||
double sumBeforeModification = buffer.Sum;
|
||||
int countBeforeModification = buffer.Count;
|
||||
|
||||
// Modify buffer after snapshot
|
||||
buffer.Add(40.0);
|
||||
Assert.Equal(4, buffer.Count);
|
||||
|
||||
// Restore to snapshot state
|
||||
buffer.Restore();
|
||||
|
||||
Assert.Equal(countBeforeModification, buffer.Count);
|
||||
Assert.Equal(sumBeforeModification, buffer.Sum);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Snapshot_Restore_WithWrapping()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
buffer.Snapshot();
|
||||
|
||||
// Add value that causes wrap
|
||||
buffer.Add(40.0);
|
||||
Assert.Equal(90.0, buffer.Sum); // 20 + 30 + 40
|
||||
|
||||
buffer.Restore();
|
||||
|
||||
Assert.Equal(60.0, buffer.Sum); // 10 + 20 + 30
|
||||
Assert.Equal(30.0, buffer.Newest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecalculateSum_CorrectsDrift()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
double recalculated = buffer.RecalculateSum();
|
||||
|
||||
Assert.Equal(60.0, recalculated);
|
||||
Assert.Equal(60.0, buffer.Sum);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecalculateSum_AfterMultipleOperations()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
|
||||
// Simulate many operations that could accumulate floating-point drift
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
buffer.Add(i * 0.1);
|
||||
}
|
||||
|
||||
double recalculated = buffer.RecalculateSum();
|
||||
|
||||
// Should be equal (or very close) since we're using exact values
|
||||
Assert.Equal(recalculated, buffer.Sum);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StartIndex_EmptyBuffer_ReturnsZero()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
Assert.Equal(0, buffer.StartIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StartIndex_PartiallyFilled_ReturnsZero()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
|
||||
Assert.Equal(0, buffer.StartIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StartIndex_FullBuffer_ReturnsHead()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
buffer.Add(40.0); // Wraps
|
||||
|
||||
// StartIndex should point to oldest element
|
||||
Assert.True(buffer.StartIndex >= 0 && buffer.StartIndex < buffer.Capacity);
|
||||
Assert.Equal(20.0, buffer.Oldest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_NegativeIndexViaFromEnd_ThrowsWhenOutOfBounds()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
// ^4 when count=3 should throw
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => _ = buffer[^4]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CopyTo_InsufficientDestinationBuffer_Behavior()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
var dest = new double[2]; // Too small
|
||||
|
||||
// This will throw IndexOutOfRangeException since we're copying 3 elements to size-2 array
|
||||
Assert.Throws<ArgumentException>(() => buffer.CopyTo(dest, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CopyTo_StartIndexOutOfRange_Behavior()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
|
||||
var dest = new double[5];
|
||||
|
||||
// Starting at index 4 with 2 elements should fail
|
||||
Assert.Throws<ArgumentException>(() => buffer.CopyTo(dest, 4));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -805,4 +805,191 @@ public class SimdScalarFallbackTests
|
||||
Assert.Equal(42.5, min);
|
||||
Assert.Equal(42.5, max);
|
||||
}
|
||||
|
||||
// Additional edge case tests
|
||||
[Fact]
|
||||
public void DotProduct_ContainsNaN_PropagatesNaN()
|
||||
{
|
||||
double[] a = [1.0, double.NaN, 3.0];
|
||||
double[] b = [4.0, 5.0, 6.0];
|
||||
double result = SimdExtensions.DotProduct(a, b);
|
||||
Assert.True(double.IsNaN(result));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DotProduct_ContainsInfinity_PropagatesCorrectly()
|
||||
{
|
||||
double[] a = [1.0, double.PositiveInfinity, 3.0];
|
||||
double[] b = [4.0, 5.0, 6.0];
|
||||
double result = SimdExtensions.DotProduct(a, b);
|
||||
Assert.True(double.IsPositiveInfinity(result));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_ContainsNaN_PropagatesNaN()
|
||||
{
|
||||
double[] left = [1.0, double.NaN, 3.0];
|
||||
double[] right = [4.0, 5.0, 6.0];
|
||||
double[] result = new double[3];
|
||||
|
||||
SimdExtensions.Add(left, right, result);
|
||||
|
||||
Assert.Equal(5.0, result[0]);
|
||||
Assert.True(double.IsNaN(result[1]));
|
||||
Assert.Equal(9.0, result[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Subtract_ContainsNaN_PropagatesNaN()
|
||||
{
|
||||
double[] left = [10.0, double.NaN, 30.0];
|
||||
double[] right = [1.0, 2.0, 3.0];
|
||||
double[] result = new double[3];
|
||||
|
||||
SimdExtensions.Subtract(left, right, result);
|
||||
|
||||
Assert.Equal(9.0, result[0]);
|
||||
Assert.True(double.IsNaN(result[1]));
|
||||
Assert.Equal(27.0, result[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsNonFinite_NegativeInfinityAtStart_ReturnsTrue()
|
||||
{
|
||||
double[] data = [double.NegativeInfinity, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(span.ContainsNonFinite());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsNonFinite_NegativeInfinityAtEnd_ReturnsTrue()
|
||||
{
|
||||
double[] data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, double.NegativeInfinity];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(span.ContainsNonFinite());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceSIMD_SingleElement_ReturnsNaN()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(double.IsNaN(span.VarianceSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDevSIMD_SingleElement_ReturnsNaN()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(double.IsNaN(span.StdDevSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDevSIMD_EmptySpan_ReturnsNaN()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.True(double.IsNaN(span.StdDevSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumSIMD_SingleElement_ReturnsElement()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(42.5, span.SumSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AverageSIMD_SingleElement_ReturnsElement()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(42.5, span.AverageSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DotProduct_SingleElement_ReturnsProduct()
|
||||
{
|
||||
double[] a = [3.0];
|
||||
double[] b = [4.0];
|
||||
Assert.Equal(12.0, SimdExtensions.DotProduct(a, b));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DotProduct_TwoElements_ReturnsCorrect()
|
||||
{
|
||||
double[] a = [2.0, 3.0];
|
||||
double[] b = [4.0, 5.0];
|
||||
// 2*4 + 3*5 = 8 + 15 = 23
|
||||
Assert.Equal(23.0, SimdExtensions.DotProduct(a, b));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_SingleElement_Works()
|
||||
{
|
||||
double[] left = [5.0];
|
||||
double[] right = [3.0];
|
||||
double[] result = new double[1];
|
||||
|
||||
SimdExtensions.Add(left, right, result);
|
||||
|
||||
Assert.Equal(8.0, result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Subtract_SingleElement_Works()
|
||||
{
|
||||
double[] left = [5.0];
|
||||
double[] right = [3.0];
|
||||
double[] result = new double[1];
|
||||
|
||||
SimdExtensions.Subtract(left, right, result);
|
||||
|
||||
Assert.Equal(2.0, result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_EmptyArrays_Works()
|
||||
{
|
||||
double[] left = [];
|
||||
double[] right = [];
|
||||
double[] result = [];
|
||||
|
||||
SimdExtensions.Add(left, right, result); // Should not throw
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Subtract_EmptyArrays_Works()
|
||||
{
|
||||
double[] left = [];
|
||||
double[] right = [];
|
||||
double[] result = [];
|
||||
|
||||
SimdExtensions.Subtract(left, right, result); // Should not throw
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_ResultTooSmall_ThrowsArgumentException()
|
||||
{
|
||||
double[] left = [1.0, 2.0, 3.0];
|
||||
double[] right = [4.0, 5.0, 6.0];
|
||||
double[] result = new double[2]; // Too small
|
||||
|
||||
Assert.Throws<ArgumentException>(() => SimdExtensions.Add(left, right, result));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Subtract_ResultTooSmall_ThrowsArgumentException()
|
||||
{
|
||||
double[] left = [1.0, 2.0, 3.0];
|
||||
double[] right = [4.0, 5.0, 6.0];
|
||||
double[] result = new double[2]; // Too small
|
||||
|
||||
Assert.Throws<ArgumentException>(() => SimdExtensions.Subtract(left, right, result));
|
||||
}
|
||||
}
|
||||
|
||||
+152
-1
@@ -345,5 +345,156 @@ public class TBarTests
|
||||
var bar2 = new TBar(12346, 100, 110, 90, 105, 1000);
|
||||
|
||||
Assert.True(bar1 != bar2);
|
||||
}
|
||||
}
|
||||
|
||||
// Additional edge case tests
|
||||
[Fact]
|
||||
public void Constructor_WithLocalDateTime_ConvertsToUtc()
|
||||
{
|
||||
var localDateTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Local);
|
||||
var bar = new TBar(localDateTime, 100, 110, 90, 105, 1000);
|
||||
|
||||
// AsDateTime should return UTC
|
||||
Assert.Equal(DateTimeKind.Utc, bar.AsDateTime.Kind);
|
||||
Assert.Equal(localDateTime.ToUniversalTime().Ticks, bar.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithUnspecifiedDateTime_ConvertsToUtc()
|
||||
{
|
||||
var unspecifiedDateTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Unspecified);
|
||||
var bar = new TBar(unspecifiedDateTime, 100, 110, 90, 105, 1000);
|
||||
|
||||
// Should be converted to UTC
|
||||
Assert.Equal(DateTimeKind.Utc, bar.AsDateTime.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultTBar_HasZeroValues()
|
||||
{
|
||||
var bar = default(TBar);
|
||||
|
||||
Assert.Equal(0, bar.Time);
|
||||
Assert.Equal(0.0, bar.Open);
|
||||
Assert.Equal(0.0, bar.High);
|
||||
Assert.Equal(0.0, bar.Low);
|
||||
Assert.Equal(0.0, bar.Close);
|
||||
Assert.Equal(0.0, bar.Volume);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TBar_WithNaN_HandlesGracefully()
|
||||
{
|
||||
var bar = new TBar(12345, double.NaN, 110, 90, 105, 1000);
|
||||
|
||||
Assert.True(double.IsNaN(bar.Open));
|
||||
Assert.True(double.IsNaN(bar.O.Value));
|
||||
Assert.True(double.IsNaN(bar.OHL3)); // Uses Open
|
||||
Assert.True(double.IsNaN(bar.OC2)); // Uses Open
|
||||
Assert.True(double.IsNaN(bar.OHLC4)); // Uses Open
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TBar_WithInfinity_HandlesGracefully()
|
||||
{
|
||||
var bar = new TBar(12345, 100, double.PositiveInfinity, 90, 105, 1000);
|
||||
|
||||
Assert.True(double.IsPositiveInfinity(bar.High));
|
||||
Assert.True(double.IsPositiveInfinity(bar.H.Value));
|
||||
Assert.True(double.IsPositiveInfinity(bar.HL2)); // Uses High
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TBar_WithMaxValue_HandlesGracefully()
|
||||
{
|
||||
var bar = new TBar(12345, double.MaxValue, double.MaxValue, double.MinValue, 105, 1000);
|
||||
|
||||
Assert.Equal(double.MaxValue, bar.Open);
|
||||
Assert.Equal(double.MaxValue, bar.High);
|
||||
Assert.Equal(double.MinValue, bar.Low);
|
||||
// HL2 calculation with extreme values
|
||||
Assert.True(double.IsFinite(bar.HL2) || double.IsInfinity(bar.HL2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TBar_WithEpsilon_HandlesGracefully()
|
||||
{
|
||||
var bar = new TBar(12345, double.Epsilon, double.Epsilon, double.Epsilon, double.Epsilon, double.Epsilon);
|
||||
|
||||
Assert.Equal(double.Epsilon, bar.Open);
|
||||
Assert.Equal(double.Epsilon, bar.Close);
|
||||
Assert.True(bar.HL2 > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HL2_WithNegativeValues_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, -100, -90, -110, -95, 1000);
|
||||
|
||||
Assert.Equal(-100.0, bar.HL2); // (-90 + -110) / 2
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OHLC4_WithNegativeValues_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, -100, -90, -110, -100, 1000);
|
||||
|
||||
Assert.Equal(-100.0, bar.OHLC4); // (-100 + -90 + -110 + -100) / 4
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImplicitConversion_ToTValue_PreservesTimeAndClose()
|
||||
{
|
||||
long time = 12_345_678_901_234_567;
|
||||
var bar = new TBar(time, 100, 110, 90, 105.5, 1000);
|
||||
|
||||
TValue tv = bar;
|
||||
|
||||
Assert.Equal(time, tv.Time);
|
||||
Assert.Equal(105.5, tv.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_WithNaN_DoesNotThrow()
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, double.NaN, double.NaN, double.NaN, double.NaN, double.NaN);
|
||||
|
||||
string result = bar.ToString();
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("NaN", result, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void O_H_L_C_V_AllHaveSameTime()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
var bar = new TBar(time, 100, 110, 90, 105, 1000);
|
||||
|
||||
Assert.Equal(time, bar.O.Time);
|
||||
Assert.Equal(time, bar.H.Time);
|
||||
Assert.Equal(time, bar.L.Time);
|
||||
Assert.Equal(time, bar.C.Time);
|
||||
Assert.Equal(time, bar.V.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HLCC4_DoubleWeightsClose()
|
||||
{
|
||||
// HLCC4 = (High + Low + Close + Close) / 4
|
||||
var bar = new TBar(0, 100, 120, 80, 100, 1000);
|
||||
|
||||
// (120 + 80 + 100 + 100) / 4 = 400 / 4 = 100
|
||||
Assert.Equal(100.0, bar.HLCC4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OHL3_ExcludesClose()
|
||||
{
|
||||
// OHL3 = (Open + High + Low) / 3
|
||||
var bar = new TBar(0, 90, 120, 60, 999, 1000);
|
||||
|
||||
// (90 + 120 + 60) / 3 = 270 / 3 = 90
|
||||
Assert.Equal(90.0, bar.OHL3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,7 +344,8 @@ public class TBarSeriesTests
|
||||
series.Add(200, 20, 25, 15, 22, 200, isNew: true);
|
||||
|
||||
var list = new List<object>();
|
||||
|
||||
|
||||
#pragma warning disable S4158
|
||||
foreach (var item in (IEnumerable)series)
|
||||
{
|
||||
list.Add(item);
|
||||
@@ -408,4 +409,146 @@ public class TBarSeriesTests
|
||||
Assert.Equal(200, series[1].Time);
|
||||
Assert.Equal(300, series[2].Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithEnumerables_MismatchedLengths_ThrowsArgumentException()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var times = new long[] { 100, 200, 300 };
|
||||
var opens = new double[] { 10, 20 }; // Mismatched length
|
||||
var highs = new double[] { 15, 25, 35 };
|
||||
var lows = new double[] { 5, 15, 25 };
|
||||
var closes = new double[] { 12, 22, 32 };
|
||||
var volumes = new double[] { 100, 200, 300 };
|
||||
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
series.Add(times, opens, highs, lows, closes, volumes));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_OutOfBounds_ThrowsException()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => _ = series[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_NegativeIndex_ThrowsException()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
|
||||
int invalidIndex = -1;
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => _ = series[invalidIndex]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_BeyondCount_ThrowsException()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => _ = series[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithNaN_PreservesNaN()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, double.NaN, 110, 90, 105, 1000);
|
||||
|
||||
series.Add(bar, isNew: true);
|
||||
|
||||
Assert.True(double.IsNaN(series.Last.Open));
|
||||
Assert.True(double.IsNaN(series.Open.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithInfinity_PreservesInfinity()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, 100, double.PositiveInfinity, 90, 105, 1000);
|
||||
|
||||
series.Add(bar, isNew: true);
|
||||
|
||||
Assert.True(double.IsPositiveInfinity(series.Last.High));
|
||||
Assert.True(double.IsPositiveInfinity(series.High.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubSeries_EmptySeries_HaveZeroCount()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
|
||||
Assert.Empty(series.Open);
|
||||
Assert.Empty(series.High);
|
||||
Assert.Empty(series.Low);
|
||||
Assert.Empty(series.Close);
|
||||
Assert.Empty(series.Volume);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubSeries_ValuesSpan_ReturnsCorrectData()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
|
||||
ReadOnlySpan<double> closeValues = series.Close.Values;
|
||||
|
||||
Assert.Equal(2, closeValues.Length);
|
||||
Assert.Equal(12.0, closeValues[0]);
|
||||
Assert.Equal(22.0, closeValues[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubSeries_TimesSpan_ReturnsCorrectData()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
|
||||
ReadOnlySpan<long> times = series.Close.Times;
|
||||
|
||||
Assert.Equal(2, times.Length);
|
||||
Assert.Equal(100, times[0]);
|
||||
Assert.Equal(200, times[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_EventArgs_ContainsIsNewFlag()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
bool? receivedIsNew = null;
|
||||
series.Pub += (object? sender, in TBarEventArgs args) => receivedIsNew = args.IsNew;
|
||||
|
||||
series.Add(new TBar(100, 10, 15, 5, 12, 100), isNew: true);
|
||||
|
||||
Assert.True(receivedIsNew);
|
||||
|
||||
series.Add(new TBar(100, 10, 18, 5, 15, 150), isNew: false);
|
||||
|
||||
Assert.False(receivedIsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithEnumerables_EmptyArrays_AddsNothing()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var empty = Array.Empty<long>();
|
||||
var emptyD = Array.Empty<double>();
|
||||
|
||||
series.Add(empty, emptyD, emptyD, emptyD, emptyD, emptyD);
|
||||
|
||||
Assert.Empty(series);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithCapacity_DoesNotAffectCount()
|
||||
{
|
||||
var series = new TBarSeries(1000);
|
||||
|
||||
Assert.Empty(series);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
@@ -16,6 +17,74 @@ public readonly struct TBarEventArgs
|
||||
public bool IsNew { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// High-performance enumerator for TBarSeries.
|
||||
/// </summary>
|
||||
public struct TBarSeriesEnumerator : IEnumerator<TBar>, IEquatable<TBarSeriesEnumerator>
|
||||
{
|
||||
private readonly List<long> _t;
|
||||
private readonly List<double> _o;
|
||||
private readonly List<double> _h;
|
||||
private readonly List<double> _l;
|
||||
private readonly List<double> _c;
|
||||
private readonly List<double> _v;
|
||||
private readonly int _count;
|
||||
private int _index;
|
||||
private TBar _current;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal TBarSeriesEnumerator(List<long> t, List<double> o, List<double> h, List<double> l, List<double> c, List<double> v)
|
||||
{
|
||||
_t = t;
|
||||
_o = o;
|
||||
_h = h;
|
||||
_l = l;
|
||||
_c = c;
|
||||
_v = v;
|
||||
_count = c.Count;
|
||||
_index = -1;
|
||||
_current = default;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (_index + 1 >= _count)
|
||||
return false;
|
||||
|
||||
_index++;
|
||||
_current = new TBar(_t[_index], _o[_index], _h[_index], _l[_index], _c[_index], _v[_index]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public readonly TBar Current => _current;
|
||||
readonly object IEnumerator.Current => Current;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset()
|
||||
{
|
||||
_index = -1;
|
||||
_current = default;
|
||||
}
|
||||
|
||||
public readonly void Dispose() { }
|
||||
|
||||
public readonly bool Equals(TBarSeriesEnumerator other) =>
|
||||
ReferenceEquals(_t, other._t) &&
|
||||
ReferenceEquals(_c, other._c) &&
|
||||
_count == other._count &&
|
||||
_index == other._index;
|
||||
|
||||
public override readonly bool Equals(object? obj) =>
|
||||
obj is TBarSeriesEnumerator other && Equals(other);
|
||||
|
||||
public override readonly int GetHashCode() =>
|
||||
HashCode.Combine(RuntimeHelpers.GetHashCode(_t), RuntimeHelpers.GetHashCode(_c), _count, _index);
|
||||
|
||||
public static bool operator ==(TBarSeriesEnumerator left, TBarSeriesEnumerator right) => left.Equals(right);
|
||||
public static bool operator !=(TBarSeriesEnumerator left, TBarSeriesEnumerator right) => !left.Equals(right);
|
||||
}
|
||||
|
||||
// Performance-focused event args struct; not derived from EventArgs by design.
|
||||
// We intentionally deviate from the standard EventArgs pattern here for perf.
|
||||
#pragma warning disable MA0046 // The second parameter must be of type 'System.EventArgs' or a derived type
|
||||
@@ -94,6 +163,60 @@ public class TBarSeries : IReadOnlyList<TBar>
|
||||
public double LastClose { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _c.Count > 0 ? _c[^1] : double.NaN; }
|
||||
public double LastVolume { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _v.Count > 0 ? _v[^1] : double.NaN; }
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying Time array as a Span.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<long> Times
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CollectionsMarshal.AsSpan(_t);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying Open array as a Span for SIMD operations.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<double> OpenValues
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CollectionsMarshal.AsSpan(_o);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying High array as a Span for SIMD operations.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<double> HighValues
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CollectionsMarshal.AsSpan(_h);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying Low array as a Span for SIMD operations.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<double> LowValues
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CollectionsMarshal.AsSpan(_l);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying Close array as a Span for SIMD operations.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<double> CloseValues
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CollectionsMarshal.AsSpan(_c);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying Volume array as a Span for SIMD operations.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<double> VolumeValues
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CollectionsMarshal.AsSpan(_v);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Add(TBar bar, bool isNew = true)
|
||||
{
|
||||
@@ -150,13 +273,10 @@ public class TBarSeries : IReadOnlyList<TBar>
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<TBar> GetEnumerator()
|
||||
{
|
||||
for (int i = 0; i < _c.Count; i++)
|
||||
{
|
||||
yield return new TBar(_t[i], _o[i], _h[i], _l[i], _c[i], _v[i]);
|
||||
}
|
||||
}
|
||||
// IEnumerable implementation with struct enumerator for zero-allocation iteration
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBarSeriesEnumerator GetEnumerator() => new(_t, _o, _h, _l, _c, _v);
|
||||
|
||||
IEnumerator<TBar> IEnumerable<TBar>.GetEnumerator() => GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
|
||||
@@ -285,11 +285,14 @@ public class TSeriesTests
|
||||
series.Add(200, 2.0);
|
||||
|
||||
var list = new List<object>();
|
||||
foreach (var item in (IEnumerable)series)
|
||||
IEnumerable enumerable = series;
|
||||
#pragma warning disable S4158
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
list.Add(item);
|
||||
}
|
||||
|
||||
Assert.Equal(2, series.Count);
|
||||
Assert.Equal(2, list.Count);
|
||||
}
|
||||
|
||||
@@ -332,4 +335,204 @@ public class TSeriesTests
|
||||
series.Add(200, 2.0);
|
||||
Assert.Equal(2, series.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithMismatchedLists_WrapsData()
|
||||
{
|
||||
// TSeries wraps the lists directly if they're List<T>, no length validation
|
||||
var times = new List<long> { 100, 200, 300 };
|
||||
var values = new List<double> { 1.0, 2.0 }; // Different length
|
||||
|
||||
var series = new TSeries(times, values);
|
||||
|
||||
// Count is based on values list
|
||||
Assert.Equal(2, series.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_OutOfBounds_ThrowsException()
|
||||
{
|
||||
var series = new TSeries();
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => _ = series[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_NegativeIndex_ThrowsException()
|
||||
{
|
||||
var series = new TSeries();
|
||||
series.Add(100, 1.0);
|
||||
|
||||
#pragma warning disable DS003 // Invalid index - intentional for testing exception
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => _ = series[-1]);
|
||||
#pragma warning restore DS003
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_BeyondCount_ThrowsException()
|
||||
{
|
||||
var series = new TSeries();
|
||||
series.Add(100, 1.0);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => _ = series[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Values_EmptySeries_ReturnsEmptySpan()
|
||||
{
|
||||
var series = new TSeries();
|
||||
|
||||
ReadOnlySpan<double> values = series.Values;
|
||||
|
||||
Assert.Equal(0, values.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Times_EmptySeries_ReturnsEmptySpan()
|
||||
{
|
||||
var series = new TSeries();
|
||||
|
||||
ReadOnlySpan<long> times = series.Times;
|
||||
|
||||
Assert.Equal(0, times.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithNaN_PreservesNaN()
|
||||
{
|
||||
var series = new TSeries();
|
||||
|
||||
series.Add(100, double.NaN);
|
||||
|
||||
Assert.True(double.IsNaN(series.Last.Value));
|
||||
Assert.True(double.IsNaN(series.LastValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithInfinity_PreservesInfinity()
|
||||
{
|
||||
var series = new TSeries();
|
||||
|
||||
series.Add(100, double.PositiveInfinity);
|
||||
|
||||
Assert.True(double.IsPositiveInfinity(series.Last.Value));
|
||||
Assert.True(double.IsPositiveInfinity(series.LastValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithNegativeInfinity_PreservesNegativeInfinity()
|
||||
{
|
||||
var series = new TSeries();
|
||||
|
||||
series.Add(100, double.NegativeInfinity);
|
||||
|
||||
Assert.True(double.IsNegativeInfinity(series.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_EnumerableDoubles_GeneratesIncreasingTimes()
|
||||
{
|
||||
var series = new TSeries();
|
||||
var values = new[] { 1.0, 2.0, 3.0 };
|
||||
|
||||
series.Add(values);
|
||||
|
||||
Assert.Equal(3, series.Count);
|
||||
// Times should be increasing by TicksPerMinute
|
||||
Assert.True(series[1].Time > series[0].Time);
|
||||
Assert.True(series[2].Time > series[1].Time);
|
||||
Assert.Equal(TimeSpan.TicksPerMinute, series[1].Time - series[0].Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_EnumerableDoubles_EmptyArray_AddsNothing()
|
||||
{
|
||||
var series = new TSeries();
|
||||
|
||||
series.Add(Array.Empty<double>());
|
||||
|
||||
Assert.Empty(series);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_EventArgs_ContainsIsNewFlag()
|
||||
{
|
||||
var series = new TSeries();
|
||||
bool? receivedIsNew = null;
|
||||
series.Pub += (object? sender, in TValueEventArgs args) => receivedIsNew = args.IsNew;
|
||||
|
||||
series.Add(new TValue(100, 42.0), isNew: true);
|
||||
|
||||
Assert.True(receivedIsNew);
|
||||
|
||||
series.Add(new TValue(100, 43.0), isNew: false);
|
||||
|
||||
Assert.False(receivedIsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithCapacity_DoesNotAffectCount()
|
||||
{
|
||||
var series = new TSeries(1000);
|
||||
|
||||
Assert.Empty(series);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithDateTimeLocal_ConvertsToUtc()
|
||||
{
|
||||
var series = new TSeries();
|
||||
var localTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Local);
|
||||
|
||||
series.Add(localTime, 100.0);
|
||||
|
||||
// The stored time should be UTC
|
||||
var storedTime = new DateTime(series.Last.Time, DateTimeKind.Utc);
|
||||
Assert.Equal(DateTimeKind.Utc, storedTime.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithDateTimeUnspecified_TreatsAsLocal()
|
||||
{
|
||||
var series = new TSeries();
|
||||
var unspecifiedTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Unspecified);
|
||||
|
||||
series.Add(unspecifiedTime, 100.0);
|
||||
|
||||
Assert.Single(series);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Values_ModifyingUnderlyingList_ReflectsInSpan()
|
||||
{
|
||||
var series = new TSeries();
|
||||
series.Add(100, 1.0);
|
||||
series.Add(200, 2.0);
|
||||
|
||||
// Get the span
|
||||
ReadOnlySpan<double> values1 = series.Values;
|
||||
Assert.Equal(2, values1.Length);
|
||||
|
||||
// Add more data
|
||||
series.Add(300, 3.0);
|
||||
|
||||
// Get new span - should reflect the change
|
||||
ReadOnlySpan<double> values2 = series.Values;
|
||||
Assert.Equal(3, values2.Length);
|
||||
Assert.Equal(3.0, values2[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithReadOnlyLists_CopiesData()
|
||||
{
|
||||
// Using arrays which implement IReadOnlyList but aren't List<T>
|
||||
IReadOnlyList<long> times = new long[] { 100, 200, 300 };
|
||||
IReadOnlyList<double> values = [1.0, 2.0, 3.0];
|
||||
|
||||
var series = new TSeries(times, values);
|
||||
|
||||
Assert.Equal(3, series.Count);
|
||||
Assert.Equal(1.0, series[0].Value);
|
||||
Assert.Equal(3.0, series[2].Value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,66 @@ using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// High-performance enumerator for TSeries.
|
||||
/// </summary>
|
||||
public struct TSeriesEnumerator : IEnumerator<TValue>, IEquatable<TSeriesEnumerator>
|
||||
{
|
||||
private readonly List<long> _t;
|
||||
private readonly List<double> _v;
|
||||
private readonly int _count;
|
||||
private int _index;
|
||||
private TValue _current;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal TSeriesEnumerator(List<long> t, List<double> v)
|
||||
{
|
||||
_t = t;
|
||||
_v = v;
|
||||
_count = v.Count;
|
||||
_index = -1;
|
||||
_current = default;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (_index + 1 >= _count)
|
||||
return false;
|
||||
|
||||
_index++;
|
||||
_current = new TValue(_t[_index], _v[_index]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public readonly TValue Current => _current;
|
||||
readonly object IEnumerator.Current => Current;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset()
|
||||
{
|
||||
_index = -1;
|
||||
_current = default;
|
||||
}
|
||||
|
||||
public readonly void Dispose() { }
|
||||
|
||||
public readonly bool Equals(TSeriesEnumerator other) =>
|
||||
ReferenceEquals(_t, other._t) &&
|
||||
ReferenceEquals(_v, other._v) &&
|
||||
_count == other._count &&
|
||||
_index == other._index;
|
||||
|
||||
public override readonly bool Equals(object? obj) =>
|
||||
obj is TSeriesEnumerator other && Equals(other);
|
||||
|
||||
public override readonly int GetHashCode() =>
|
||||
HashCode.Combine(RuntimeHelpers.GetHashCode(_t), RuntimeHelpers.GetHashCode(_v), _count, _index);
|
||||
|
||||
public static bool operator ==(TSeriesEnumerator left, TSeriesEnumerator right) => left.Equals(right);
|
||||
public static bool operator !=(TSeriesEnumerator left, TSeriesEnumerator right) => !left.Equals(right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A high-performance time series implementation using Structure of Arrays (SoA) layout.
|
||||
/// Stores Time (long) and Value (double) in separate contiguous arrays for SIMD efficiency.
|
||||
@@ -132,14 +192,10 @@ public class TSeries : IReadOnlyList<TValue>, ITValuePublisher
|
||||
}
|
||||
}
|
||||
|
||||
// IEnumerable implementation
|
||||
public IEnumerator<TValue> GetEnumerator()
|
||||
{
|
||||
for (int i = 0; i < _v.Count; i++)
|
||||
{
|
||||
yield return new TValue(_t[i], _v[i]);
|
||||
}
|
||||
}
|
||||
// IEnumerable implementation with struct enumerator for zero-allocation iteration
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TSeriesEnumerator GetEnumerator() => new(_t, _v);
|
||||
|
||||
IEnumerator<TValue> IEnumerable<TValue>.GetEnumerator() => GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
|
||||
+327
-137
@@ -1,182 +1,372 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
namespace QuanTAlib.Tests
|
||||
public class TValueTests
|
||||
{
|
||||
public class TValueTests
|
||||
[Fact]
|
||||
public void Constructor_WithLongTime_SetsPropertiesCorrectly()
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_WithLongTime_SetsPropertiesCorrectly()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
double value = 123.45;
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
double value = 123.45;
|
||||
|
||||
var tValue = new TValue(time, value);
|
||||
var tValue = new TValue(time, value);
|
||||
|
||||
Assert.Equal(time, tValue.Time);
|
||||
Assert.Equal(value, tValue.Value);
|
||||
}
|
||||
Assert.Equal(time, tValue.Time);
|
||||
Assert.Equal(value, tValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithDateTime_SetsPropertiesCorrectly()
|
||||
{
|
||||
var dateTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc);
|
||||
double value = 123.45;
|
||||
[Fact]
|
||||
public void Constructor_WithDateTime_SetsPropertiesCorrectly()
|
||||
{
|
||||
var dateTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc);
|
||||
double value = 123.45;
|
||||
|
||||
var tValue = new TValue(dateTime, value);
|
||||
var tValue = new TValue(dateTime, value);
|
||||
|
||||
Assert.Equal(dateTime.Ticks, tValue.Time);
|
||||
Assert.Equal(value, tValue.Value);
|
||||
}
|
||||
Assert.Equal(dateTime.Ticks, tValue.Time);
|
||||
Assert.Equal(value, tValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsDateTime_ReturnsCorrectDateTime()
|
||||
{
|
||||
var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, 100.0);
|
||||
[Fact]
|
||||
public void AsDateTime_ReturnsCorrectDateTime()
|
||||
{
|
||||
var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, 100.0);
|
||||
|
||||
Assert.Equal(dt, tValue.AsDateTime);
|
||||
Assert.Equal(DateTimeKind.Utc, tValue.AsDateTime.Kind);
|
||||
}
|
||||
Assert.Equal(dt, tValue.AsDateTime);
|
||||
Assert.Equal(DateTimeKind.Utc, tValue.AsDateTime.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_FormatsCorrectly()
|
||||
{
|
||||
var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, 123.456);
|
||||
[Fact]
|
||||
public void ToString_FormatsCorrectly()
|
||||
{
|
||||
var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, 123.456);
|
||||
|
||||
string result = tValue.ToString();
|
||||
string result = tValue.ToString();
|
||||
|
||||
Assert.Contains("2023-01-01", result, StringComparison.Ordinal);
|
||||
Assert.Contains("12:00:00", result, StringComparison.Ordinal);
|
||||
Assert.Contains("123.46", result, StringComparison.Ordinal);
|
||||
}
|
||||
Assert.Contains("2023-01-01", result, StringComparison.Ordinal);
|
||||
Assert.Contains("12:00:00", result, StringComparison.Ordinal);
|
||||
Assert.Contains("123.46", result, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImplicitConversion_ToDouble_ReturnsValue()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0);
|
||||
[Fact]
|
||||
public void ImplicitConversion_ToDouble_ReturnsValue()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0);
|
||||
|
||||
double val = tValue;
|
||||
double val = tValue;
|
||||
|
||||
Assert.Equal(42.0, val);
|
||||
}
|
||||
Assert.Equal(42.0, val);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImplicitConversion_ToDateTime_ReturnsCorrectDateTime()
|
||||
{
|
||||
var dateTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dateTime.Ticks, 100.0);
|
||||
[Fact]
|
||||
public void ImplicitConversion_ToDateTime_ReturnsCorrectDateTime()
|
||||
{
|
||||
var dateTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dateTime.Ticks, 100.0);
|
||||
|
||||
DateTime result = tValue;
|
||||
DateTime result = tValue;
|
||||
|
||||
Assert.Equal(dateTime, result);
|
||||
Assert.Equal(DateTimeKind.Utc, result.Kind);
|
||||
}
|
||||
Assert.Equal(dateTime, result);
|
||||
Assert.Equal(DateTimeKind.Utc, result.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_TValue_SameValues_ReturnsTrue()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12345, 100.0);
|
||||
[Fact]
|
||||
public void Equals_TValue_SameValues_ReturnsTrue()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12345, 100.0);
|
||||
|
||||
Assert.True(tv1.Equals(tv2));
|
||||
}
|
||||
Assert.True(tv1.Equals(tv2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_TValue_DifferentTime_ReturnsFalse()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12346, 100.0);
|
||||
[Fact]
|
||||
public void Equals_TValue_DifferentTime_ReturnsFalse()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12346, 100.0);
|
||||
|
||||
Assert.False(tv1.Equals(tv2));
|
||||
}
|
||||
Assert.False(tv1.Equals(tv2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_TValue_DifferentValue_ReturnsFalse()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12345, 101.0);
|
||||
[Fact]
|
||||
public void Equals_TValue_DifferentValue_ReturnsFalse()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12345, 101.0);
|
||||
|
||||
Assert.False(tv1.Equals(tv2));
|
||||
}
|
||||
Assert.False(tv1.Equals(tv2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_Object_SameTValue_ReturnsTrue()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
object tv2 = new TValue(12345, 100.0);
|
||||
[Fact]
|
||||
public void Equals_Object_SameTValue_ReturnsTrue()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
object tv2 = new TValue(12345, 100.0);
|
||||
|
||||
Assert.True(tv1.Equals(tv2));
|
||||
}
|
||||
Assert.True(tv1.Equals(tv2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_Object_DifferentType_ReturnsFalse()
|
||||
{
|
||||
var tv = new TValue(12345, 100.0);
|
||||
object other = "not a TValue";
|
||||
[Fact]
|
||||
public void Equals_Object_DifferentType_ReturnsFalse()
|
||||
{
|
||||
var tv = new TValue(12345, 100.0);
|
||||
object other = "not a TValue";
|
||||
|
||||
Assert.False(tv.Equals(other));
|
||||
}
|
||||
Assert.False(tv.Equals(other));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_Object_Null_ReturnsFalse()
|
||||
{
|
||||
var tv = new TValue(12345, 100.0);
|
||||
[Fact]
|
||||
public void Equals_Object_Null_ReturnsFalse()
|
||||
{
|
||||
var tv = new TValue(12345, 100.0);
|
||||
|
||||
Assert.False(tv.Equals(null));
|
||||
}
|
||||
Assert.False(tv.Equals(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_SameValues_ReturnsSameHashCode()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12345, 100.0);
|
||||
[Fact]
|
||||
public void GetHashCode_SameValues_ReturnsSameHashCode()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12345, 100.0);
|
||||
|
||||
Assert.Equal(tv1.GetHashCode(), tv2.GetHashCode());
|
||||
}
|
||||
Assert.Equal(tv1.GetHashCode(), tv2.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_DifferentValues_ReturnsDifferentHashCode()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12346, 100.0);
|
||||
[Fact]
|
||||
public void GetHashCode_DifferentValues_ReturnsDifferentHashCode()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12346, 100.0);
|
||||
|
||||
Assert.NotEqual(tv1.GetHashCode(), tv2.GetHashCode());
|
||||
}
|
||||
Assert.NotEqual(tv1.GetHashCode(), tv2.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualityOperator_SameValues_ReturnsTrue()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12345, 100.0);
|
||||
[Fact]
|
||||
public void EqualityOperator_SameValues_ReturnsTrue()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12345, 100.0);
|
||||
|
||||
Assert.True(tv1 == tv2);
|
||||
}
|
||||
Assert.True(tv1 == tv2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualityOperator_DifferentValues_ReturnsFalse()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12346, 100.0);
|
||||
[Fact]
|
||||
public void EqualityOperator_DifferentValues_ReturnsFalse()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12346, 100.0);
|
||||
|
||||
Assert.False(tv1 == tv2);
|
||||
}
|
||||
Assert.False(tv1 == tv2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InequalityOperator_SameValues_ReturnsFalse()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12345, 100.0);
|
||||
[Fact]
|
||||
public void InequalityOperator_SameValues_ReturnsFalse()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12345, 100.0);
|
||||
|
||||
Assert.False(tv1 != tv2);
|
||||
}
|
||||
Assert.False(tv1 != tv2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InequalityOperator_DifferentValues_ReturnsTrue()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12346, 100.0);
|
||||
[Fact]
|
||||
public void InequalityOperator_DifferentValues_ReturnsTrue()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12346, 100.0);
|
||||
|
||||
Assert.True(tv1 != tv2);
|
||||
}
|
||||
Assert.True(tv1 != tv2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithDateTimeLocal_ConvertsToUtc()
|
||||
{
|
||||
var localTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Local);
|
||||
double value = 123.45;
|
||||
|
||||
var tValue = new TValue(localTime, value);
|
||||
|
||||
// Time should be stored as UTC ticks
|
||||
var expectedUtc = localTime.ToUniversalTime();
|
||||
Assert.Equal(expectedUtc.Ticks, tValue.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithDateTimeUnspecified_ConvertsToUtc()
|
||||
{
|
||||
var unspecifiedTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Unspecified);
|
||||
double value = 123.45;
|
||||
|
||||
var tValue = new TValue(unspecifiedTime, value);
|
||||
|
||||
// Unspecified is treated as local and converted to UTC
|
||||
var expectedUtc = unspecifiedTime.ToUniversalTime();
|
||||
Assert.Equal(expectedUtc.Ticks, tValue.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithDateTimeUtc_PreservesTicks()
|
||||
{
|
||||
var utcTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc);
|
||||
double value = 123.45;
|
||||
|
||||
var tValue = new TValue(utcTime, value);
|
||||
|
||||
Assert.Equal(utcTime.Ticks, tValue.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default_TValue_HasZeroTimeAndValue()
|
||||
{
|
||||
var defaultTValue = default(TValue);
|
||||
|
||||
Assert.Equal(0, defaultTValue.Time);
|
||||
Assert.Equal(0.0, defaultTValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNaN_PreservesNaN()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, double.NaN);
|
||||
|
||||
Assert.True(double.IsNaN(tValue.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithPositiveInfinity_PreservesInfinity()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, double.PositiveInfinity);
|
||||
|
||||
Assert.True(double.IsPositiveInfinity(tValue.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNegativeInfinity_PreservesInfinity()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, double.NegativeInfinity);
|
||||
|
||||
Assert.True(double.IsNegativeInfinity(tValue.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithMaxValue_PreservesMaxValue()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, double.MaxValue);
|
||||
|
||||
Assert.Equal(double.MaxValue, tValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithMinValue_PreservesMinValue()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, double.MinValue);
|
||||
|
||||
Assert.Equal(double.MinValue, tValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithEpsilon_PreservesEpsilon()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, double.Epsilon);
|
||||
|
||||
Assert.Equal(double.Epsilon, tValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImplicitConversion_ToDouble_WithNaN_ReturnsNaN()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, double.NaN);
|
||||
|
||||
double val = tValue;
|
||||
|
||||
Assert.True(double.IsNaN(val));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_WithNaN_FormatsCorrectly()
|
||||
{
|
||||
var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, double.NaN);
|
||||
|
||||
string result = tValue.ToString();
|
||||
|
||||
Assert.Contains("NaN", result, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_WithInfinity_FormatsCorrectly()
|
||||
{
|
||||
var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, double.PositiveInfinity);
|
||||
|
||||
string result = tValue.ToString();
|
||||
|
||||
Assert.Contains("∞", result, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_WithNegativeValue_FormatsCorrectly()
|
||||
{
|
||||
var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, -123.456);
|
||||
|
||||
string result = tValue.ToString();
|
||||
|
||||
Assert.Contains("-123.46", result, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsDateTime_ReturnsUtcKind()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, 100.0);
|
||||
|
||||
Assert.Equal(DateTimeKind.Utc, tValue.AsDateTime.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_WithNaN_BothNaN_ReturnsFalse()
|
||||
{
|
||||
// NaN != NaN in IEEE 754
|
||||
var tv1 = new TValue(12345, double.NaN);
|
||||
var tv2 = new TValue(12345, double.NaN);
|
||||
|
||||
// Record struct equality compares fields directly
|
||||
// double.NaN.Equals(double.NaN) returns true in .NET
|
||||
Assert.True(tv1.Equals(tv2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_WithNaN_DoesNotThrow()
|
||||
{
|
||||
var tv = new TValue(12345, double.NaN);
|
||||
|
||||
var hash = tv.GetHashCode();
|
||||
|
||||
Assert.True(hash != 0 || hash == 0); // Just verify it doesn't throw
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithZeroTime_Allowed()
|
||||
{
|
||||
var tValue = new TValue(0, 100.0);
|
||||
|
||||
Assert.Equal(0, tValue.Time);
|
||||
Assert.Equal(100.0, tValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNegativeTime_Allowed()
|
||||
{
|
||||
var tValue = new TValue(-12345, 100.0);
|
||||
|
||||
Assert.Equal(-12345, tValue.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithMaxLongTime_Allowed()
|
||||
{
|
||||
var tValue = new TValue(long.MaxValue, 100.0);
|
||||
|
||||
Assert.Equal(long.MaxValue, tValue.Time);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user