diff --git a/Thinksharp.TimeFlow.Test/TimeSeriesInPlaceTest.cs b/Thinksharp.TimeFlow.Test/TimeSeriesInPlaceTest.cs new file mode 100644 index 0000000..28e4679 --- /dev/null +++ b/Thinksharp.TimeFlow.Test/TimeSeriesInPlaceTest.cs @@ -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"); + } + } +} \ No newline at end of file diff --git a/Thinksharp.TimeFlow/IndexedSeries.cs b/Thinksharp.TimeFlow/IndexedSeries.cs index eeca4fe..9d0a082 100644 --- a/Thinksharp.TimeFlow/IndexedSeries.cs +++ b/Thinksharp.TimeFlow/IndexedSeries.cs @@ -9,6 +9,8 @@ public abstract class IndexedSeries : IReadOnlyList seriesDictionary = new Dictionary(); protected readonly IList> sortedValues = new List>(); + + private bool allowMutation = false; protected IndexedSeries(IEnumerable> sortedSeries) { @@ -25,28 +27,35 @@ protected IndexedSeries(IEnumerable> 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)} because it is an immutable data structure"); + set + { + if (!allowMutation) + throw new NotSupportedException($"IT is not allowed to change {nameof(IndexedSeries)} because it is an immutable data structure"); + + this.seriesDictionary[key] = value; + UpdateSortedValues(); + } } public IndexedSeriesItem 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 Keys => this.seriesDictionary.Keys; @@ -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(); + + /// + /// Enables mutation for in-place operations. Should only be used by derived classes during controlled mutations. + /// + protected void EnableMutation() + { + allowMutation = true; + } + + /// + /// Disables mutation after in-place operations complete. + /// + protected void DisableMutation() + { + allowMutation = false; + } + + /// + /// Updates the internal data structures during in-place operations. + /// + protected void UpdateInPlace(IEnumerable> newSortedSeries) + { + if (!allowMutation) + throw new InvalidOperationException("Mutation is not enabled. Call EnableMutation() first."); + + this.sortedValues.Clear(); + this.seriesDictionary.Clear(); + + if (newSortedSeries is IList> 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(); + } + + /// + /// Updates the sorted values from the dictionary during in-place operations. + /// + private void UpdateSortedValues() + { + this.sortedValues.Clear(); + foreach (var kvp in this.seriesDictionary.OrderBy(x => x.Key)) + { + this.sortedValues.Add(new IndexedSeriesItem(kvp.Key, kvp.Value)); + } + UpdateProperties(); + } + + /// + /// Updates Start, End, and IsEmpty properties. + /// + 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(ref T field, T value) + { + field = value; + } + + private TKey _start; + private TKey _end; + private bool _isEmpty; } } \ No newline at end of file diff --git a/Thinksharp.TimeFlow/TimeSeries.cs b/Thinksharp.TimeFlow/TimeSeries.cs index ae127ef..0323ae2 100644 --- a/Thinksharp.TimeFlow/TimeSeries.cs +++ b/Thinksharp.TimeFlow/TimeSeries.cs @@ -72,7 +72,42 @@ internal TimeSeries(IEnumerable> sor /// public TimeSeries Apply(Func func) { - return new TimeSeries(this.sortedValues.Select(x => new IndexedSeriesItem(x.Key, func(x.Value))), this.Frequency, this.TimeZone); + return Apply(func, inPlace: false); + } + + /// + /// Maps all values using the specified mapping function. + /// + /// + /// The mapping function to use. + /// + /// + /// If true, modifies this time series instance instead of creating a new one. Default is false. + /// + /// + /// A time series where all values are mapped using the specified mapping function. + /// If inPlace is true, returns this instance; otherwise returns a new instance. + /// + public TimeSeries Apply(Func func, bool inPlace = false) + { + if (inPlace) + { + this.EnableMutation(); + try + { + var newSortedSeries = this.sortedValues.Select(x => new IndexedSeriesItem(x.Key, func(x.Value))).ToList(); + this.UpdateInPlace(newSortedSeries); + return this; + } + finally + { + this.DisableMutation(); + } + } + else + { + return new TimeSeries(this.sortedValues.Select(x => new IndexedSeriesItem(x.Key, func(x.Value))), this.Frequency, this.TimeZone); + } } /// @@ -88,7 +123,42 @@ public TimeSeries Apply(Func func) /// public TimeSeries ApplyValues(Func func) { - return new TimeSeries(this.sortedValues.Select(x => new IndexedSeriesItem(x.Key, x.Value.HasValue ? (decimal?)func(x.Value.Value) : null)), this.Frequency, this.TimeZone); + return ApplyValues(func, inPlace: false); + } + + /// + /// Maps all values using the specified mapping function whereas null values remain as null values. + /// + /// + /// The mapping function to use for mapping non-nullable values. + /// + /// + /// If true, modifies this time series instance instead of creating a new one. Default is false. + /// + /// + /// A time series where all values are mapped using the specified mapping function whereas null values + /// remain as null values. If inPlace is true, returns this instance; otherwise returns a new instance. + /// + public TimeSeries ApplyValues(Func func, bool inPlace = false) + { + if (inPlace) + { + this.EnableMutation(); + try + { + var newSortedSeries = this.sortedValues.Select(x => new IndexedSeriesItem(x.Key, x.Value.HasValue ? (decimal?)func(x.Value.Value) : null)).ToList(); + this.UpdateInPlace(newSortedSeries); + return this; + } + finally + { + this.DisableMutation(); + } + } + else + { + return new TimeSeries(this.sortedValues.Select(x => new IndexedSeriesItem(x.Key, x.Value.HasValue ? (decimal?)func(x.Value.Value) : null)), this.Frequency, this.TimeZone); + } } /// @@ -106,20 +176,68 @@ public TimeSeries ApplyValues(Func func) /// A new time series with the same time points as this one but with values produced by the join. /// public TimeSeries JoinLeft(TimeSeries dateTimeSeries, Func agg) + { + return JoinLeft(dateTimeSeries, agg, inPlace: false); + } + + /// + /// Joins the specified time series to this time series using the specified aggregation function to combine the + /// values. + /// + /// + /// The time series to join. + /// + /// + /// The aggregation function to combine values from 2 equal time points. + /// The first value of the aggregation function is from this time series, the second value from the passed one. + /// + /// + /// If true, modifies this time series instance instead of creating a new one. Default is false. + /// + /// + /// A time series with the same time points as this one but with values produced by the join. + /// If inPlace is true, returns this instance; otherwise returns a new instance. + /// + public TimeSeries JoinLeft(TimeSeries dateTimeSeries, Func agg, bool inPlace = false) { var freq = EnsureFrequenciesAreCompatible(dateTimeSeries); var tz = EnsureTimeZonesAreCompatible(dateTimeSeries); - var result = new List>(); - foreach (var sortedValue in this.sortedValues) + if (inPlace) { - var leftValue = sortedValue.Value; - var rightValue = dateTimeSeries[sortedValue.Key]; + this.EnableMutation(); + try + { + var result = new List>(); + foreach (var sortedValue in this.sortedValues) + { + var leftValue = sortedValue.Value; + var rightValue = dateTimeSeries[sortedValue.Key]; - result.Add(new IndexedSeriesItem(sortedValue.Key, agg(leftValue, rightValue))); + result.Add(new IndexedSeriesItem(sortedValue.Key, agg(leftValue, rightValue))); + } + + this.UpdateInPlace(result); + return this; + } + finally + { + this.DisableMutation(); + } } + else + { + var result = new List>(); + foreach (var sortedValue in this.sortedValues) + { + var leftValue = sortedValue.Value; + var rightValue = dateTimeSeries[sortedValue.Key]; - return new TimeSeries(result, freq, tz); + result.Add(new IndexedSeriesItem(sortedValue.Key, agg(leftValue, rightValue))); + } + + return new TimeSeries(result, freq, tz); + } } /// @@ -139,20 +257,70 @@ public TimeSeries JoinLeft(TimeSeries dateTimeSeries, Func public TimeSeries JoinLeft(TimeSeries dateTimeSeries, Func agg) + { + return JoinLeft(dateTimeSeries, agg, inPlace: false); + } + + /// + /// Joins the specified time series to this time series using the specified aggregation function to combine the + /// values. + /// + /// + /// The time series to join. + /// + /// + /// The aggregation function to combine values from 2 equal time points. + /// 1. value: Timestamp of the time point to aggregate + /// 2. value: Value of the first time series. + /// 3. value: Value of the second time series. + /// + /// + /// If true, modifies this time series instance instead of creating a new one. Default is false. + /// + /// + /// A time series with the same time points as this one but with values produced by the join. + /// If inPlace is true, returns this instance; otherwise returns a new instance. + /// + public TimeSeries JoinLeft(TimeSeries dateTimeSeries, Func agg, bool inPlace = false) { var freq = EnsureFrequenciesAreCompatible(dateTimeSeries); var tz = EnsureTimeZonesAreCompatible(dateTimeSeries); - var result = new List>(); - foreach (var sortedValue in this.sortedValues) + if (inPlace) { - var leftValue = sortedValue.Value; - var rightValue = dateTimeSeries[sortedValue.Key]; + this.EnableMutation(); + try + { + var result = new List>(); + foreach (var sortedValue in this.sortedValues) + { + var leftValue = sortedValue.Value; + var rightValue = dateTimeSeries[sortedValue.Key]; - result.Add(new IndexedSeriesItem(sortedValue.Key, agg(sortedValue.Key, leftValue, rightValue))); + result.Add(new IndexedSeriesItem(sortedValue.Key, agg(sortedValue.Key, leftValue, rightValue))); + } + + this.UpdateInPlace(result); + return this; + } + finally + { + this.DisableMutation(); + } } + else + { + var result = new List>(); + foreach (var sortedValue in this.sortedValues) + { + var leftValue = sortedValue.Value; + var rightValue = dateTimeSeries[sortedValue.Key]; - return new TimeSeries(result, freq, tz); + result.Add(new IndexedSeriesItem(sortedValue.Key, agg(sortedValue.Key, leftValue, rightValue))); + } + + return new TimeSeries(result, freq, tz); + } } /// @@ -170,7 +338,29 @@ public TimeSeries JoinLeft(TimeSeries dateTimeSeries, Func public TimeSeries JoinLeft(TimeSeries dateTimeSeries, JoinOperation op) { - return this.JoinLeft(dateTimeSeries, op.Apply); + return JoinLeft(dateTimeSeries, op, inPlace: false); + } + + /// + /// Joins the specified time series to this time series using the specified aggregation function to combine the + /// values. + /// + /// + /// The time series to join. + /// + /// + /// The join operation to use. + /// + /// + /// If true, modifies this time series instance instead of creating a new one. Default is false. + /// + /// + /// A time series with the same time points as this one but with values produced by the join. + /// If inPlace is true, returns this instance; otherwise returns a new instance. + /// + public TimeSeries JoinLeft(TimeSeries dateTimeSeries, JoinOperation op, bool inPlace = false) + { + return this.JoinLeft(dateTimeSeries, op.Apply, inPlace); } /// @@ -744,7 +934,45 @@ public TimeSeries Slice(DateTimeOffset timestampFrom, Period period) /// public TimeSeries Slice(int startIndex, int count) { - return new TimeSeries(this.sortedValues.Skip(startIndex).Take(count), this.Frequency, this.TimeZone); + return Slice(startIndex, count, inPlace: false); + } + + /// + /// Creates a time series that contains only time points for the specified range. + /// + /// + /// The zero based start index. + /// + /// + /// The number of time points to get. + /// + /// + /// If true, modifies this time series instance instead of creating a new one. Default is false. + /// + /// + /// A time series that contains only time points for the specified range. + /// If inPlace is true, returns this instance; otherwise returns a new instance. + /// + public TimeSeries Slice(int startIndex, int count, bool inPlace = false) + { + if (inPlace) + { + this.EnableMutation(); + try + { + var slicedData = this.sortedValues.Skip(startIndex).Take(count).ToList(); + this.UpdateInPlace(slicedData); + return this; + } + finally + { + this.DisableMutation(); + } + } + else + { + return new TimeSeries(this.sortedValues.Skip(startIndex).Take(count), this.Frequency, this.TimeZone); + } } /// @@ -940,6 +1168,90 @@ private TimeZoneInfo EnsureTimeZonesAreCompatible(params TimeSeries[] dateTimeSe return allNonEmpty.FirstOrDefault()?.TimeZone ?? this.TimeZone; } + #region InPlace Convenience Methods + + /// + /// Adds a scalar value to all elements in place. + /// + /// The value to add. + /// This TimeSeries instance for method chaining. + public TimeSeries AddInPlace(decimal value) + { + return this.Apply(x => x + value, inPlace: true); + } + + /// + /// Subtracts a scalar value from all elements in place. + /// + /// The value to subtract. + /// This TimeSeries instance for method chaining. + public TimeSeries SubtractInPlace(decimal value) + { + return this.Apply(x => x - value, inPlace: true); + } + + /// + /// Multiplies all elements by a scalar value in place. + /// + /// The value to multiply by. + /// This TimeSeries instance for method chaining. + public TimeSeries MultiplyInPlace(decimal value) + { + return this.Apply(x => x * value, inPlace: true); + } + + /// + /// Divides all elements by a scalar value in place. + /// + /// The value to divide by. + /// This TimeSeries instance for method chaining. + public TimeSeries DivideInPlace(decimal value) + { + return this.Apply(x => x / value, inPlace: true); + } + + /// + /// Adds another time series to this one in place using JoinLeft. + /// + /// The time series to add. + /// This TimeSeries instance for method chaining. + public TimeSeries AddInPlace(TimeSeries other) + { + return this.JoinLeft(other, JoinOperation.Add, inPlace: true); + } + + /// + /// Subtracts another time series from this one in place using JoinLeft. + /// + /// The time series to subtract. + /// This TimeSeries instance for method chaining. + public TimeSeries SubtractInPlace(TimeSeries other) + { + return this.JoinLeft(other, JoinOperation.Subtract, inPlace: true); + } + + /// + /// Multiplies this time series by another one in place using JoinLeft. + /// + /// The time series to multiply by. + /// This TimeSeries instance for method chaining. + public TimeSeries MultiplyInPlace(TimeSeries other) + { + return this.JoinLeft(other, JoinOperation.Multiply, inPlace: true); + } + + /// + /// Divides this time series by another one in place using JoinLeft. + /// + /// The time series to divide by. + /// This TimeSeries instance for method chaining. + public TimeSeries DivideInPlace(TimeSeries other) + { + return this.JoinLeft(other, JoinOperation.Divide, inPlace: true); + } + + #endregion + #region Operators public static TimeSeries operator +(TimeSeries ts)