feat: Add Prime method to various indicators for initializing state with historical data

- Implemented Prime method in Vel, Ao, Apo, Frama, Adl, Adosc, Aobv, Cmf, Efi, Eom, Iii, Kvo, Mfi, Nvi, Obv, Pvd, Pvi, Pvo, Pvr, Pvt, Tvi, Twap, Va, Vf, Vo, Vroc, Vwad, Vwap, and Vwma classes.
- The Prime method resets the indicator state and processes the provided historical bar data to initialize the indicator.
- Added warmup period property to Adl and Wad classes to define the minimum number of data points required for validity.
- Updated benchmark tests to use Batch methods for performance evaluation.
This commit is contained in:
Miha Kralj
2026-02-11 20:38:38 -08:00
parent 75c6a9f135
commit 653aafacd8
71 changed files with 10527 additions and 242 deletions
+712
View File
@@ -0,0 +1,712 @@
namespace QuanTAlib.Tests;
/// <summary>
/// Tests for BiInputIndicatorBase abstract class, exercised through Mae (simplest subclass).
/// Covers: constructor validation, Period/IsHot/Name/WarmupPeriod properties,
/// Update(TValue,TValue), Update(double,double), Update(TValue) throws, Update(TSeries) throws,
/// Prime throws, Reset, SanitizeActual/Predicted (NaN, Infinity, first-value-NaN),
/// ProcessNewBar, ProcessBarCorrection (isNew=false), sliding window, resync,
/// CalculateImpl, ValidateBatchInputs, Dispose, Pub event, PostProcess (via Rmse).
/// </summary>
public class BiInputIndicatorBaseTests
{
// ═══════════════════════════════ Constructor ═══════════════════════════════
[Fact]
public void Constructor_ZeroPeriod_Throws()
{
Assert.Throws<ArgumentException>(() => new Mae(0));
}
[Fact]
public void Constructor_NegativePeriod_Throws()
{
Assert.Throws<ArgumentException>(() => new Mae(-1));
}
[Fact]
public void Constructor_LargeNegativePeriod_Throws()
{
Assert.Throws<ArgumentException>(() => new Mae(-100));
}
[Fact]
public void Constructor_ValidPeriod_Succeeds()
{
var indicator = new Mae(10);
Assert.NotNull(indicator);
}
[Fact]
public void Constructor_PeriodOne_IsValid()
{
var indicator = new Mae(1);
Assert.NotNull(indicator);
Assert.Equal(1, indicator.Period);
}
// ═══════════════════════════════ Properties ═══════════════════════════════
[Fact]
public void Period_ReturnsConstructorValue()
{
Assert.Equal(5, new Mae(5).Period);
Assert.Equal(20, new Mae(20).Period);
Assert.Equal(100, new Mae(100).Period);
}
[Fact]
public void WarmupPeriod_EqualsPeriod()
{
var indicator = new Mae(14);
Assert.Equal(14, indicator.WarmupPeriod);
}
[Fact]
public void Name_ContainsIndicatorName()
{
var indicator = new Mae(10);
Assert.Contains("Mae", indicator.Name, StringComparison.Ordinal);
}
[Fact]
public void IsHot_FalseInitially()
{
var indicator = new Mae(5);
Assert.False(indicator.IsHot);
}
[Fact]
public void IsHot_FalseBeforePeriodReached()
{
var indicator = new Mae(5);
for (int i = 0; i < 4; i++)
{
indicator.Update(i * 10.0, i * 10.0 + 5.0);
Assert.False(indicator.IsHot);
}
}
[Fact]
public void IsHot_TrueAfterPeriodReached()
{
var indicator = new Mae(5);
for (int i = 0; i < 5; i++)
{
indicator.Update(i * 10.0, i * 10.0 + 5.0);
}
Assert.True(indicator.IsHot);
}
[Fact]
public void IsHot_StaysTrueAfterMoreUpdates()
{
var indicator = new Mae(3);
for (int i = 0; i < 20; i++)
{
indicator.Update(i * 10.0, i * 10.0 + 5.0);
}
Assert.True(indicator.IsHot);
}
[Fact]
public void Last_DefaultBeforeUpdate()
{
var indicator = new Mae(5);
Assert.Equal(0.0, indicator.Last.Value);
}
// ═══════════════════════════════ Update(double, double) ═══════════════════
[Fact]
public void Update_DoubleDouble_ReturnsResult()
{
var indicator = new Mae(3);
var result = indicator.Update(100.0, 110.0);
Assert.Equal(10.0, result.Value, 10);
}
[Fact]
public void Update_DoubleDouble_SetsLast()
{
var indicator = new Mae(3);
indicator.Update(100.0, 110.0);
Assert.Equal(10.0, indicator.Last.Value, 10);
}
[Fact]
public void Update_DoubleDouble_IsNewDefaultTrue()
{
var indicator = new Mae(3);
indicator.Update(100.0, 110.0);
indicator.Update(200.0, 220.0);
// Two distinct updates means 2 bars were added
Assert.Equal(15.0, indicator.Last.Value, 10); // (10 + 20) / 2
}
// ═══════════════════════════════ Update(TValue, TValue) ═══════════════════
[Fact]
public void Update_TValueTValue_ReturnsResult()
{
var indicator = new Mae(3);
var now = DateTime.UtcNow;
var result = indicator.Update(
new TValue(now, 100.0),
new TValue(now, 110.0));
Assert.Equal(10.0, result.Value, 10);
}
[Fact]
public void Update_TValueTValue_PreservesTime()
{
var indicator = new Mae(3);
var now = DateTime.UtcNow;
var result = indicator.Update(
new TValue(now, 50.0),
new TValue(now, 60.0));
Assert.Equal(now.Ticks, result.Time);
}
// ═══════════════════════════════ Single-input throws ═══════════════════════
[Fact]
public void Update_SingleTValue_Throws()
{
var indicator = new Mae(5);
Assert.Throws<NotSupportedException>(() =>
indicator.Update(new TValue(DateTime.UtcNow, 100.0)));
}
[Fact]
public void Update_SingleTSeries_Throws()
{
var indicator = new Mae(5);
Assert.Throws<NotSupportedException>(() =>
indicator.Update(new TSeries()));
}
[Fact]
public void Prime_SingleSpan_Throws()
{
var indicator = new Mae(5);
Assert.Throws<NotSupportedException>(() =>
indicator.Prime(new double[] { 1, 2, 3 }));
}
// ═══════════════════════════════ Sliding Window ═══════════════════════════
[Fact]
public void SlidingWindow_DropsOldestValue()
{
var indicator = new Mae(3);
// |10-15|=5, |20-30|=10, |30-25|=5 → mean=(5+10+5)/3=6.667
indicator.Update(10.0, 15.0);
indicator.Update(20.0, 30.0);
indicator.Update(30.0, 25.0);
Assert.Equal(20.0 / 3.0, indicator.Last.Value, 10);
// Window slides: drop 5, add |40-50|=10 → mean=(10+5+10)/3=8.333
indicator.Update(40.0, 50.0);
Assert.Equal(25.0 / 3.0, indicator.Last.Value, 10);
}
[Fact]
public void SlidingWindow_PeriodOne_AlwaysLatestError()
{
var indicator = new Mae(1);
indicator.Update(10.0, 15.0);
Assert.Equal(5.0, indicator.Last.Value, 10);
indicator.Update(20.0, 30.0);
Assert.Equal(10.0, indicator.Last.Value, 10);
indicator.Update(100.0, 100.0);
Assert.Equal(0.0, indicator.Last.Value, 10);
}
// ═══════════════════════════════ Bar Correction (isNew=false) ═════════════
[Fact]
public void BarCorrection_OverwritesLastBar()
{
var indicator = new Mae(5);
indicator.Update(100.0, 110.0); // error=10
indicator.Update(200.0, 220.0); // error=20
// Correct last bar
indicator.Update(200.0, 210.0, isNew: false); // error=10
// Mean = (10 + 10) / 2 = 10
Assert.Equal(10.0, indicator.Last.Value, 10);
}
[Fact]
public void BarCorrection_MultipleCorrections_LastOneWins()
{
var indicator = new Mae(5);
indicator.Update(100.0, 110.0); // error=10
indicator.Update(200.0, 220.0, isNew: true); // error=20
// Multiple corrections to same bar
indicator.Update(200.0, 215.0, isNew: false); // error=15
indicator.Update(200.0, 205.0, isNew: false); // error=5
indicator.Update(200.0, 203.0, isNew: false); // error=3
// Mean = (10 + 3) / 2 = 6.5
Assert.Equal(6.5, indicator.Last.Value, 10);
}
[Fact]
public void BarCorrection_RestoresToOriginalWhenSameValue()
{
var indicator = new Mae(5);
for (int i = 0; i < 10; i++)
{
indicator.Update(i * 10.0, i * 10.0 + 5.0);
}
double original = indicator.Last.Value;
// Correct with different values
indicator.Update(999.0, 888.0, isNew: false);
Assert.NotEqual(original, indicator.Last.Value);
// Restore original
indicator.Update(90.0, 95.0, isNew: false);
Assert.Equal(original, indicator.Last.Value, 10);
}
// ═══════════════════════════════ NaN/Infinity Sanitization ════════════════
[Fact]
public void NaN_Actual_UsesLastValidActual()
{
var indicator = new Mae(5);
indicator.Update(100.0, 110.0);
indicator.Update(double.NaN, 120.0);
Assert.True(double.IsFinite(indicator.Last.Value));
}
[Fact]
public void NaN_Predicted_UsesLastValidPredicted()
{
var indicator = new Mae(5);
indicator.Update(100.0, 110.0);
indicator.Update(120.0, double.NaN);
Assert.True(double.IsFinite(indicator.Last.Value));
}
[Fact]
public void NaN_Both_UsesLastValidValues()
{
var indicator = new Mae(5);
indicator.Update(100.0, 110.0);
indicator.Update(120.0, 130.0);
var result = indicator.Update(double.NaN, double.NaN);
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void PositiveInfinity_Sanitized()
{
var indicator = new Mae(5);
indicator.Update(100.0, 110.0);
var result = indicator.Update(double.PositiveInfinity, 120.0);
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void NegativeInfinity_Sanitized()
{
var indicator = new Mae(5);
indicator.Update(100.0, 110.0);
var result = indicator.Update(120.0, double.NegativeInfinity);
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void FirstValue_NaN_ReturnsZeroSubstitute()
{
var indicator = new Mae(5);
var result = indicator.Update(double.NaN, double.NaN);
// When no last valid value exists, 0.0 is substituted
Assert.True(double.IsFinite(result.Value));
Assert.Equal(0.0, result.Value, 10);
}
[Fact]
public void MultipleConsecutiveNaN_AllFinite()
{
var indicator = new Mae(5);
indicator.Update(100.0, 110.0);
for (int i = 0; i < 10; i++)
{
var result = indicator.Update(double.NaN, double.NaN);
Assert.True(double.IsFinite(result.Value));
}
}
// ═══════════════════════════════ Reset ════════════════════════════════════
[Fact]
public void Reset_ClearsIsHot()
{
var indicator = new Mae(3);
for (int i = 0; i < 5; i++)
{
indicator.Update(i * 10.0, i * 10.0 + 5.0);
}
Assert.True(indicator.IsHot);
indicator.Reset();
Assert.False(indicator.IsHot);
}
[Fact]
public void Reset_ClearsLast()
{
var indicator = new Mae(3);
indicator.Update(100.0, 110.0);
Assert.NotEqual(0.0, indicator.Last.Value);
indicator.Reset();
Assert.Equal(0.0, indicator.Last.Value);
}
[Fact]
public void Reset_AllowsReuse()
{
var indicator = new Mae(3);
// First use
for (int i = 0; i < 5; i++)
{
indicator.Update(100.0, 110.0);
}
double firstResult = indicator.Last.Value;
indicator.Reset();
// Second use - should produce same results
for (int i = 0; i < 5; i++)
{
indicator.Update(100.0, 110.0);
}
double secondResult = indicator.Last.Value;
Assert.Equal(firstResult, secondResult, 10);
}
// ═══════════════════════════════ Resync ═══════════════════════════════════
[Fact]
public void Resync_After1000Updates_MaintainsAccuracy()
{
var indicator = new Mae(5);
for (int i = 0; i < 1100; i++)
{
indicator.Update(i * 1.0, i + 10.0);
}
// Constant error of 10, so MAE should be 10
Assert.Equal(10.0, indicator.Last.Value, 8);
}
// ═══════════════════════════════ Pub Event ════════════════════════════════
[Fact]
public void PubEvent_FiredOnUpdate()
{
var indicator = new Mae(3);
int eventCount = 0;
TValuePublishedHandler handler = (object? sender, in TValueEventArgs args) => eventCount++;
indicator.Pub += handler;
indicator.Update(100.0, 110.0);
Assert.Equal(1, eventCount);
indicator.Update(200.0, 220.0);
Assert.Equal(2, eventCount);
indicator.Pub -= handler;
}
[Fact]
public void PubEvent_FiredOnBarCorrection()
{
var indicator = new Mae(3);
int eventCount = 0;
TValuePublishedHandler handler = (object? sender, in TValueEventArgs args) => eventCount++;
indicator.Pub += handler;
indicator.Update(100.0, 110.0);
indicator.Update(100.0, 120.0, isNew: false); // correction
Assert.Equal(2, eventCount);
indicator.Pub -= handler;
}
// ═══════════════════════════════ PostProcess (via Rmse) ═══════════════════
[Fact]
public void PostProcess_Rmse_AppliesSqrt()
{
var rmse = new Rmse(3);
// |10-15|²=25, RMSE=sqrt(25/1)=5
var result = rmse.Update(10.0, 15.0);
Assert.Equal(5.0, result.Value, 10);
}
[Fact]
public void PostProcess_Mae_ReturnsUnchanged()
{
var mae = new Mae(3);
// |10-15|=5, MAE=5/1=5
var result = mae.Update(10.0, 15.0);
Assert.Equal(5.0, result.Value, 10);
}
// ═══════════════════════════════ ValidateBatchInputs ═════════════════════
[Fact]
public void BatchValidation_MismatchedLengths_Throws()
{
double[] actual = [1, 2, 3];
double[] predicted = [1, 2, 3, 4, 5];
double[] output = new double[3];
Assert.Throws<ArgumentException>(() =>
Mae.Batch(actual.AsSpan(), predicted.AsSpan(), output.AsSpan(), 3));
}
[Fact]
public void BatchValidation_MismatchedOutput_Throws()
{
double[] actual = [1, 2, 3, 4, 5];
double[] predicted = [1, 2, 3, 4, 5];
double[] output = new double[3];
Assert.Throws<ArgumentException>(() =>
Mae.Batch(actual.AsSpan(), predicted.AsSpan(), output.AsSpan(), 3));
}
[Fact]
public void BatchValidation_ZeroPeriod_Throws()
{
double[] actual = [1, 2, 3];
double[] predicted = [1, 2, 3];
double[] output = new double[3];
Assert.Throws<ArgumentException>(() =>
Mae.Batch(actual.AsSpan(), predicted.AsSpan(), output.AsSpan(), 0));
}
[Fact]
public void BatchValidation_NegativePeriod_Throws()
{
double[] actual = [1, 2, 3];
double[] predicted = [1, 2, 3];
double[] output = new double[3];
Assert.Throws<ArgumentException>(() =>
Mae.Batch(actual.AsSpan(), predicted.AsSpan(), output.AsSpan(), -5));
}
[Fact]
public void BatchValidation_EmptyInput_NoException()
{
double[] actual = [];
double[] predicted = [];
double[] output = [];
Mae.Batch(actual.AsSpan(), predicted.AsSpan(), output.AsSpan(), 3);
Assert.True(true); // Verify no exception thrown
}
// ═══════════════════════════════ CalculateImpl (via Batch TSeries) ════════
[Fact]
public void CalculateImpl_MismatchedSeries_Throws()
{
var actual = new TSeries();
var predicted = new TSeries();
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
actual.Add(now.AddMinutes(i), i * 10.0);
}
for (int i = 0; i < 5; i++)
{
predicted.Add(now.AddMinutes(i), i * 10.0);
}
Assert.Throws<ArgumentException>(() => Mae.Batch(actual, predicted, 3));
}
[Fact]
public void CalculateImpl_ValidSeries_ReturnsCorrectCount()
{
var actual = new TSeries();
var predicted = new TSeries();
var now = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
actual.Add(now.AddMinutes(i), i * 10.0);
predicted.Add(now.AddMinutes(i), i * 10.0 + 5.0);
}
var result = Mae.Batch(actual, predicted, 5);
Assert.Equal(20, result.Count);
}
[Fact]
public void CalculateImpl_ConstantError_AllWindowedValuesEqual()
{
var actual = new TSeries();
var predicted = new TSeries();
var now = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
actual.Add(now.AddMinutes(i), 100.0);
predicted.Add(now.AddMinutes(i), 107.0);
}
var result = Mae.Batch(actual, predicted, 5);
// Once window is full (index >= 4), all values should be 7.0
for (int i = 4; i < 20; i++)
{
Assert.Equal(7.0, result[i].Value, 10);
}
}
// ═══════════════════════════════ Calculate static ═════════════════════════
[Fact]
public void Calculate_ReturnsResultsAndIndicator()
{
var actual = new TSeries();
var predicted = new TSeries();
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
actual.Add(now.AddMinutes(i), i * 10.0);
predicted.Add(now.AddMinutes(i), i * 10.0 + 3.0);
}
var (results, indicator) = Mae.Calculate(actual, predicted, 5);
Assert.Equal(10, results.Count);
Assert.NotNull(indicator);
Assert.Equal(5, indicator.Period);
}
// ═══════════════════════════════ Dispose ═════════════════════════════════
[Fact]
public void Dispose_DoesNotThrow()
{
var indicator = new Mae(5);
indicator.Update(100.0, 110.0);
indicator.Dispose();
Assert.True(true); // Verify no exception thrown
}
[Fact]
public void Dispose_CalledMultipleTimes_NoException()
{
var indicator = new Mae(5);
indicator.Dispose();
indicator.Dispose();
Assert.True(true); // Verify no exception thrown
}
// ═══════════════════════════════ Batch vs Streaming ═════════════════════
[Fact]
public void Batch_MatchesStreaming_RandomData()
{
const int period = 7;
const int count = 200;
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
double[] actual = new double[count];
double[] predicted = new double[count];
for (int i = 0; i < count; i++)
{
var bar = gbm.Next();
actual[i] = bar.Close;
predicted[i] = bar.Close * 1.03 + 1.0;
}
// Streaming
var mae = new Mae(period);
double[] streamResults = new double[count];
for (int i = 0; i < count; i++)
{
streamResults[i] = mae.Update(actual[i], predicted[i]).Value;
}
// Batch
double[] batchResults = new double[count];
Mae.Batch(actual, predicted, batchResults, period);
for (int i = 0; i < count; i++)
{
Assert.Equal(streamResults[i], batchResults[i], 9);
}
}
// ═══════════════════════════════ Edge Cases ═══════════════════════════════
[Fact]
public void Update_LargeValues_NoOverflow()
{
var indicator = new Mae(3);
indicator.Update(1e300, 1e300 + 1e290);
Assert.True(double.IsFinite(indicator.Last.Value));
}
[Fact]
public void Update_VerySmallValues_NoUnderflow()
{
var indicator = new Mae(3);
indicator.Update(1e-300, 2e-300);
Assert.True(double.IsFinite(indicator.Last.Value));
}
[Fact]
public void Update_ZeroValues_ReturnsZero()
{
var indicator = new Mae(3);
indicator.Update(0.0, 0.0);
Assert.Equal(0.0, indicator.Last.Value, 10);
}
[Fact]
public void Update_NegativeValues_HandledCorrectly()
{
var indicator = new Mae(3);
var result = indicator.Update(-100.0, -110.0);
Assert.Equal(10.0, result.Value, 10);
}
[Fact]
public void Update_MixedSignValues_AbsoluteError()
{
var indicator = new Mae(1);
var result = indicator.Update(-50.0, 50.0);
Assert.Equal(100.0, result.Value, 10);
}
}
@@ -0,0 +1,655 @@
namespace QuanTAlib.Tests;
/// <summary>
/// Tests for MonotonicDeque — O(1) amortized sliding window min/max data structure.
/// Covers: constructor validation, PushMax, PushMin, GetExtremum, Reset,
/// RebuildMax, RebuildMin, FrontIndex, Count, window expiration,
/// edge cases (equal values, descending/ascending sequences).
/// </summary>
public class MonotonicDequeTests
{
// ═══════════════════════════════ Constructor ═══════════════════════════════
[Fact]
public void Constructor_ZeroPeriod_Throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new MonotonicDeque(0));
}
[Fact]
public void Constructor_NegativePeriod_Throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new MonotonicDeque(-1));
}
[Fact]
public void Constructor_ValidPeriod_Succeeds()
{
var deque = new MonotonicDeque(5);
Assert.NotNull(deque);
}
[Fact]
public void Constructor_PeriodOne_IsValid()
{
var deque = new MonotonicDeque(1);
Assert.Equal(0, deque.Count);
}
// ═══════════════════════════════ Initial State ════════════════════════════
[Fact]
public void Count_InitiallyZero()
{
var deque = new MonotonicDeque(5);
Assert.Equal(0, deque.Count);
}
[Fact]
public void FrontIndex_InitiallyNegativeOne()
{
var deque = new MonotonicDeque(5);
Assert.Equal(-1, deque.FrontIndex);
}
[Fact]
public void GetExtremum_Empty_ReturnsNaN()
{
var deque = new MonotonicDeque(5);
double[] buffer = new double[5];
Assert.True(double.IsNaN(deque.GetExtremum(buffer)));
}
// ═══════════════════════════════ PushMax ═══════════════════════════════════
[Fact]
public void PushMax_SingleValue_Tracked()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = 100.0;
deque.PushMax(0, 100.0, buffer);
Assert.Equal(1, deque.Count);
Assert.Equal(100.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMax_AscendingValues_TracksMaximum()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
for (int i = 0; i < 5; i++)
{
buffer[i % period] = i * 10.0;
deque.PushMax(i, i * 10.0, buffer);
}
// Maximum should be 40 (last value in ascending sequence)
Assert.Equal(40.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMax_DescendingValues_TracksMaximum()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
for (int i = 0; i < 5; i++)
{
double val = (4 - i) * 10.0;
buffer[i % period] = val;
deque.PushMax(i, val, buffer);
}
// Maximum should be 40 (first value in descending sequence)
Assert.Equal(40.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMax_EqualValues_TracksCorrectly()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
for (int i = 0; i < 5; i++)
{
buffer[i % period] = 50.0;
deque.PushMax(i, 50.0, buffer);
}
Assert.Equal(50.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMax_WindowExpiration_DropsOldMax()
{
int period = 3;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
// Push: 100, 50, 30, then 20 (window slides past 100)
buffer[0 % period] = 100.0;
deque.PushMax(0, 100.0, buffer);
Assert.Equal(100.0, deque.GetExtremum(buffer));
buffer[1 % period] = 50.0;
deque.PushMax(1, 50.0, buffer);
Assert.Equal(100.0, deque.GetExtremum(buffer));
buffer[2 % period] = 30.0;
deque.PushMax(2, 30.0, buffer);
Assert.Equal(100.0, deque.GetExtremum(buffer));
// Index 3 — window now [1,2,3], so 100 (index 0) expires
buffer[3 % period] = 20.0;
deque.PushMax(3, 20.0, buffer);
Assert.Equal(50.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMax_NewMaxReplacesAll()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0 % period] = 10.0;
deque.PushMax(0, 10.0, buffer);
buffer[1 % period] = 20.0;
deque.PushMax(1, 20.0, buffer);
buffer[2 % period] = 30.0;
deque.PushMax(2, 30.0, buffer);
// New value 100 should replace all (they're all <=)
buffer[3 % period] = 100.0;
deque.PushMax(3, 100.0, buffer);
Assert.Equal(100.0, deque.GetExtremum(buffer));
Assert.Equal(1, deque.Count);
}
// ═══════════════════════════════ PushMin ═══════════════════════════════════
[Fact]
public void PushMin_SingleValue_Tracked()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = 100.0;
deque.PushMin(0, 100.0, buffer);
Assert.Equal(1, deque.Count);
Assert.Equal(100.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMin_DescendingValues_TracksMinimum()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
for (int i = 0; i < 5; i++)
{
double val = (4 - i) * 10.0;
buffer[i % period] = val;
deque.PushMin(i, val, buffer);
}
// Minimum should be 0 (last value in descending sequence)
Assert.Equal(0.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMin_AscendingValues_TracksMinimum()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
for (int i = 0; i < 5; i++)
{
buffer[i % period] = i * 10.0;
deque.PushMin(i, i * 10.0, buffer);
}
// Minimum should be 0 (first value in ascending sequence)
Assert.Equal(0.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMin_EqualValues_TracksCorrectly()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
for (int i = 0; i < 5; i++)
{
buffer[i % period] = 50.0;
deque.PushMin(i, 50.0, buffer);
}
Assert.Equal(50.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMin_WindowExpiration_DropsOldMin()
{
int period = 3;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
// Push: 10, 50, 80, then 90 (window slides past 10)
buffer[0 % period] = 10.0;
deque.PushMin(0, 10.0, buffer);
Assert.Equal(10.0, deque.GetExtremum(buffer));
buffer[1 % period] = 50.0;
deque.PushMin(1, 50.0, buffer);
Assert.Equal(10.0, deque.GetExtremum(buffer));
buffer[2 % period] = 80.0;
deque.PushMin(2, 80.0, buffer);
Assert.Equal(10.0, deque.GetExtremum(buffer));
// Index 3 — window now [1,2,3], so 10 (index 0) expires
buffer[3 % period] = 90.0;
deque.PushMin(3, 90.0, buffer);
Assert.Equal(50.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMin_NewMinReplacesAll()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0 % period] = 100.0;
deque.PushMin(0, 100.0, buffer);
buffer[1 % period] = 80.0;
deque.PushMin(1, 80.0, buffer);
buffer[2 % period] = 60.0;
deque.PushMin(2, 60.0, buffer);
// New value 5 should replace all (they're all >=)
buffer[3 % period] = 5.0;
deque.PushMin(3, 5.0, buffer);
Assert.Equal(5.0, deque.GetExtremum(buffer));
Assert.Equal(1, deque.Count);
}
// ═══════════════════════════════ FrontIndex ═══════════════════════════════
[Fact]
public void FrontIndex_TracksCurrentExtremumIndex()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = 100.0;
deque.PushMax(0, 100.0, buffer);
Assert.Equal(0, deque.FrontIndex);
buffer[1 % period] = 50.0;
deque.PushMax(1, 50.0, buffer);
Assert.Equal(0, deque.FrontIndex); // 100 is still max
buffer[2 % period] = 200.0;
deque.PushMax(2, 200.0, buffer);
Assert.Equal(2, deque.FrontIndex); // 200 is new max
}
// ═══════════════════════════════ Reset ════════════════════════════════════
[Fact]
public void Reset_ClearsCount()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
for (int i = 0; i < 3; i++)
{
buffer[i] = i * 10.0;
deque.PushMax(i, i * 10.0, buffer);
}
Assert.True(deque.Count > 0);
deque.Reset();
Assert.Equal(0, deque.Count);
}
[Fact]
public void Reset_FrontIndexBecomesNegativeOne()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = 50.0;
deque.PushMax(0, 50.0, buffer);
deque.Reset();
Assert.Equal(-1, deque.FrontIndex);
}
[Fact]
public void Reset_GetExtremumReturnsNaN()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = 50.0;
deque.PushMax(0, 50.0, buffer);
deque.Reset();
Assert.True(double.IsNaN(deque.GetExtremum(buffer)));
}
[Fact]
public void Reset_AllowsReuse()
{
int period = 3;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = 100.0;
deque.PushMax(0, 100.0, buffer);
deque.Reset();
buffer[0] = 50.0;
deque.PushMax(0, 50.0, buffer);
Assert.Equal(50.0, deque.GetExtremum(buffer));
Assert.Equal(1, deque.Count);
}
// ═══════════════════════════════ RebuildMax ═══════════════════════════════
[Fact]
public void RebuildMax_EmptyBuffer_NoOp()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
deque.RebuildMax(buffer, 0, 0);
Assert.Equal(0, deque.Count);
}
[Fact]
public void RebuildMax_RebuildsCorrectly()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
// Fill buffer: [10, 50, 30, 20, 40]
buffer[0] = 10.0;
buffer[1] = 50.0;
buffer[2] = 30.0;
buffer[3] = 20.0;
buffer[4] = 40.0;
deque.RebuildMax(buffer, 4, 5);
// Maximum should be 50 (at index 1)
Assert.Equal(50.0, deque.GetExtremum(buffer));
}
[Fact]
public void RebuildMax_SingleElement()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = 42.0;
deque.RebuildMax(buffer, 0, 1);
Assert.Equal(42.0, deque.GetExtremum(buffer));
Assert.Equal(1, deque.Count);
}
[Fact]
public void RebuildMax_AfterPreviousData_Resets()
{
int period = 3;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
// First push some data
buffer[0] = 100.0;
deque.PushMax(0, 100.0, buffer);
buffer[1] = 200.0;
deque.PushMax(1, 200.0, buffer);
// Rebuild with different data
buffer[0] = 10.0;
buffer[1] = 20.0;
buffer[2] = 15.0;
deque.RebuildMax(buffer, 2, 3);
Assert.Equal(20.0, deque.GetExtremum(buffer));
}
// ═══════════════════════════════ RebuildMin ═══════════════════════════════
[Fact]
public void RebuildMin_EmptyBuffer_NoOp()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
deque.RebuildMin(buffer, 0, 0);
Assert.Equal(0, deque.Count);
}
[Fact]
public void RebuildMin_RebuildsCorrectly()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
// Fill buffer: [10, 50, 30, 20, 40]
buffer[0] = 10.0;
buffer[1] = 50.0;
buffer[2] = 30.0;
buffer[3] = 20.0;
buffer[4] = 40.0;
deque.RebuildMin(buffer, 4, 5);
// Minimum should be 10 (at index 0)
Assert.Equal(10.0, deque.GetExtremum(buffer));
}
[Fact]
public void RebuildMin_SingleElement()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = 42.0;
deque.RebuildMin(buffer, 0, 1);
Assert.Equal(42.0, deque.GetExtremum(buffer));
Assert.Equal(1, deque.Count);
}
[Fact]
public void RebuildMin_AfterPreviousData_Resets()
{
int period = 3;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
// First push some data
buffer[0] = 5.0;
deque.PushMin(0, 5.0, buffer);
buffer[1] = 3.0;
deque.PushMin(1, 3.0, buffer);
// Rebuild with different data
buffer[0] = 100.0;
buffer[1] = 200.0;
buffer[2] = 150.0;
deque.RebuildMin(buffer, 2, 3);
Assert.Equal(100.0, deque.GetExtremum(buffer));
}
// ═══════════════════════════════ Sliding Window Scenarios ═════════════════
[Fact]
public void PushMax_LongSequence_TracksRollingMax()
{
int period = 3;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
double[] values = [10, 20, 30, 15, 25, 5, 35, 10, 40, 20];
double[] expectedMax = [10, 20, 30, 30, 30, 25, 35, 35, 40, 40];
for (int i = 0; i < values.Length; i++)
{
buffer[i % period] = values[i];
deque.PushMax(i, values[i], buffer);
double actual = deque.GetExtremum(buffer);
Assert.True(actual == expectedMax[i],
$"Max mismatch at index {i}: expected {expectedMax[i]}, got {actual}");
}
}
[Fact]
public void PushMin_LongSequence_TracksRollingMin()
{
int period = 3;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
double[] values = [50, 40, 30, 45, 35, 55, 25, 60, 20, 50];
double[] expectedMin = [50, 40, 30, 30, 30, 35, 25, 25, 20, 20];
for (int i = 0; i < values.Length; i++)
{
buffer[i % period] = values[i];
deque.PushMin(i, values[i], buffer);
double actual = deque.GetExtremum(buffer);
Assert.True(actual == expectedMin[i],
$"Min mismatch at index {i}: expected {expectedMin[i]}, got {actual}");
}
}
// ═══════════════════════════════ Period 1 ═════════════════════════════════
[Fact]
public void PushMax_PeriodOne_AlwaysLatestValue()
{
int period = 1;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = 100.0;
deque.PushMax(0, 100.0, buffer);
Assert.Equal(100.0, deque.GetExtremum(buffer));
buffer[0] = 50.0;
deque.PushMax(1, 50.0, buffer);
Assert.Equal(50.0, deque.GetExtremum(buffer));
buffer[0] = 200.0;
deque.PushMax(2, 200.0, buffer);
Assert.Equal(200.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMin_PeriodOne_AlwaysLatestValue()
{
int period = 1;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = 100.0;
deque.PushMin(0, 100.0, buffer);
Assert.Equal(100.0, deque.GetExtremum(buffer));
buffer[0] = 50.0;
deque.PushMin(1, 50.0, buffer);
Assert.Equal(50.0, deque.GetExtremum(buffer));
}
// ═══════════════════════════════ Edge Cases ═══════════════════════════════
[Fact]
public void PushMax_NegativeValues_TracksCorrectly()
{
int period = 3;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = -30.0;
deque.PushMax(0, -30.0, buffer);
buffer[1 % period] = -10.0;
deque.PushMax(1, -10.0, buffer);
buffer[2 % period] = -20.0;
deque.PushMax(2, -20.0, buffer);
Assert.Equal(-10.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMin_NegativeValues_TracksCorrectly()
{
int period = 3;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = -10.0;
deque.PushMin(0, -10.0, buffer);
buffer[1 % period] = -30.0;
deque.PushMin(1, -30.0, buffer);
buffer[2 % period] = -20.0;
deque.PushMin(2, -20.0, buffer);
Assert.Equal(-30.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMax_VeryLargeValues_NoOverflow()
{
int period = 3;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = 1e300;
deque.PushMax(0, 1e300, buffer);
Assert.Equal(1e300, deque.GetExtremum(buffer));
}
[Fact]
public void PushMax_ZeroValues_TracksCorrectly()
{
int period = 3;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
for (int i = 0; i < 3; i++)
{
buffer[i] = 0.0;
deque.PushMax(i, 0.0, buffer);
}
Assert.Equal(0.0, deque.GetExtremum(buffer));
}
}
File diff suppressed because it is too large Load Diff
+658
View File
@@ -598,4 +598,662 @@ public class TBarSeriesTests
Assert.Equal(12.0, closes[0]);
Assert.Equal(22.0, closes[1]);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: TryGetLast
// ────────────────────────────────────────────────────────────────────
[Fact]
public void TryGetLast_EmptySeries_ReturnsFalseAndDefault()
{
var series = new TBarSeries();
bool result = series.TryGetLast(out TBar bar);
Assert.False(result);
Assert.Equal(0, bar.Time);
Assert.Equal(0.0, bar.Close);
}
[Fact]
public void TryGetLast_NonEmptySeries_ReturnsTrueAndLastBar()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
bool result = series.TryGetLast(out TBar bar);
Assert.True(result);
Assert.Equal(200, bar.Time);
Assert.Equal(20.0, bar.Open);
Assert.Equal(25.0, bar.High);
Assert.Equal(15.0, bar.Low);
Assert.Equal(22.0, bar.Close);
Assert.Equal(200.0, bar.Volume);
}
[Fact]
public void TryGetLast_SingleBar_ReturnsOnlyBar()
{
var series = new TBarSeries();
series.Add(999, 50, 60, 40, 55, 500);
bool result = series.TryGetLast(out TBar bar);
Assert.True(result);
Assert.Equal(999, bar.Time);
Assert.Equal(55.0, bar.Close);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: Span Accessors (Times, OpenValues, HighValues, etc.)
// ────────────────────────────────────────────────────────────────────
[Fact]
public void Times_ReturnsCorrectSpan()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
series.Add(300, 30, 35, 25, 32, 300);
ReadOnlySpan<long> times = series.Times;
Assert.Equal(3, times.Length);
Assert.Equal(100, times[0]);
Assert.Equal(200, times[1]);
Assert.Equal(300, times[2]);
}
[Fact]
public void Times_EmptySeries_ReturnsEmptySpan()
{
var series = new TBarSeries();
ReadOnlySpan<long> times = series.Times;
Assert.Equal(0, times.Length);
}
[Fact]
public void OpenValues_ReturnsCorrectSpan()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
ReadOnlySpan<double> opens = series.OpenValues;
Assert.Equal(2, opens.Length);
Assert.Equal(10.0, opens[0]);
Assert.Equal(20.0, opens[1]);
}
[Fact]
public void HighValues_ReturnsCorrectSpan()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
ReadOnlySpan<double> highs = series.HighValues;
Assert.Equal(2, highs.Length);
Assert.Equal(15.0, highs[0]);
Assert.Equal(25.0, highs[1]);
}
[Fact]
public void LowValues_ReturnsCorrectSpan()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
ReadOnlySpan<double> lows = series.LowValues;
Assert.Equal(2, lows.Length);
Assert.Equal(5.0, lows[0]);
Assert.Equal(15.0, lows[1]);
}
[Fact]
public void CloseValues_ReturnsCorrectSpan()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
ReadOnlySpan<double> closes = series.CloseValues;
Assert.Equal(2, closes.Length);
Assert.Equal(12.0, closes[0]);
Assert.Equal(22.0, closes[1]);
}
[Fact]
public void VolumeValues_ReturnsCorrectSpan()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
ReadOnlySpan<double> vols = series.VolumeValues;
Assert.Equal(2, vols.Length);
Assert.Equal(100.0, vols[0]);
Assert.Equal(200.0, vols[1]);
}
[Fact]
public void SpanAccessors_EmptySeries_AllReturnEmptySpans()
{
var series = new TBarSeries();
Assert.Equal(0, series.OpenValues.Length);
Assert.Equal(0, series.HighValues.Length);
Assert.Equal(0, series.LowValues.Length);
Assert.Equal(0, series.CloseValues.Length);
Assert.Equal(0, series.VolumeValues.Length);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: AddRange with 6 span parameters
// ────────────────────────────────────────────────────────────────────
[Fact]
public void AddRange_WithSpans_AddsAllBars()
{
var series = new TBarSeries();
long[] times = [100, 200, 300];
double[] opens = [10, 20, 30];
double[] highs = [15, 25, 35];
double[] lows = [5, 15, 25];
double[] closes = [12, 22, 32];
double[] volumes = [1000, 2000, 3000];
series.AddRange(times, opens, highs, lows, closes, volumes);
Assert.Equal(3, series.Count);
Assert.Equal(100, series[0].Time);
Assert.Equal(10.0, series[0].Open);
Assert.Equal(15.0, series[0].High);
Assert.Equal(5.0, series[0].Low);
Assert.Equal(12.0, series[0].Close);
Assert.Equal(1000.0, series[0].Volume);
Assert.Equal(300, series[2].Time);
Assert.Equal(32.0, series[2].Close);
}
[Fact]
public void AddRange_WithSpans_EmptySpans_DoesNothing()
{
var series = new TBarSeries();
ReadOnlySpan<long> t = ReadOnlySpan<long>.Empty;
ReadOnlySpan<double> d = ReadOnlySpan<double>.Empty;
series.AddRange(t, d, d, d, d, d);
Assert.Empty(series);
}
[Fact]
public void AddRange_WithSpans_MismatchedLengths_ThrowsArgumentException()
{
var series = new TBarSeries();
long[] times = [100, 200, 300];
double[] opens = [10, 20]; // Mismatched
double[] highs = [15, 25, 35];
double[] lows = [5, 15, 25];
double[] closes = [12, 22, 32];
double[] volumes = [1000, 2000, 3000];
Assert.Throws<ArgumentException>(() =>
series.AddRange(times, opens, highs, lows, closes, volumes));
}
[Fact]
public void AddRange_WithSpans_DoesNotFirePubEvent()
{
var series = new TBarSeries();
int pubCount = 0;
series.Pub += (object? sender, in TBarEventArgs args) => pubCount++;
long[] times = [100, 200, 300];
double[] opens = [10, 20, 30];
double[] highs = [15, 25, 35];
double[] lows = [5, 15, 25];
double[] closes = [12, 22, 32];
double[] volumes = [1000, 2000, 3000];
series.AddRange(times, opens, highs, lows, closes, volumes);
Assert.Equal(0, pubCount);
Assert.Equal(3, series.Count);
}
[Fact]
public void AddRange_WithSpans_AppendsToExistingData()
{
var series = new TBarSeries();
series.Add(50, 5, 8, 3, 6, 500);
long[] times = [100, 200];
double[] opens = [10, 20];
double[] highs = [15, 25];
double[] lows = [5, 15];
double[] closes = [12, 22];
double[] volumes = [1000, 2000];
series.AddRange(times, opens, highs, lows, closes, volumes);
Assert.Equal(3, series.Count);
Assert.Equal(50, series[0].Time);
Assert.Equal(100, series[1].Time);
Assert.Equal(200, series[2].Time);
}
[Fact]
public void AddRange_WithSpans_SubSeriesReflectData()
{
var series = new TBarSeries();
long[] times = [100, 200];
double[] opens = [10, 20];
double[] highs = [15, 25];
double[] lows = [5, 15];
double[] closes = [12, 22];
double[] volumes = [1000, 2000];
series.AddRange(times, opens, highs, lows, closes, volumes);
// Sub-series share underlying storage, so they reflect AddRange data
Assert.Equal(2, series.Open.Count);
Assert.Equal(10.0, series.Open[0].Value);
Assert.Equal(20.0, series.Open[1].Value);
Assert.Equal(22.0, series.Close[1].Value);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: AddRange with ReadOnlySpan<TBar>
// ────────────────────────────────────────────────────────────────────
[Fact]
public void AddRange_WithTBarSpan_AddsAllBars()
{
var series = new TBarSeries();
TBar[] bars =
[
new TBar(100, 10, 15, 5, 12, 1000),
new TBar(200, 20, 25, 15, 22, 2000),
new TBar(300, 30, 35, 25, 32, 3000),
];
series.AddRange(bars);
Assert.Equal(3, series.Count);
Assert.Equal(100, series[0].Time);
Assert.Equal(12.0, series[0].Close);
Assert.Equal(300, series[2].Time);
Assert.Equal(32.0, series[2].Close);
Assert.Equal(3000.0, series[2].Volume);
}
[Fact]
public void AddRange_WithTBarSpan_EmptySpan_DoesNothing()
{
var series = new TBarSeries();
series.AddRange(ReadOnlySpan<TBar>.Empty);
Assert.Empty(series);
}
[Fact]
public void AddRange_WithTBarSpan_AppendsToExistingData()
{
var series = new TBarSeries();
series.Add(50, 5, 8, 3, 6, 500);
TBar[] bars =
[
new TBar(100, 10, 15, 5, 12, 1000),
new TBar(200, 20, 25, 15, 22, 2000),
];
series.AddRange(bars);
Assert.Equal(3, series.Count);
Assert.Equal(50, series[0].Time);
Assert.Equal(100, series[1].Time);
Assert.Equal(200, series[2].Time);
}
[Fact]
public void AddRange_WithTBarSpan_DoesNotFirePubEvent()
{
var series = new TBarSeries();
int pubCount = 0;
series.Pub += (object? sender, in TBarEventArgs args) => pubCount++;
TBar[] bars = [new TBar(100, 10, 15, 5, 12, 1000)];
series.AddRange(bars);
Assert.Equal(0, pubCount);
Assert.Single(series);
}
[Fact]
public void AddRange_WithTBarSpan_CorrectlySplitsToSoA()
{
var series = new TBarSeries();
TBar[] bars =
[
new TBar(100, 10, 15, 5, 12, 1000),
new TBar(200, 20, 25, 15, 22, 2000),
];
series.AddRange(bars);
// Verify SoA layout via span accessors
Assert.Equal(100, series.Times[0]);
Assert.Equal(200, series.Times[1]);
Assert.Equal(10.0, series.OpenValues[0]);
Assert.Equal(20.0, series.OpenValues[1]);
Assert.Equal(15.0, series.HighValues[0]);
Assert.Equal(25.0, series.HighValues[1]);
Assert.Equal(5.0, series.LowValues[0]);
Assert.Equal(15.0, series.LowValues[1]);
Assert.Equal(12.0, series.CloseValues[0]);
Assert.Equal(22.0, series.CloseValues[1]);
Assert.Equal(1000.0, series.VolumeValues[0]);
Assert.Equal(2000.0, series.VolumeValues[1]);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: TBarEventArgs struct
// ────────────────────────────────────────────────────────────────────
[Fact]
public void TBarEventArgs_Equals_SameValues_ReturnsTrue()
{
var bar = new TBar(100, 10, 15, 5, 12, 1000);
var a = new TBarEventArgs { Value = bar, IsNew = true };
var b = new TBarEventArgs { Value = bar, IsNew = true };
Assert.True(a.Equals(b));
Assert.True(a == b);
Assert.False(a != b);
}
[Fact]
public void TBarEventArgs_Equals_DifferentIsNew_ReturnsFalse()
{
var bar = new TBar(100, 10, 15, 5, 12, 1000);
var a = new TBarEventArgs { Value = bar, IsNew = true };
var b = new TBarEventArgs { Value = bar, IsNew = false };
Assert.False(a.Equals(b));
Assert.False(a == b);
Assert.True(a != b);
}
[Fact]
public void TBarEventArgs_Equals_DifferentValue_ReturnsFalse()
{
var bar1 = new TBar(100, 10, 15, 5, 12, 1000);
var bar2 = new TBar(200, 20, 25, 15, 22, 2000);
var a = new TBarEventArgs { Value = bar1, IsNew = true };
var b = new TBarEventArgs { Value = bar2, IsNew = true };
Assert.False(a.Equals(b));
}
[Fact]
public void TBarEventArgs_Equals_Object_SameValues_ReturnsTrue()
{
var bar = new TBar(100, 10, 15, 5, 12, 1000);
var a = new TBarEventArgs { Value = bar, IsNew = true };
object b = new TBarEventArgs { Value = bar, IsNew = true };
Assert.True(a.Equals(b));
}
[Fact]
public void TBarEventArgs_Equals_Object_DifferentType_ReturnsFalse()
{
var bar = new TBar(100, 10, 15, 5, 12, 1000);
var a = new TBarEventArgs { Value = bar, IsNew = true };
Assert.False(a.Equals("not a TBarEventArgs"));
Assert.False(a.Equals(null));
}
[Fact]
public void TBarEventArgs_GetHashCode_EqualObjects_SameHash()
{
var bar = new TBar(100, 10, 15, 5, 12, 1000);
var a = new TBarEventArgs { Value = bar, IsNew = true };
var b = new TBarEventArgs { Value = bar, IsNew = true };
Assert.Equal(a.GetHashCode(), b.GetHashCode());
}
[Fact]
public void TBarEventArgs_GetHashCode_DifferentObjects_LikelyDifferentHash()
{
var bar1 = new TBar(100, 10, 15, 5, 12, 1000);
var bar2 = new TBar(200, 20, 25, 15, 22, 2000);
var a = new TBarEventArgs { Value = bar1, IsNew = true };
var b = new TBarEventArgs { Value = bar2, IsNew = false };
// Not guaranteed but extremely likely for distinct values
Assert.NotEqual(a.GetHashCode(), b.GetHashCode());
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: TBarSeriesEnumerator struct
// ────────────────────────────────────────────────────────────────────
[Fact]
public void TBarSeriesEnumerator_Reset_AllowsReIteration()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
var enumerator = series.GetEnumerator();
// First pass
int count1 = 0;
while (enumerator.MoveNext()) { count1++; }
Assert.Equal(2, count1);
// Reset and re-iterate
enumerator.Reset();
int count2 = 0;
while (enumerator.MoveNext()) { count2++; }
Assert.Equal(2, count2);
}
[Fact]
public void TBarSeriesEnumerator_Dispose_DoesNotThrow()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
var enumerator = series.GetEnumerator();
enumerator.MoveNext();
enumerator.Dispose(); // Should be no-op
Assert.Equal(12.0, enumerator.Current.Close);
}
[Fact]
public void TBarSeriesEnumerator_Equals_SameState_ReturnsTrue()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
var a = series.GetEnumerator();
var b = series.GetEnumerator();
// Both at initial state (_index = -1)
Assert.True(a.Equals(b));
Assert.True(a == b);
Assert.False(a != b);
}
[Fact]
public void TBarSeriesEnumerator_Equals_DifferentState_ReturnsFalse()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
var a = series.GetEnumerator();
var b = series.GetEnumerator();
a.MoveNext(); // a is at index 0, b is at -1
Assert.False(a.Equals(b));
Assert.False(a == b);
Assert.True(a != b);
}
[Fact]
public void TBarSeriesEnumerator_Equals_Object_SameState_ReturnsTrue()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
var a = series.GetEnumerator();
object b = series.GetEnumerator();
Assert.True(a.Equals(b));
}
[Fact]
public void TBarSeriesEnumerator_Equals_Object_DifferentType_ReturnsFalse()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
var a = series.GetEnumerator();
Assert.False(a.Equals("not an enumerator"));
Assert.False(a.Equals(null));
}
[Fact]
public void TBarSeriesEnumerator_GetHashCode_SameState_SameHash()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
var a = series.GetEnumerator();
var b = series.GetEnumerator();
Assert.Equal(a.GetHashCode(), b.GetHashCode());
}
[Fact]
public void TBarSeriesEnumerator_GetHashCode_DifferentState_LikelyDifferentHash()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
var a = series.GetEnumerator();
var b = series.GetEnumerator();
a.MoveNext();
Assert.NotEqual(a.GetHashCode(), b.GetHashCode());
}
[Fact]
public void TBarSeriesEnumerator_Current_ViaIEnumerator_ReturnsBoxedTBar()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
IEnumerator enumerator = series.GetEnumerator();
enumerator.MoveNext();
object current = enumerator.Current;
Assert.IsType<TBar>(current);
Assert.Equal(12.0, ((TBar)current).Close);
}
[Fact]
public void TBarSeriesEnumerator_DifferentSeries_NotEqual()
{
var series1 = new TBarSeries();
series1.Add(100, 10, 15, 5, 12, 100);
var series2 = new TBarSeries();
series2.Add(100, 10, 15, 5, 12, 100);
var a = series1.GetEnumerator();
var b = series2.GetEnumerator();
// Different underlying list references -> not equal
Assert.False(a.Equals(b));
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: AddRange large data set (capacity pre-alloc path)
// ────────────────────────────────────────────────────────────────────
[Fact]
public void AddRange_LargeDataSet_AllBarsAccessible()
{
var series = new TBarSeries();
const int N = 1000;
var times = new long[N];
var opens = new double[N];
var highs = new double[N];
var lows = new double[N];
var closes = new double[N];
var volumes = new double[N];
for (int i = 0; i < N; i++)
{
times[i] = i;
opens[i] = i * 10.0;
highs[i] = i * 10.0 + 5.0;
lows[i] = i * 10.0 - 5.0;
closes[i] = i * 10.0 + 2.0;
volumes[i] = i * 100.0;
}
series.AddRange(times, opens, highs, lows, closes, volumes);
Assert.Equal(N, series.Count);
Assert.Equal(0, series[0].Time);
Assert.Equal((N - 1) * 10.0 + 2.0, series[N - 1].Close);
Assert.Equal((N - 1) * 100.0, series[N - 1].Volume);
}
[Fact]
public void AddRange_TBarSpan_LargeDataSet_AllBarsAccessible()
{
var series = new TBarSeries();
const int N = 500;
var bars = new TBar[N];
for (int i = 0; i < N; i++)
{
bars[i] = new TBar(i, i * 10.0, i * 10.0 + 5.0, i * 10.0 - 5.0, i * 10.0 + 2.0, i * 100.0);
}
series.AddRange(bars);
Assert.Equal(N, series.Count);
Assert.Equal(0, series[0].Time);
Assert.Equal(2.0, series[0].Close);
Assert.Equal((N - 1) * 10.0 + 2.0, series[N - 1].Close);
}
}
+208
View File
@@ -583,4 +583,212 @@ public class TSeriesTests
Assert.Equal(1.0, values[0]);
Assert.Equal(2.0, values[1]);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: ShareStorageTag struct
// ────────────────────────────────────────────────────────────────────
[Fact]
public void ShareStorageTag_Instance_IsDefault()
{
var tag = ShareStorageTag.Instance;
// It's a readonly struct with no fields — default should work
Assert.Equal(default(ShareStorageTag), tag);
}
[Fact]
public void ShareStorageTag_TwoInstances_AreEqual()
{
var a = ShareStorageTag.Instance;
var b = ShareStorageTag.Instance;
Assert.Equal(a, b);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: TSeriesEnumerator struct
// ────────────────────────────────────────────────────────────────────
[Fact]
public void TSeriesEnumerator_Reset_AllowsReIteration()
{
var series = new TSeries();
series.Add(100, 1.0);
series.Add(200, 2.0);
var enumerator = series.GetEnumerator();
// First pass
int count1 = 0;
while (enumerator.MoveNext()) { count1++; }
Assert.Equal(2, count1);
// Reset and re-iterate
enumerator.Reset();
int count2 = 0;
while (enumerator.MoveNext()) { count2++; }
Assert.Equal(2, count2);
}
[Fact]
public void TSeriesEnumerator_Dispose_DoesNotThrow()
{
var series = new TSeries();
series.Add(100, 1.0);
var enumerator = series.GetEnumerator();
enumerator.MoveNext();
enumerator.Dispose(); // Should be no-op
Assert.Equal(1.0, enumerator.Current.Value);
}
[Fact]
public void TSeriesEnumerator_Equals_SameState_ReturnsTrue()
{
var series = new TSeries();
series.Add(100, 1.0);
var a = series.GetEnumerator();
var b = series.GetEnumerator();
// Both at initial state (_index = -1)
Assert.True(a.Equals(b));
Assert.True(a == b);
Assert.False(a != b);
}
[Fact]
public void TSeriesEnumerator_Equals_DifferentState_ReturnsFalse()
{
var series = new TSeries();
series.Add(100, 1.0);
series.Add(200, 2.0);
var a = series.GetEnumerator();
var b = series.GetEnumerator();
a.MoveNext(); // a is at index 0, b is at -1
Assert.False(a.Equals(b));
Assert.False(a == b);
Assert.True(a != b);
}
[Fact]
public void TSeriesEnumerator_Equals_Object_SameState_ReturnsTrue()
{
var series = new TSeries();
series.Add(100, 1.0);
var a = series.GetEnumerator();
object b = series.GetEnumerator();
Assert.True(a.Equals(b));
}
[Fact]
public void TSeriesEnumerator_Equals_Object_DifferentType_ReturnsFalse()
{
var series = new TSeries();
series.Add(100, 1.0);
var a = series.GetEnumerator();
Assert.False(a.Equals("not an enumerator"));
Assert.False(a.Equals(null));
}
[Fact]
public void TSeriesEnumerator_GetHashCode_SameState_SameHash()
{
var series = new TSeries();
series.Add(100, 1.0);
var a = series.GetEnumerator();
var b = series.GetEnumerator();
Assert.Equal(a.GetHashCode(), b.GetHashCode());
}
[Fact]
public void TSeriesEnumerator_GetHashCode_DifferentState_LikelyDifferentHash()
{
var series = new TSeries();
series.Add(100, 1.0);
series.Add(200, 2.0);
var a = series.GetEnumerator();
var b = series.GetEnumerator();
a.MoveNext();
Assert.NotEqual(a.GetHashCode(), b.GetHashCode());
}
[Fact]
public void TSeriesEnumerator_Current_ViaIEnumerator_ReturnsBoxedTValue()
{
var series = new TSeries();
series.Add(100, 42.0);
IEnumerator enumerator = series.GetEnumerator();
enumerator.MoveNext();
object current = enumerator.Current;
Assert.IsType<TValue>(current);
Assert.Equal(42.0, ((TValue)current).Value);
}
[Fact]
public void TSeriesEnumerator_DifferentSeries_NotEqual()
{
var series1 = new TSeries();
series1.Add(100, 1.0);
var series2 = new TSeries();
series2.Add(100, 1.0);
var a = series1.GetEnumerator();
var b = series2.GetEnumerator();
// Different underlying list references -> not equal
Assert.False(a.Equals(b));
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: Constructor defensive copy verification
// ────────────────────────────────────────────────────────────────────
[Fact]
public void Constructor_WithReadOnlyLists_MakesDefensiveCopy()
{
var times = new List<long> { 100, 200 };
var values = new List<double> { 1.0, 2.0 };
// Use IReadOnlyList<T> constructor which makes defensive copies
var series = new TSeries((IReadOnlyList<long>)times, (IReadOnlyList<double>)values);
// Mutate original lists
times.Add(300);
values.Add(3.0);
// Series should NOT reflect the mutation (defensive copy was made)
Assert.Equal(2, series.Count);
}
[Fact]
public void Constructor_WithReadOnlyLists_OriginalMutationDoesNotAffectSeries()
{
long[] originalTimes = [100, 200, 300];
double[] originalValues = [1.0, 2.0, 3.0];
var series = new TSeries(originalTimes, originalValues);
// Mutate original arrays
originalValues[0] = 999.0;
// Series should NOT reflect the mutation (defensive copy was made)
Assert.Equal(1.0, series[0].Value);
}
}
+499 -1
View File
@@ -303,7 +303,7 @@ public class TValueTests
string result = tValue.ToString(null, CultureInfo.InvariantCulture);
Assert.Contains("", result, StringComparison.Ordinal);
Assert.Contains("\u221E", result, StringComparison.Ordinal);
}
[Fact]
@@ -373,4 +373,502 @@ public class TValueTests
Assert.Equal(long.MaxValue, tValue.Time);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: ToString() parameterless override
// ────────────────────────────────────────────────────────────────────
[Fact]
public void ToString_Parameterless_NormalValue_FormatsCorrectly()
{
var dt = new DateTime(2023, 6, 15, 14, 30, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, 42.0);
#pragma warning disable MA0011 // IFormatProvider is missing - intentionally testing parameterless overload
string result = tValue.ToString();
#pragma warning restore MA0011
Assert.StartsWith("[", result, StringComparison.Ordinal);
Assert.EndsWith("]", result, StringComparison.Ordinal);
Assert.Contains("2023-06-15", result, StringComparison.Ordinal);
Assert.Contains("14:30:00", result, StringComparison.Ordinal);
Assert.Contains("42.00", result, StringComparison.Ordinal);
}
[Fact]
public void ToString_Parameterless_PositiveInfinity_ContainsInfinitySymbol()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, double.PositiveInfinity);
#pragma warning disable MA0011
string result = tValue.ToString();
#pragma warning restore MA0011
Assert.Contains("\u221E", result, StringComparison.Ordinal);
}
[Fact]
public void ToString_Parameterless_NegativeInfinity_ContainsMinusInfinitySymbol()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, double.NegativeInfinity);
#pragma warning disable MA0011
string result = tValue.ToString();
#pragma warning restore MA0011
Assert.Contains("-\u221E", result, StringComparison.Ordinal);
}
[Fact]
public void ToString_Parameterless_NaN_ContainsNaN()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, double.NaN);
#pragma warning disable MA0011
string result = tValue.ToString();
#pragma warning restore MA0011
Assert.Contains("NaN", result, StringComparison.Ordinal);
}
[Fact]
public void ToString_Parameterless_ZeroValue_FormatsAsZero()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, 0.0);
#pragma warning disable MA0011
string result = tValue.ToString();
#pragma warning restore MA0011
Assert.Contains("0.00", result, StringComparison.Ordinal);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: ToString(string?, IFormatProvider?) — NotSupportedException
// ────────────────────────────────────────────────────────────────────
[Fact]
public void ToString_WithCustomFormat_ThrowsNotSupportedException()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0);
Assert.Throws<NotSupportedException>(() => tValue.ToString("F4", CultureInfo.InvariantCulture));
}
[Fact]
public void ToString_WithNonEmptyFormat_ThrowsNotSupportedException()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0);
var ex = Assert.Throws<NotSupportedException>(() => tValue.ToString("G", null));
Assert.Contains("Custom format", ex.Message, StringComparison.Ordinal);
Assert.Contains("'G'", ex.Message, StringComparison.Ordinal);
}
[Fact]
public void ToString_WithNullFormat_DoesNotThrow()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, 42.0);
string result = tValue.ToString(null, null);
Assert.Contains("42.00", result, StringComparison.Ordinal);
}
[Fact]
public void ToString_WithEmptyFormat_DoesNotThrow()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, 42.0);
string result = tValue.ToString("", null);
Assert.Contains("42.00", result, StringComparison.Ordinal);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: TryFormat — allocation-free span formatting
// ────────────────────────────────────────────────────────────────────
[Fact]
public void TryFormat_NormalValue_FormatsCorrectly()
{
var dt = new DateTime(2023, 6, 15, 14, 30, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, 42.0);
Span<char> buffer = stackalloc char[128];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, CultureInfo.InvariantCulture);
Assert.True(result);
Assert.True(charsWritten > 0);
string formatted = new string(buffer.Slice(0, charsWritten));
Assert.StartsWith("[", formatted, StringComparison.Ordinal);
Assert.EndsWith("]", formatted, StringComparison.Ordinal);
Assert.Contains("2023-06-15", formatted, StringComparison.Ordinal);
Assert.Contains("14:30:00", formatted, StringComparison.Ordinal);
Assert.Contains("42.00", formatted, StringComparison.Ordinal);
}
[Fact]
public void TryFormat_PositiveInfinity_FormatsWithSymbol()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, double.PositiveInfinity);
Span<char> buffer = stackalloc char[128];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.True(result);
string formatted = new string(buffer.Slice(0, charsWritten));
Assert.Contains("\u221E", formatted, StringComparison.Ordinal);
Assert.StartsWith("[", formatted, StringComparison.Ordinal);
Assert.EndsWith("]", formatted, StringComparison.Ordinal);
}
[Fact]
public void TryFormat_NegativeInfinity_FormatsWithMinusSymbol()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, double.NegativeInfinity);
Span<char> buffer = stackalloc char[128];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.True(result);
string formatted = new string(buffer.Slice(0, charsWritten));
Assert.Contains("-\u221E", formatted, StringComparison.Ordinal);
}
[Fact]
public void TryFormat_NaN_FormatsAsNaN()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, double.NaN);
Span<char> buffer = stackalloc char[128];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.True(result);
string formatted = new string(buffer.Slice(0, charsWritten));
Assert.Contains("NaN", formatted, StringComparison.Ordinal);
}
[Fact]
public void TryFormat_BufferTooSmall_ReturnsFalse()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0);
Span<char> buffer = stackalloc char[10]; // Way too small (< 24)
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.False(result);
Assert.Equal(0, charsWritten);
}
[Fact]
public void TryFormat_BufferExactlyTooSmall_ReturnsFalse()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0);
Span<char> buffer = stackalloc char[23]; // One less than minimum (24)
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.False(result);
Assert.Equal(0, charsWritten);
}
[Fact]
public void TryFormat_BufferBarelyLargeEnough_ForShortValue()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
// NaN is shortest value format (3 chars): "[yyyy-MM-dd HH:mm:ss, NaN]" = 26 chars
var tValue = new TValue(dt.Ticks, double.NaN);
Span<char> buffer = stackalloc char[26];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.True(result);
Assert.Equal(26, charsWritten);
}
[Fact]
public void TryFormat_BufferTooSmallForSeparator_ReturnsFalse()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, 42.0);
// Buffer: 1 (opening bracket) + 19 (datetime) + 1 = 21 (too small for ", ")
Span<char> buffer = stackalloc char[21];
// This should fail because the initial < 24 check triggers first
bool result = tValue.TryFormat(buffer, out _, ReadOnlySpan<char>.Empty, null);
Assert.False(result);
}
[Fact]
public void TryFormat_BufferTooSmallForClosingBracket_ReturnsFalse()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
// ∞ is 1 char: "[yyyy-MM-dd HH:mm:ss, ∞]" = 24 chars
var tValue = new TValue(dt.Ticks, double.PositiveInfinity);
// Need exactly 24 chars: 1 + 19 + 2 + 1 + 1 = 24
// Providing 23 — enough to pass the initial check but too small for final ']'
// Actually the initial check is < 24, so 23 fails there.
// Let's use 24 which is exactly enough for ∞ case
Span<char> buffer = stackalloc char[24];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.True(result);
Assert.Equal(24, charsWritten);
}
[Fact]
public void TryFormat_LargeNegativeValue_FormatsCorrectly()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, -99999.99);
Span<char> buffer = stackalloc char[128];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, CultureInfo.InvariantCulture);
Assert.True(result);
string formatted = new string(buffer.Slice(0, charsWritten));
Assert.Contains("-99999.99", formatted, StringComparison.Ordinal);
}
[Fact]
public void TryFormat_ZeroValue_FormatsAsZero()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, 0.0);
Span<char> buffer = stackalloc char[128];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, CultureInfo.InvariantCulture);
Assert.True(result);
string formatted = new string(buffer.Slice(0, charsWritten));
Assert.Contains("0.00", formatted, StringComparison.Ordinal);
}
[Fact]
public void TryFormat_ConsistentWithToString()
{
var dt = new DateTime(2023, 6, 15, 14, 30, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, 123.456);
#pragma warning disable MA0011
string fromToString = tValue.ToString();
#pragma warning restore MA0011
Span<char> buffer = stackalloc char[128];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.True(result);
string fromTryFormat = new string(buffer.Slice(0, charsWritten));
Assert.Equal(fromToString, fromTryFormat);
}
[Fact]
public void TryFormat_NaN_ConsistentWithToString()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, double.NaN);
#pragma warning disable MA0011
string fromToString = tValue.ToString();
#pragma warning restore MA0011
Span<char> buffer = stackalloc char[128];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.True(result);
string fromTryFormat = new string(buffer.Slice(0, charsWritten));
Assert.Equal(fromToString, fromTryFormat);
}
[Fact]
public void TryFormat_PositiveInfinity_ConsistentWithToString()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, double.PositiveInfinity);
#pragma warning disable MA0011
string fromToString = tValue.ToString();
#pragma warning restore MA0011
Span<char> buffer = stackalloc char[128];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.True(result);
string fromTryFormat = new string(buffer.Slice(0, charsWritten));
Assert.Equal(fromToString, fromTryFormat);
}
[Fact]
public void TryFormat_NegativeInfinity_ConsistentWithToString()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, double.NegativeInfinity);
#pragma warning disable MA0011
string fromToString = tValue.ToString();
#pragma warning restore MA0011
Span<char> buffer = stackalloc char[128];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.True(result);
string fromTryFormat = new string(buffer.Slice(0, charsWritten));
Assert.Equal(fromToString, fromTryFormat);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: Record struct features (with expression, Deconstruct)
// ────────────────────────────────────────────────────────────────────
[Fact]
public void WithExpression_CreatesModifiedCopy()
{
var original = new TValue(12345, 100.0);
var modified = original with { Value = 200.0 };
Assert.Equal(12345, modified.Time);
Assert.Equal(200.0, modified.Value);
Assert.Equal(100.0, original.Value); // Original unchanged
}
[Fact]
public void WithExpression_TimeModified_CreatesModifiedCopy()
{
var original = new TValue(12345, 100.0);
var modified = original with { Time = 99999 };
Assert.Equal(99999, modified.Time);
Assert.Equal(100.0, modified.Value);
}
[Fact]
public void Deconstruct_ExtractsTimeAndValue()
{
var tValue = new TValue(12345, 42.0);
var (time, value) = tValue;
Assert.Equal(12345, time);
Assert.Equal(42.0, value);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: ISpanFormattable interface contract
// ────────────────────────────────────────────────────────────────────
[Fact]
public void ISpanFormattable_ImplementedCorrectly()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0);
// Verify TValue implements ISpanFormattable
Assert.IsAssignableFrom<ISpanFormattable>(tValue);
}
[Fact]
public void IFormattable_ImplementedCorrectly()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0);
// ISpanFormattable extends IFormattable
Assert.IsAssignableFrom<IFormattable>(tValue);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: ToString with negative infinity (was missing)
// ────────────────────────────────────────────────────────────────────
[Fact]
public void ToString_WithNegativeInfinity_FormatsCorrectly()
{
var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, double.NegativeInfinity);
string result = tValue.ToString(null, CultureInfo.InvariantCulture);
Assert.Contains("-\u221E", result, StringComparison.Ordinal);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: Explicit double conversion edge cases
// ────────────────────────────────────────────────────────────────────
[Fact]
public void ExplicitConversion_ToDouble_WithPositiveInfinity_ReturnsInfinity()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, double.PositiveInfinity);
double val = (double)tValue;
Assert.True(double.IsPositiveInfinity(val));
}
[Fact]
public void ExplicitConversion_ToDouble_WithNegativeInfinity_ReturnsNegativeInfinity()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, double.NegativeInfinity);
double val = (double)tValue;
Assert.True(double.IsNegativeInfinity(val));
}
[Fact]
public void ExplicitConversion_ToDouble_WithZero_ReturnsZero()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, 0.0);
double val = (double)tValue;
Assert.Equal(0.0, val);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: TryFormat buffer edge cases for -∞ and NaN
// ────────────────────────────────────────────────────────────────────
[Fact]
public void TryFormat_NegativeInfinity_ExactBuffer()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
// "-∞" is 2 chars: "[yyyy-MM-dd HH:mm:ss, -∞]" = 25 chars
var tValue = new TValue(dt.Ticks, double.NegativeInfinity);
Span<char> buffer = stackalloc char[25];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.True(result);
Assert.Equal(25, charsWritten);
}
[Fact]
public void TryFormat_EmptyBuffer_ReturnsFalse()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0);
Span<char> buffer = Span<char>.Empty;
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.False(result);
Assert.Equal(0, charsWritten);
}
}