Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
236 changes: 236 additions & 0 deletions Thinksharp.TimeFlow.Test/TimeSeriesInPlaceTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
namespace Thinksharp.TimeFlow
{
using System;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class TimeSeriesInPlaceTest
{
[TestMethod]
public void TestApply_InPlace_ModifiesOriginalInstance()
{
// Arrange
var ts = TimeSeries.Factory.FromValue(10, new DateTime(2023, 1, 1), 5, Period.Day);
var originalRef = ts;

// Act
var result = ts.Apply(x => x * 2, inPlace: true);

// Assert
Assert.AreSame(originalRef, result, "Should return the same instance");
Assert.IsTrue(ts.All(x => x.Value == 20), "All values should be doubled");
}

[TestMethod]
public void TestApply_InPlace_False_CreatesNewInstance()
{
// Arrange
var ts = TimeSeries.Factory.FromValue(10, new DateTime(2023, 1, 1), 5, Period.Day);
var originalRef = ts;

// Act
var result = ts.Apply(x => x * 2, inPlace: false);

// Assert
Assert.AreNotSame(originalRef, result, "Should return a different instance");
Assert.IsTrue(ts.All(x => x.Value == 10), "Original should be unchanged");
Assert.IsTrue(result.All(x => x.Value == 20), "Result should have doubled values");
}

[TestMethod]
public void TestApply_DefaultBehavior_CreatesNewInstance()
{
// Arrange
var ts = TimeSeries.Factory.FromValue(10, new DateTime(2023, 1, 1), 5, Period.Day);
var originalRef = ts;

// Act
var result = ts.Apply(x => x * 2); // No inPlace parameter

// Assert
Assert.AreNotSame(originalRef, result, "Should return a different instance by default");
Assert.IsTrue(ts.All(x => x.Value == 10), "Original should be unchanged");
Assert.IsTrue(result.All(x => x.Value == 20), "Result should have doubled values");
}

[TestMethod]
public void TestApplyValues_InPlace_ModifiesOriginalInstance()
{
// Arrange
var ts = TimeSeries.Factory.FromValue(10, new DateTime(2023, 1, 1), 5, Period.Day);
var originalRef = ts;

// Act
var result = ts.ApplyValues(x => x + 5, inPlace: true);

// Assert
Assert.AreSame(originalRef, result, "Should return the same instance");
Assert.IsTrue(ts.All(x => x.Value == 15), "All values should be incremented by 5");
}

[TestMethod]
public void TestJoinLeft_InPlace_ModifiesOriginalInstance()
{
// Arrange
var ts1 = TimeSeries.Factory.FromValue(10, new DateTime(2023, 1, 1), 5, Period.Day);
var ts2 = TimeSeries.Factory.FromValue(3, new DateTime(2023, 1, 1), 5, Period.Day);
var originalRef = ts1;

// Act
var result = ts1.JoinLeft(ts2, (left, right) => left + right, inPlace: true);

// Assert
Assert.AreSame(originalRef, result, "Should return the same instance");
Assert.IsTrue(ts1.All(x => x.Value == 13), "All values should be sum of left and right");
}

[TestMethod]
public void TestJoinLeft_WithJoinOperation_InPlace_ModifiesOriginalInstance()
{
// Arrange
var ts1 = TimeSeries.Factory.FromValue(10, new DateTime(2023, 1, 1), 5, Period.Day);
var ts2 = TimeSeries.Factory.FromValue(2, new DateTime(2023, 1, 1), 5, Period.Day);
var originalRef = ts1;

// Act
var result = ts1.JoinLeft(ts2, JoinOperation.Multiply, inPlace: true);

// Assert
Assert.AreSame(originalRef, result, "Should return the same instance");
Assert.IsTrue(ts1.All(x => x.Value == 20), "All values should be multiplied");
}

[TestMethod]
public void TestSlice_InPlace_ModifiesOriginalInstance()
{
// Arrange
var ts = TimeSeries.Factory.FromValue(10, new DateTime(2023, 1, 1), 10, Period.Day);
var originalRef = ts;
var originalCount = ts.Count;

// Act
var result = ts.Slice(2, 5, inPlace: true);

// Assert
Assert.AreSame(originalRef, result, "Should return the same instance");
Assert.AreEqual(5, ts.Count, "Should have 5 elements after slice");
Assert.AreNotEqual(originalCount, ts.Count, "Count should have changed");
Assert.AreEqual(new DateTime(2023, 1, 3), ts.Start.DateTime, "Start should be third day");
Assert.AreEqual(new DateTime(2023, 1, 7), ts.End.DateTime, "End should be seventh day");
}

[TestMethod]
public void TestConvenienceMethods_AddInPlace()
{
// Arrange
var ts = TimeSeries.Factory.FromValue(10, new DateTime(2023, 1, 1), 5, Period.Day);
var originalRef = ts;

// Act
var result = ts.AddInPlace(5);

// Assert
Assert.AreSame(originalRef, result, "Should return the same instance");
Assert.IsTrue(ts.All(x => x.Value == 15), "All values should be incremented by 5");
}

[TestMethod]
public void TestConvenienceMethods_MultiplyInPlace()
{
// Arrange
var ts = TimeSeries.Factory.FromValue(10, new DateTime(2023, 1, 1), 5, Period.Day);
var originalRef = ts;

// Act
var result = ts.MultiplyInPlace(3);

// Assert
Assert.AreSame(originalRef, result, "Should return the same instance");
Assert.IsTrue(ts.All(x => x.Value == 30), "All values should be multiplied by 3");
}

[TestMethod]
public void TestConvenienceMethods_MethodChaining()
{
// Arrange
var ts = TimeSeries.Factory.FromValue(10, new DateTime(2023, 1, 1), 5, Period.Day);
var originalRef = ts;

// Act
var result = ts.AddInPlace(5).MultiplyInPlace(2).SubtractInPlace(10);

// Assert
Assert.AreSame(originalRef, result, "Should return the same instance");
Assert.IsTrue(ts.All(x => x.Value == 20), "Should apply all operations: (10+5)*2-10 = 20");
}

[TestMethod]
public void TestConvenienceMethods_AddInPlace_WithTimeSeries()
{
// Arrange
var ts1 = TimeSeries.Factory.FromValue(10, new DateTime(2023, 1, 1), 5, Period.Day);
var ts2 = TimeSeries.Factory.FromValue(7, new DateTime(2023, 1, 1), 5, Period.Day);
var originalRef = ts1;

// Act
var result = ts1.AddInPlace(ts2);

// Assert
Assert.AreSame(originalRef, result, "Should return the same instance");
Assert.IsTrue(ts1.All(x => x.Value == 17), "All values should be sum: 10 + 7 = 17");
}

[TestMethod]
public void TestInPlace_WithNullValues()
{
// Arrange
var values = new decimal?[] { 10, null, 20, null, 30 };
var ts = TimeSeries.Factory.FromValues(values, new DateTime(2023, 1, 1), Period.Day);
var originalRef = ts;

// Act
var result = ts.Apply(x => x.HasValue ? x * 2 : null, inPlace: true);

// Assert
Assert.AreSame(originalRef, result, "Should return the same instance");
Assert.AreEqual(20, ts[new DateTimeOffset(new DateTime(2023, 1, 1))]);
Assert.AreEqual(null, ts[new DateTimeOffset(new DateTime(2023, 1, 2))]);
Assert.AreEqual(40, ts[new DateTimeOffset(new DateTime(2023, 1, 3))]);
Assert.AreEqual(null, ts[new DateTimeOffset(new DateTime(2023, 1, 4))]);
Assert.AreEqual(60, ts[new DateTimeOffset(new DateTime(2023, 1, 5))]);
}

[TestMethod]
public void TestInPlace_PreservesFrequencyAndTimeZone()
{
// Arrange
var ts = TimeSeries.Factory.FromValue(10, new DateTime(2023, 1, 1), 5, Period.Hour);
var originalFrequency = ts.Frequency;
var originalTimeZone = ts.TimeZone;

// Act
ts.Apply(x => x * 2, inPlace: true);

// Assert
Assert.AreEqual(originalFrequency, ts.Frequency, "Frequency should be preserved");
Assert.AreEqual(originalTimeZone, ts.TimeZone, "TimeZone should be preserved");
}

[TestMethod]
public void TestInPlace_EmptyTimeSeries()
{
// Arrange
var ts = TimeSeries.Factory.Empty();
var originalRef = ts;

// Act
var result = ts.Apply(x => x * 2, inPlace: true);

// Assert
Assert.AreSame(originalRef, result, "Should return the same instance");
Assert.AreEqual(0, ts.Count, "Empty time series should remain empty");
Assert.IsTrue(ts.IsEmpty, "Should still be empty");
}
}
}
123 changes: 114 additions & 9 deletions Thinksharp.TimeFlow/IndexedSeries.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public abstract class IndexedSeries<TKey, TValue> : IReadOnlyList<IndexedSeriesI
{
protected readonly IDictionary<TKey, TValue> seriesDictionary = new Dictionary<TKey, TValue>();
protected readonly IList<IndexedSeriesItem<TKey, TValue>> sortedValues = new List<IndexedSeriesItem<TKey, TValue>>();

private bool allowMutation = false;

protected IndexedSeries(IEnumerable<IndexedSeriesItem<TKey, TValue>> sortedSeries)
{
Expand All @@ -25,28 +27,35 @@ protected IndexedSeries(IEnumerable<IndexedSeriesItem<TKey, TValue>> sortedSerie

if (this.sortedValues.Count > 0)
{
this.Start = this.sortedValues.First().Key;
this.End = this.sortedValues.Last().Key;
_start = this.sortedValues.First().Key;
_end = this.sortedValues.Last().Key;
}
else
{
this.Start = default(TKey);
this.End = default(TKey);
_start = default(TKey);
_end = default(TKey);
}

this.IsEmpty = this.sortedValues.Count == 0;
_isEmpty = this.sortedValues.Count == 0;
}

public TValue this[TKey key]
{
get => this.seriesDictionary.TryGetValue(key, out var value) ? value : default(TValue);
set => throw new NotSupportedException($"IT is not allowed to change {nameof(IndexedSeries<TKey, TValue>)} because it is an immutable data structure");
set
{
if (!allowMutation)
throw new NotSupportedException($"IT is not allowed to change {nameof(IndexedSeries<TKey, TValue>)} because it is an immutable data structure");

this.seriesDictionary[key] = value;
UpdateSortedValues();
}
}
public IndexedSeriesItem<TKey, TValue> this[int index] => sortedValues[index];

public TKey Start { get; }
public TKey End { get; }
public bool IsEmpty { get; }
public TKey Start => _start;
public TKey End => _end;
public bool IsEmpty => _isEmpty;
public int Count => this.sortedValues.Count;

public IEnumerable<TKey> Keys => this.seriesDictionary.Keys;
Expand All @@ -60,5 +69,101 @@ public TValue this[TKey key]
public bool TryGetValue(TKey key, out TValue value) => this.seriesDictionary.TryGetValue(key, out value);

IEnumerator IEnumerable.GetEnumerator() => this.sortedValues.GetEnumerator();

/// <summary>
/// Enables mutation for in-place operations. Should only be used by derived classes during controlled mutations.
/// </summary>
protected void EnableMutation()
{
allowMutation = true;
}

/// <summary>
/// Disables mutation after in-place operations complete.
/// </summary>
protected void DisableMutation()
{
allowMutation = false;
}

/// <summary>
/// Updates the internal data structures during in-place operations.
/// </summary>
protected void UpdateInPlace(IEnumerable<IndexedSeriesItem<TKey, TValue>> newSortedSeries)
{
if (!allowMutation)
throw new InvalidOperationException("Mutation is not enabled. Call EnableMutation() first.");

this.sortedValues.Clear();
this.seriesDictionary.Clear();

if (newSortedSeries is IList<IndexedSeriesItem<TKey, TValue>> sortedSeriesList)
{
foreach (var item in sortedSeriesList)
{
this.sortedValues.Add(item);
}
}
else
{
foreach (var item in newSortedSeries)
{
this.sortedValues.Add(item);
}
}

foreach (var item in this.sortedValues)
{
this.seriesDictionary[item.Key] = item.Value;
}

UpdateProperties();
}

/// <summary>
/// Updates the sorted values from the dictionary during in-place operations.
/// </summary>
private void UpdateSortedValues()
{
this.sortedValues.Clear();
foreach (var kvp in this.seriesDictionary.OrderBy(x => x.Key))
{
this.sortedValues.Add(new IndexedSeriesItem<TKey, TValue>(kvp.Key, kvp.Value));
}
UpdateProperties();
}

/// <summary>
/// Updates Start, End, and IsEmpty properties.
/// </summary>
private void UpdateProperties()
{
if (this.sortedValues.Count > 0)
{
SetStart(this.sortedValues.First().Key);
SetEnd(this.sortedValues.Last().Key);
}
else
{
SetStart(default(TKey));
SetEnd(default(TKey));
}

SetIsEmpty(this.sortedValues.Count == 0);
}

// These methods allow updating the readonly properties during in-place operations
private void SetStart(TKey value) => SetProperty(ref _start, value);
private void SetEnd(TKey value) => SetProperty(ref _end, value);
private void SetIsEmpty(bool value) => SetProperty(ref _isEmpty, value);

private void SetProperty<T>(ref T field, T value)
{
field = value;
}

private TKey _start;
private TKey _end;
private bool _isEmpty;
}
}
Loading