From bda7efe83c1ef748d15c6eea0a5748553988311d Mon Sep 17 00:00:00 2001 From: Alexandre Giard Date: Mon, 10 Aug 2026 22:54:15 -0400 Subject: [PATCH] feat: add refresh-aware and immutable Sum operators --- src/DynamicData.Benchmarks/Cache/Sum_Cache.cs | 24 +- src/DynamicData.Benchmarks/List/Sum_List.cs | 27 +- ...ts.DynamicDataTests.DotNet9_0.verified.txt | 50 ++ .../AggregationTests/SumFixture.ForCache.cs | 58 +++ .../AggregationTests/SumFixture.ForList.cs | 61 +++ .../Aggregation/SumEx.Immutable.cs | 352 ++++++++++++++ src/DynamicData/Aggregation/SumEx.cs | 428 +++++++++++++++--- 7 files changed, 909 insertions(+), 91 deletions(-) create mode 100644 src/DynamicData/Aggregation/SumEx.Immutable.cs diff --git a/src/DynamicData.Benchmarks/Cache/Sum_Cache.cs b/src/DynamicData.Benchmarks/Cache/Sum_Cache.cs index 008192d9b..50dc896b2 100644 --- a/src/DynamicData.Benchmarks/Cache/Sum_Cache.cs +++ b/src/DynamicData.Benchmarks/Cache/Sum_Cache.cs @@ -19,6 +19,7 @@ public class Sum_Cache private IChangeSet _seedAfterAdds = null!; private IChangeSet _seedAfterReplaces = null!; + private IChangeSet _seedAfterRefreshes = null!; [Params(100, 500, 1_000, 10_000)] public int Count { get; set; } @@ -45,6 +46,11 @@ public void Setup() var addedItems = (Item[])items.Clone(); + // Each non-add benchmark only forms a valid sequence for a stateful operator after it has seen + // the preceding population. Seed snapshots are collapsed into one change set so their cost stays + // outside the measured sequence as far as possible. + _seedAfterAdds = BuildSeed(addedItems); + var replaceChangeSets = new List>(capacity: Count); for (var id = 1; id <= Count; ++id) { @@ -58,6 +64,7 @@ public void Setup() replaceChangeSets.Add(source.CaptureChanges()); } _replaceChangeSets = replaceChangeSets; + _seedAfterReplaces = BuildSeed(items); var refreshChangeSets = new List>(capacity: Count); for (var id = 1; id <= Count; ++id) @@ -68,6 +75,7 @@ public void Setup() refreshChangeSets.Add(source.CaptureChanges()); } _refreshChangeSets = refreshChangeSets; + _seedAfterRefreshes = BuildSeed(items); var removeChangeSets = new List>(capacity: Count); for (var id = 1; id <= Count; ++id) @@ -76,14 +84,6 @@ public void Setup() removeChangeSets.Add(source.CaptureChanges()); } _removeChangeSets = removeChangeSets; - - // Replaces, refreshes, and removes only form a valid sequence for an operator that has already - // seen the items they refer to, so each of those runs gets seeded with the population as it stood - // beforehand. Collapsing the seed into a single change set keeps its cost off the measurement as - // far as possible: replaces follow on from the items that were added, while refreshes and removes - // follow on from the items that replaced them. - _seedAfterAdds = BuildSeed(addedItems); - _seedAfterReplaces = BuildSeed(items); } [Benchmark] @@ -96,14 +96,18 @@ public void Setup() public void Refreshes() => Run(_seedAfterReplaces, _refreshChangeSets); [Benchmark] - public void Removes() => Run(_seedAfterReplaces, _removeChangeSets); + public void Removes() => Run(_seedAfterRefreshes, _removeChangeSets); private static IChangeSet BuildSeed(Item[] items) { var seed = new ChangeAwareCache(capacity: items.Length - 1); for (var id = 1; id < items.Length; ++id) - seed.Add(items[id], key: id); + seed.Add(new Item() + { + Id = items[id].Id, + Value = items[id].Value + }, key: id); return seed.CaptureChanges(); } diff --git a/src/DynamicData.Benchmarks/List/Sum_List.cs b/src/DynamicData.Benchmarks/List/Sum_List.cs index 87a90c97e..d3ddafa75 100644 --- a/src/DynamicData.Benchmarks/List/Sum_List.cs +++ b/src/DynamicData.Benchmarks/List/Sum_List.cs @@ -19,6 +19,7 @@ public class Sum_List private IChangeSet _seedAfterAdds = null!; private IChangeSet _seedAfterReplaces = null!; + private IChangeSet _seedAfterRefreshes = null!; [Params(100, 500, 1_000, 10_000)] public int Count { get; set; } @@ -44,6 +45,11 @@ public void Setup() var addedItems = (Item[])items.Clone(); + // Each non-add benchmark only forms a valid sequence for a stateful operator after it has seen + // the preceding population. Seed snapshots are collapsed into one change set so their cost stays + // outside the measured sequence as far as possible. + _seedAfterAdds = BuildSeed(addedItems); + var replaceChangeSets = new List>(capacity: Count); for (var index = 0; index < Count; ++index) { @@ -56,6 +62,7 @@ public void Setup() replaceChangeSets.Add(source.CaptureChanges()); } _replaceChangeSets = replaceChangeSets; + _seedAfterReplaces = BuildSeed(items); var refreshChangeSets = new List>(capacity: Count); for (var index = 0; index < Count; ++index) @@ -66,6 +73,7 @@ public void Setup() refreshChangeSets.Add(source.CaptureChanges()); } _refreshChangeSets = refreshChangeSets; + _seedAfterRefreshes = BuildSeed(items); var removeChangeSets = new List>(capacity: Count); for (var id = 1; id <= Count; ++id) @@ -74,14 +82,6 @@ public void Setup() removeChangeSets.Add(source.CaptureChanges()); } _removeChangeSets = removeChangeSets; - - // Replaces, refreshes, and removes only form a valid sequence for an operator that has already - // seen the items they refer to, so each of those runs gets seeded with the population as it stood - // beforehand. Collapsing the seed into a single change set keeps its cost off the measurement as - // far as possible: replaces follow on from the items that were added, while refreshes and removes - // follow on from the items that replaced them. - _seedAfterAdds = BuildSeed(addedItems); - _seedAfterReplaces = BuildSeed(items); } [Benchmark] @@ -94,13 +94,20 @@ public void Setup() public void Refreshes() => Run(_seedAfterReplaces, _refreshChangeSets); [Benchmark] - public void Removes() => Run(_seedAfterReplaces, _removeChangeSets); + public void Removes() => Run(_seedAfterRefreshes, _removeChangeSets); private static IChangeSet BuildSeed(Item[] items) { var seed = new ChangeAwareList(capacity: items.Length); - seed.AddRange(items); + foreach (var item in items) + { + seed.Add(new Item() + { + Id = item.Id, + Value = item.Value + }); + } return seed.CaptureChanges(); } diff --git a/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet9_0.verified.txt b/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet9_0.verified.txt index 5e852c6d4..8207992c3 100644 --- a/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet9_0.verified.txt +++ b/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet9_0.verified.txt @@ -225,6 +225,56 @@ namespace DynamicData.Aggregation public static System.IObservable Sum(this System.IObservable> source, System.Func valueSelector) where TObject : notnull where TKey : notnull { } + public static System.IObservable SumImmutable(this System.IObservable> source, System.Func valueSelector) + where T : notnull { } + public static System.IObservable SumImmutable(this System.IObservable> source, System.Func valueSelector) + where T : notnull { } + public static System.IObservable SumImmutable(this System.IObservable> source, System.Func valueSelector) + where T : notnull { } + public static System.IObservable SumImmutable(this System.IObservable> source, System.Func valueSelector) + where T : notnull { } + public static System.IObservable SumImmutable(this System.IObservable> source, System.Func valueSelector) + where T : notnull { } + public static System.IObservable SumImmutable(this System.IObservable> source, System.Func valueSelector) + where T : notnull { } + public static System.IObservable SumImmutable(this System.IObservable> source, System.Func valueSelector) + where T : notnull { } + public static System.IObservable SumImmutable(this System.IObservable> source, System.Func valueSelector) + where T : notnull { } + public static System.IObservable SumImmutable(this System.IObservable> source, System.Func valueSelector) + where T : notnull { } + public static System.IObservable SumImmutable(this System.IObservable> source, System.Func valueSelector) + where T : notnull { } + public static System.IObservable SumImmutable(this System.IObservable> source, System.Func valueSelector) + where TObject : notnull + where TKey : notnull { } + public static System.IObservable SumImmutable(this System.IObservable> source, System.Func valueSelector) + where TObject : notnull + where TKey : notnull { } + public static System.IObservable SumImmutable(this System.IObservable> source, System.Func valueSelector) + where TObject : notnull + where TKey : notnull { } + public static System.IObservable SumImmutable(this System.IObservable> source, System.Func valueSelector) + where TObject : notnull + where TKey : notnull { } + public static System.IObservable SumImmutable(this System.IObservable> source, System.Func valueSelector) + where TObject : notnull + where TKey : notnull { } + public static System.IObservable SumImmutable(this System.IObservable> source, System.Func valueSelector) + where TObject : notnull + where TKey : notnull { } + public static System.IObservable SumImmutable(this System.IObservable> source, System.Func valueSelector) + where TObject : notnull + where TKey : notnull { } + public static System.IObservable SumImmutable(this System.IObservable> source, System.Func valueSelector) + where TObject : notnull + where TKey : notnull { } + public static System.IObservable SumImmutable(this System.IObservable> source, System.Func valueSelector) + where TObject : notnull + where TKey : notnull { } + public static System.IObservable SumImmutable(this System.IObservable> source, System.Func valueSelector) + where TObject : notnull + where TKey : notnull { } } } namespace DynamicData.Alias diff --git a/src/DynamicData.Tests/AggregationTests/SumFixture.ForCache.cs b/src/DynamicData.Tests/AggregationTests/SumFixture.ForCache.cs index 078a8f1b0..1ec218377 100644 --- a/src/DynamicData.Tests/AggregationTests/SumFixture.ForCache.cs +++ b/src/DynamicData.Tests/AggregationTests/SumFixture.ForCache.cs @@ -348,6 +348,64 @@ public void NullableValuesAreTreatedAsZero() .Which.Should().Be(40, "null values should be treated as zero, so the sum should be 10 + 0 + 30 = 40"); } + [Fact] + public void ItemIsRefreshed_SumReflectsMutatedValue() + { + using var source = new TestSourceCache(p => p.Name); + var person = new Person("A", 10); + + source.AddOrUpdate(person); + + using var subscription = source.Connect() + .Sum(p => p.Age) + .RecordValues(out var results); + + person.Age = 40; + source.Refresh(person); + + results.RecordedValues.Should().Equal(10, 40); + + source.Remove(person.Name); + + results.RecordedValues[^1].Should().Be(0, "removal should subtract the value captured by the refresh"); + } + + [Fact] + public void NullableItemIsRefreshed_SumReflectsMutatedValue() + { + using var source = new TestSourceCache(p => p.Name); + var person = new Person("A", new int?(10)); + + source.AddOrUpdate(person); + + using var subscription = source.Connect() + .Sum(p => p.AgeNullable) + .RecordValues(out var results); + + person.AgeNullable = null; + source.Refresh(person); + + results.RecordedValues.Should().Equal(10, 0); + } + + [Fact] + public void ItemIsRefreshed_SumImmutableDoesNotReevaluateMutatedValue() + { + using var source = new TestSourceCache(p => p.Name); + var person = new Person("A", 10); + + source.AddOrUpdate(person); + + using var subscription = source.Connect() + .SumImmutable(p => p.Age) + .RecordValues(out var results); + + person.Age = 40; + source.Refresh(person); + + results.RecordedValues.Should().Equal(10, 10); + } + [Theory] [InlineData(new[] { 10, 20, 30 }, 60)] [InlineData(new[] { int.MaxValue }, int.MaxValue)] diff --git a/src/DynamicData.Tests/AggregationTests/SumFixture.ForList.cs b/src/DynamicData.Tests/AggregationTests/SumFixture.ForList.cs index 9e9108602..efe3731ad 100644 --- a/src/DynamicData.Tests/AggregationTests/SumFixture.ForList.cs +++ b/src/DynamicData.Tests/AggregationTests/SumFixture.ForList.cs @@ -2,6 +2,7 @@ using System.Linq; using DynamicData.Aggregation; +using DynamicData.Tests.Domain; using DynamicData.Tests.Utilities; using FluentAssertions; @@ -265,6 +266,66 @@ public void SourceFailsImmediately_ErrorPropagates() results.HasCompleted.Should().BeFalse("an error is not a completion"); } + [Fact] + public void ItemIsRefreshed_SumReflectsMutatedValue() + { + using var source = new TestSourceList(); + var person = new Person("A", 10); + + source.Add(person); + + using var subscription = source.Connect() + .Sum(p => p.Age) + .RecordValues(out var results); + + person.Age = 40; + source.Refresh(0); + + results.RecordedValues.Should().Equal(10, 40); + + source.RemoveAt(0); + + results.RecordedValues[^1].Should().Be(0, "removal should subtract the value captured by the refresh"); + } + + [Fact] + public void ItemIsMoved_RefreshUsesItsNewIndex() + { + using var source = new TestSourceList(); + var first = new Person("A", 10); + var second = new Person("B", 20); + + source.AddRange(new[] { first, second }); + + using var subscription = source.Connect() + .Sum(p => p.Age) + .RecordValues(out var results); + + source.Move(1, 0); + second.Age = 50; + source.Refresh(0); + + results.RecordedValues.Should().Equal(30, 30, 60); + } + + [Fact] + public void ItemIsRefreshed_SumImmutableDoesNotReevaluateMutatedValue() + { + using var source = new TestSourceList(); + var person = new Person("A", 10); + + source.Add(person); + + using var subscription = source.Connect() + .SumImmutable(p => p.Age) + .RecordValues(out var results); + + person.Age = 40; + source.Refresh(0); + + results.RecordedValues.Should().Equal(10, 10); + } + [Theory] [InlineData(new[] { 10, 20, 30 }, 60)] [InlineData(new[] { int.MaxValue }, int.MaxValue)] diff --git a/src/DynamicData/Aggregation/SumEx.Immutable.cs b/src/DynamicData/Aggregation/SumEx.Immutable.cs new file mode 100644 index 000000000..0eb075fbc --- /dev/null +++ b/src/DynamicData/Aggregation/SumEx.Immutable.cs @@ -0,0 +1,352 @@ +// Copyright (c) 2011-2025 Roland Pheasant. All rights reserved. +// Roland Pheasant licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Reactive.Linq; + +namespace DynamicData.Aggregation; + +/// +/// Provides immutable-item sum aggregation extensions. +/// +public static partial class SumEx +{ + /// + /// Continually computes the sum, optimized for immutable items. Refresh changes do not re-evaluate items. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The value selector. + /// An observable which emits the summed value. + public static IObservable SumImmutable(this IObservable> source, Func valueSelector) + where TObject : notnull + where TKey : notnull => SumCacheImmutable(source, valueSelector, 0, static (current, value) => current + value, static (current, value) => current - value); + + /// + /// Continually computes the sum, optimized for immutable items. Refresh changes do not re-evaluate items. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The value selector. + /// An observable which emits the summed value. + public static IObservable SumImmutable(this IObservable> source, Func valueSelector) + where TObject : notnull + where TKey : notnull => SumCacheImmutableNullable(source, valueSelector, 0, static (current, value) => current + value, static (current, value) => current - value); + + /// + /// Continually computes the sum, optimized for immutable items. Refresh changes do not re-evaluate items. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The value selector. + /// An observable which emits the summed value. + public static IObservable SumImmutable(this IObservable> source, Func valueSelector) + where TObject : notnull + where TKey : notnull => SumCacheImmutable(source, valueSelector, 0L, static (current, value) => current + value, static (current, value) => current - value); + + /// + /// Continually computes the sum, optimized for immutable items. Refresh changes do not re-evaluate items. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The value selector. + /// An observable which emits the summed value. + public static IObservable SumImmutable(this IObservable> source, Func valueSelector) + where TObject : notnull + where TKey : notnull => SumCacheImmutableNullable(source, valueSelector, 0L, static (current, value) => current + value, static (current, value) => current - value); + + /// + /// Continually computes the sum, optimized for immutable items. Refresh changes do not re-evaluate items. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The value selector. + /// An observable which emits the summed value. + public static IObservable SumImmutable(this IObservable> source, Func valueSelector) + where TObject : notnull + where TKey : notnull => SumCacheImmutable(source, valueSelector, 0D, static (current, value) => current + value, static (current, value) => current - value); + + /// + /// Continually computes the sum, optimized for immutable items. Refresh changes do not re-evaluate items. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The value selector. + /// An observable which emits the summed value. + public static IObservable SumImmutable(this IObservable> source, Func valueSelector) + where TObject : notnull + where TKey : notnull => SumCacheImmutableNullable(source, valueSelector, 0D, static (current, value) => current + value, static (current, value) => current - value); + + /// + /// Continually computes the sum, optimized for immutable items. Refresh changes do not re-evaluate items. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The value selector. + /// An observable which emits the summed value. + public static IObservable SumImmutable(this IObservable> source, Func valueSelector) + where TObject : notnull + where TKey : notnull => SumCacheImmutable(source, valueSelector, 0M, static (current, value) => current + value, static (current, value) => current - value); + + /// + /// Continually computes the sum, optimized for immutable items. Refresh changes do not re-evaluate items. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The value selector. + /// An observable which emits the summed value. + public static IObservable SumImmutable(this IObservable> source, Func valueSelector) + where TObject : notnull + where TKey : notnull => SumCacheImmutableNullable(source, valueSelector, 0M, static (current, value) => current + value, static (current, value) => current - value); + + /// + /// Continually computes the sum, optimized for immutable items. Refresh changes do not re-evaluate items. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The value selector. + /// An observable which emits the summed value. + public static IObservable SumImmutable(this IObservable> source, Func valueSelector) + where TObject : notnull + where TKey : notnull => SumCacheImmutable(source, valueSelector, 0F, static (current, value) => current + value, static (current, value) => current - value); + + /// + /// Continually computes the sum, optimized for immutable items. Refresh changes do not re-evaluate items. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The value selector. + /// An observable which emits the summed value. + public static IObservable SumImmutable(this IObservable> source, Func valueSelector) + where TObject : notnull + where TKey : notnull => SumCacheImmutableNullable(source, valueSelector, 0F, static (current, value) => current + value, static (current, value) => current - value); + + /// + /// Continually computes the sum, optimized for immutable items. Refresh changes do not re-evaluate items. + /// + /// The type of the item. + /// The source. + /// The value selector. + /// An observable which emits the summed value. + public static IObservable SumImmutable(this IObservable> source, Func valueSelector) + where T : notnull => SumListImmutable(source, valueSelector, 0, static (current, value) => current + value, static (current, value) => current - value); + + /// + /// Continually computes the sum, optimized for immutable items. Refresh changes do not re-evaluate items. + /// + /// The type of the item. + /// The source. + /// The value selector. + /// An observable which emits the summed value. + public static IObservable SumImmutable(this IObservable> source, Func valueSelector) + where T : notnull => SumListImmutableNullable(source, valueSelector, 0, static (current, value) => current + value, static (current, value) => current - value); + + /// + /// Continually computes the sum, optimized for immutable items. Refresh changes do not re-evaluate items. + /// + /// The type of the item. + /// The source. + /// The value selector. + /// An observable which emits the summed value. + public static IObservable SumImmutable(this IObservable> source, Func valueSelector) + where T : notnull => SumListImmutable(source, valueSelector, 0L, static (current, value) => current + value, static (current, value) => current - value); + + /// + /// Continually computes the sum, optimized for immutable items. Refresh changes do not re-evaluate items. + /// + /// The type of the item. + /// The source. + /// The value selector. + /// An observable which emits the summed value. + public static IObservable SumImmutable(this IObservable> source, Func valueSelector) + where T : notnull => SumListImmutableNullable(source, valueSelector, 0L, static (current, value) => current + value, static (current, value) => current - value); + + /// + /// Continually computes the sum, optimized for immutable items. Refresh changes do not re-evaluate items. + /// + /// The type of the item. + /// The source. + /// The value selector. + /// An observable which emits the summed value. + public static IObservable SumImmutable(this IObservable> source, Func valueSelector) + where T : notnull => SumListImmutable(source, valueSelector, 0D, static (current, value) => current + value, static (current, value) => current - value); + + /// + /// Continually computes the sum, optimized for immutable items. Refresh changes do not re-evaluate items. + /// + /// The type of the item. + /// The source. + /// The value selector. + /// An observable which emits the summed value. + public static IObservable SumImmutable(this IObservable> source, Func valueSelector) + where T : notnull => SumListImmutableNullable(source, valueSelector, 0D, static (current, value) => current + value, static (current, value) => current - value); + + /// + /// Continually computes the sum, optimized for immutable items. Refresh changes do not re-evaluate items. + /// + /// The type of the item. + /// The source. + /// The value selector. + /// An observable which emits the summed value. + public static IObservable SumImmutable(this IObservable> source, Func valueSelector) + where T : notnull => SumListImmutable(source, valueSelector, 0M, static (current, value) => current + value, static (current, value) => current - value); + + /// + /// Continually computes the sum, optimized for immutable items. Refresh changes do not re-evaluate items. + /// + /// The type of the item. + /// The source. + /// The value selector. + /// An observable which emits the summed value. + public static IObservable SumImmutable(this IObservable> source, Func valueSelector) + where T : notnull => SumListImmutableNullable(source, valueSelector, 0M, static (current, value) => current + value, static (current, value) => current - value); + + /// + /// Continually computes the sum, optimized for immutable items. Refresh changes do not re-evaluate items. + /// + /// The type of the item. + /// The source. + /// The value selector. + /// An observable which emits the summed value. + public static IObservable SumImmutable(this IObservable> source, Func valueSelector) + where T : notnull => SumListImmutable(source, valueSelector, 0F, static (current, value) => current + value, static (current, value) => current - value); + + /// + /// Continually computes the sum, optimized for immutable items. Refresh changes do not re-evaluate items. + /// + /// The type of the item. + /// The source. + /// The value selector. + /// An observable which emits the summed value. + public static IObservable SumImmutable(this IObservable> source, Func valueSelector) + where T : notnull => SumListImmutableNullable(source, valueSelector, 0F, static (current, value) => current + value, static (current, value) => current - value); + + private static IObservable SumCacheImmutable( + IObservable> source, + Func valueSelector, + TValue seed, + Func add, + Func subtract) + where TObject : notnull + where TKey : notnull + { + source.ThrowArgumentNullExceptionIfNull(nameof(source)); + valueSelector.ThrowArgumentNullExceptionIfNull(nameof(valueSelector)); + + return source.Scan(seed, (sum, changes) => + { + foreach (var change in changes) + { + switch (change.Reason) + { + case ChangeReason.Add: + sum = add(sum, valueSelector(change.Current)); + break; + + case ChangeReason.Update: + sum = subtract(sum, valueSelector(change.Previous.Value)); + sum = add(sum, valueSelector(change.Current)); + break; + + case ChangeReason.Remove: + sum = subtract(sum, valueSelector(change.Current)); + break; + } + } + + return sum; + }); + } + + private static IObservable SumCacheImmutableNullable( + IObservable> source, + Func valueSelector, + TValue seed, + Func add, + Func subtract) + where TObject : notnull + where TKey : notnull + where TValue : struct + { + valueSelector.ThrowArgumentNullExceptionIfNull(nameof(valueSelector)); + + return SumCacheImmutable(source, item => valueSelector(item).GetValueOrDefault(), seed, add, subtract); + } + + private static IObservable SumListImmutable( + IObservable> source, + Func valueSelector, + TValue seed, + Func add, + Func subtract) + where TObject : notnull + { + source.ThrowArgumentNullExceptionIfNull(nameof(source)); + valueSelector.ThrowArgumentNullExceptionIfNull(nameof(valueSelector)); + + return source.Scan(seed, (sum, changes) => + { + foreach (var change in changes) + { + switch (change.Reason) + { + case ListChangeReason.Add: + sum = add(sum, valueSelector(change.Item.Current)); + break; + + case ListChangeReason.AddRange: + foreach (var item in change.Range) + { + sum = add(sum, valueSelector(item)); + } + + break; + + case ListChangeReason.Replace: + sum = subtract(sum, valueSelector(change.Item.Previous.Value)); + sum = add(sum, valueSelector(change.Item.Current)); + break; + + case ListChangeReason.Remove: + sum = subtract(sum, valueSelector(change.Item.Current)); + break; + + case ListChangeReason.RemoveRange: + case ListChangeReason.Clear: + foreach (var item in change.Range) + { + sum = subtract(sum, valueSelector(item)); + } + + break; + } + } + + return sum; + }); + } + + private static IObservable SumListImmutableNullable( + IObservable> source, + Func valueSelector, + TValue seed, + Func add, + Func subtract) + where TObject : notnull + where TValue : struct + { + valueSelector.ThrowArgumentNullExceptionIfNull(nameof(valueSelector)); + + return SumListImmutable(source, item => valueSelector(item).GetValueOrDefault(), seed, add, subtract); + } +} diff --git a/src/DynamicData/Aggregation/SumEx.cs b/src/DynamicData/Aggregation/SumEx.cs index 6d3f59a9a..2c86c70ed 100644 --- a/src/DynamicData/Aggregation/SumEx.cs +++ b/src/DynamicData/Aggregation/SumEx.cs @@ -2,12 +2,17 @@ // Roland Pheasant licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Reactive.Linq; + namespace DynamicData.Aggregation; /// /// Aggregation extensions. /// -public static class SumEx +/// +/// Sum overloads operating directly on cache and list change sets retain projection state and re-evaluate refreshed items. +/// +public static partial class SumEx { /// /// Continual computes the sum of values matching the value selector. @@ -19,7 +24,7 @@ public static class SumEx /// An observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) where TObject : notnull - where TKey : notnull => source.ForAggregation().Sum(valueSelector); + where TKey : notnull => SumCacheStateful(source, valueSelector, 0, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -31,7 +36,7 @@ public static IObservable Sum(this IObservableAn observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) where TObject : notnull - where TKey : notnull => source.ForAggregation().Sum(valueSelector); + where TKey : notnull => SumCacheStatefulNullable(source, valueSelector, 0, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -43,7 +48,7 @@ public static IObservable Sum(this IObservableAn observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) where TObject : notnull - where TKey : notnull => source.ForAggregation().Sum(valueSelector); + where TKey : notnull => SumCacheStateful(source, valueSelector, 0L, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -55,7 +60,7 @@ public static IObservable Sum(this IObservableAn observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) where TObject : notnull - where TKey : notnull => source.ForAggregation().Sum(valueSelector); + where TKey : notnull => SumCacheStatefulNullable(source, valueSelector, 0L, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -67,7 +72,7 @@ public static IObservable Sum(this IObservableAn observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) where TObject : notnull - where TKey : notnull => source.ForAggregation().Sum(valueSelector); + where TKey : notnull => SumCacheStateful(source, valueSelector, 0D, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -79,7 +84,7 @@ public static IObservable Sum(this IObservableAn observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) where TObject : notnull - where TKey : notnull => source.ForAggregation().Sum(valueSelector); + where TKey : notnull => SumCacheStatefulNullable(source, valueSelector, 0D, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -91,7 +96,7 @@ public static IObservable Sum(this IObservableAn observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) where TObject : notnull - where TKey : notnull => source.ForAggregation().Sum(valueSelector); + where TKey : notnull => SumCacheStateful(source, valueSelector, 0M, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -103,7 +108,7 @@ public static IObservable Sum(this IObservableAn observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) where TObject : notnull - where TKey : notnull => source.ForAggregation().Sum(valueSelector); + where TKey : notnull => SumCacheStatefulNullable(source, valueSelector, 0M, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -115,7 +120,7 @@ public static IObservable Sum(this IObservableAn observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) where TObject : notnull - where TKey : notnull => source.ForAggregation().Sum(valueSelector); + where TKey : notnull => SumCacheStateful(source, valueSelector, 0F, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -127,7 +132,7 @@ public static IObservable Sum(this IObservableAn observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) where TObject : notnull - where TKey : notnull => source.ForAggregation().Sum(valueSelector); + where TKey : notnull => SumCacheStatefulNullable(source, valueSelector, 0F, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -137,7 +142,7 @@ public static IObservable Sum(this IObservableThe value selector. /// An observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) - where T : notnull => source.ForAggregation().Sum(valueSelector); + where T : notnull => SumListStateful(source, valueSelector, 0, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -147,7 +152,7 @@ public static IObservable Sum(this IObservable> source, Fu /// The value selector. /// An observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) - where T : notnull => source.ForAggregation().Sum(valueSelector); + where T : notnull => SumListStatefulNullable(source, valueSelector, 0, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -157,7 +162,7 @@ public static IObservable Sum(this IObservable> source, Fu /// The value selector. /// An observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) - where T : notnull => source.ForAggregation().Sum(valueSelector); + where T : notnull => SumListStateful(source, valueSelector, 0L, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -167,7 +172,7 @@ public static IObservable Sum(this IObservable> source, F /// The value selector. /// An observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) - where T : notnull => source.ForAggregation().Sum(valueSelector); + where T : notnull => SumListStatefulNullable(source, valueSelector, 0L, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -177,7 +182,7 @@ public static IObservable Sum(this IObservable> source, F /// The value selector. /// An observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) - where T : notnull => source.ForAggregation().Sum(valueSelector); + where T : notnull => SumListStateful(source, valueSelector, 0D, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -187,7 +192,7 @@ public static IObservable Sum(this IObservable> source, /// The value selector. /// An observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) - where T : notnull => source.ForAggregation().Sum(valueSelector); + where T : notnull => SumListStatefulNullable(source, valueSelector, 0D, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -197,7 +202,7 @@ public static IObservable Sum(this IObservable> source, /// The value selector. /// An observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) - where T : notnull => source.ForAggregation().Sum(valueSelector); + where T : notnull => SumListStateful(source, valueSelector, 0M, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -207,7 +212,7 @@ public static IObservable Sum(this IObservable> source /// The value selector. /// An observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) - where T : notnull => source.ForAggregation().Sum(valueSelector); + where T : notnull => SumListStatefulNullable(source, valueSelector, 0M, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -217,7 +222,7 @@ public static IObservable Sum(this IObservable> source /// The value selector. /// An observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) - where T : notnull => source.ForAggregation().Sum(valueSelector); + where T : notnull => SumListStateful(source, valueSelector, 0F, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -227,7 +232,7 @@ public static IObservable Sum(this IObservable> source, /// The value selector. /// An observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) - where T : notnull => source.ForAggregation().Sum(valueSelector); + where T : notnull => SumListStatefulNullable(source, valueSelector, 0F, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -237,12 +242,7 @@ public static IObservable Sum(this IObservable> source, /// The value selector. /// An observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) - { - source.ThrowArgumentNullExceptionIfNull(nameof(source)); - valueSelector.ThrowArgumentNullExceptionIfNull(nameof(valueSelector)); - - return source.Accumulate(0, valueSelector, (current, value) => current + value, (current, value) => current - value); - } + => SumAggregate(source, valueSelector, 0, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -251,7 +251,8 @@ public static IObservable Sum(this IObservable> s /// The source. /// The value selector. /// An observable which emits the summed value. - public static IObservable Sum(this IObservable> source, Func valueSelector) => source.Accumulate(0, t => valueSelector(t).GetValueOrDefault(), (current, value) => current + value, (current, value) => current - value); + public static IObservable Sum(this IObservable> source, Func valueSelector) + => SumAggregateNullable(source, valueSelector, 0, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -261,12 +262,7 @@ public static IObservable Sum(this IObservable> s /// The value selector. /// An observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) - { - source.ThrowArgumentNullExceptionIfNull(nameof(source)); - valueSelector.ThrowArgumentNullExceptionIfNull(nameof(valueSelector)); - - return source.Accumulate(0, valueSelector, (current, value) => current + value, (current, value) => current - value); - } + => SumAggregate(source, valueSelector, 0L, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -276,12 +272,7 @@ public static IObservable Sum(this IObservable> /// The value selector. /// An observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) - { - source.ThrowArgumentNullExceptionIfNull(nameof(source)); - valueSelector.ThrowArgumentNullExceptionIfNull(nameof(valueSelector)); - - return source.Accumulate(0L, t => valueSelector(t).ValueOr(0), (current, value) => current + value, (current, value) => current - value); - } + => SumAggregateNullable(source, valueSelector, 0L, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -291,12 +282,7 @@ public static IObservable Sum(this IObservable> /// The value selector. /// An observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) - { - source.ThrowArgumentNullExceptionIfNull(nameof(source)); - valueSelector.ThrowArgumentNullExceptionIfNull(nameof(valueSelector)); - - return source.Accumulate(0, valueSelector, (current, value) => current + value, (current, value) => current - value); - } + => SumAggregate(source, valueSelector, 0D, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -306,12 +292,7 @@ public static IObservable Sum(this IObservable /// The value selector. /// An observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) - { - source.ThrowArgumentNullExceptionIfNull(nameof(source)); - valueSelector.ThrowArgumentNullExceptionIfNull(nameof(valueSelector)); - - return source.Accumulate(0D, t => valueSelector(t).ValueOr(0), (current, value) => current + value, (current, value) => current - value); - } + => SumAggregateNullable(source, valueSelector, 0D, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -321,12 +302,7 @@ public static IObservable Sum(this IObservable /// The value selector. /// An observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) - { - source.ThrowArgumentNullExceptionIfNull(nameof(source)); - valueSelector.ThrowArgumentNullExceptionIfNull(nameof(valueSelector)); - - return source.Accumulate(0, valueSelector, (current, value) => current + value, (current, value) => current - value); - } + => SumAggregate(source, valueSelector, 0M, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -336,12 +312,7 @@ public static IObservable Sum(this IObservableThe value selector. /// An observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) - { - source.ThrowArgumentNullExceptionIfNull(nameof(source)); - valueSelector.ThrowArgumentNullExceptionIfNull(nameof(valueSelector)); - - return source.Accumulate(0M, t => valueSelector(t).ValueOr(0), (current, value) => current + value, (current, value) => current - value); - } + => SumAggregateNullable(source, valueSelector, 0M, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -351,12 +322,7 @@ public static IObservable Sum(this IObservableThe value selector. /// An observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) - { - source.ThrowArgumentNullExceptionIfNull(nameof(source)); - valueSelector.ThrowArgumentNullExceptionIfNull(nameof(valueSelector)); - - return source.Accumulate(0, valueSelector, (current, value) => current + value, (current, value) => current - value); - } + => SumAggregate(source, valueSelector, 0F, static (current, value) => current + value, static (current, value) => current - value); /// /// Continual computes the sum of values matching the value selector. @@ -366,10 +332,330 @@ public static IObservable Sum(this IObservable> /// The value selector. /// An observable which emits the summed value. public static IObservable Sum(this IObservable> source, Func valueSelector) + => SumAggregateNullable(source, valueSelector, 0F, static (current, value) => current + value, static (current, value) => current - value); + + private static IObservable SumCacheStateful( + IObservable> source, + Func valueSelector, + TValue seed, + Func add, + Func subtract) + where TObject : notnull + where TKey : notnull + { + source.ThrowArgumentNullExceptionIfNull(nameof(source)); + valueSelector.ThrowArgumentNullExceptionIfNull(nameof(valueSelector)); + + return Observable.Defer(() => + { + var values = new Dictionary(); + + return source.Scan(seed, (sum, changes) => + { + foreach (var change in changes) + { + switch (change.Reason) + { + case ChangeReason.Add: + { + var value = valueSelector(change.Current); + values.Add(change.Key, value); + sum = add(sum, value); + break; + } + + case ChangeReason.Update: + case ChangeReason.Refresh: + { + var previousValue = values[change.Key]; + var currentValue = valueSelector(change.Current); + values[change.Key] = currentValue; + sum = subtract(sum, previousValue); + sum = add(sum, currentValue); + break; + } + + case ChangeReason.Remove: + { + var value = values[change.Key]; + values.Remove(change.Key); + sum = subtract(sum, value); + break; + } + } + } + + return sum; + }); + }); + } + + private static IObservable SumCacheStatefulNullable( + IObservable> source, + Func valueSelector, + TValue seed, + Func add, + Func subtract) + where TObject : notnull + where TKey : notnull + where TValue : struct + { + valueSelector.ThrowArgumentNullExceptionIfNull(nameof(valueSelector)); + + return SumCacheStateful(source, item => valueSelector(item).GetValueOrDefault(), seed, add, subtract); + } + + private static IObservable SumListStateful( + IObservable> source, + Func valueSelector, + TValue seed, + Func add, + Func subtract) + where TObject : notnull + { + source.ThrowArgumentNullExceptionIfNull(nameof(source)); + valueSelector.ThrowArgumentNullExceptionIfNull(nameof(valueSelector)); + + return Observable.Defer(() => + { + var values = new List<(TObject Item, TValue Value)>(); + + return source.Scan(seed, (sum, changes) => + { + foreach (var change in changes) + { + switch (change.Reason) + { + case ListChangeReason.Add: + { + var item = (Item: change.Item.Current, Value: valueSelector(change.Item.Current)); + if (change.Item.CurrentIndex < 0 || change.Item.CurrentIndex >= values.Count) + { + values.Add(item); + } + else + { + values.Insert(change.Item.CurrentIndex, item); + } + + sum = add(sum, item.Value); + break; + } + + case ListChangeReason.AddRange: + { + var items = new List<(TObject Item, TValue Value)>(change.Range.Count); + foreach (var item in change.Range) + { + var value = valueSelector(item); + items.Add((item, value)); + sum = add(sum, value); + } + + if (change.Range.Index < 0 || change.Range.Index >= values.Count) + { + values.AddRange(items); + } + else + { + values.InsertRange(change.Range.Index, items); + } + + break; + } + + case ListChangeReason.Replace: + { + var previousIndex = change.Item.PreviousIndex; + if (previousIndex < 0) + { + previousIndex = IndexOf(values, change.Item.Previous.Value); + } + + if (previousIndex < 0) + { + throw new UnspecifiedIndexException($"Cannot find index of {change.Item.Previous.Value}"); + } + + var previousValue = values[previousIndex].Value; + var currentValue = valueSelector(change.Item.Current); + + if (change.Item.CurrentIndex < 0 || change.Item.CurrentIndex == previousIndex) + { + values[previousIndex] = (change.Item.Current, currentValue); + } + else + { + values.RemoveAt(previousIndex); + values.Insert(change.Item.CurrentIndex, (change.Item.Current, currentValue)); + } + + sum = subtract(sum, previousValue); + sum = add(sum, currentValue); + break; + } + + case ListChangeReason.Remove: + { + var index = change.Item.CurrentIndex; + if (index < 0) + { + index = IndexOf(values, change.Item.Current); + } + + if (index < 0) + { + throw new UnspecifiedIndexException($"Cannot find index of {change.Item.Current}"); + } + + var value = values[index].Value; + values.RemoveAt(index); + sum = subtract(sum, value); + break; + } + + case ListChangeReason.RemoveRange: + if (change.Range.Index >= 0) + { + var rangeEnd = change.Range.Index + change.Range.Count; + for (var index = change.Range.Index; index < rangeEnd; ++index) + { + sum = subtract(sum, values[index].Value); + } + + values.RemoveRange(change.Range.Index, change.Range.Count); + } + else + { + foreach (var item in change.Range) + { + var index = IndexOf(values, item); + if (index < 0) + { + throw new UnspecifiedIndexException($"Cannot find index of {item}"); + } + + sum = subtract(sum, values[index].Value); + values.RemoveAt(index); + } + } + + break; + + case ListChangeReason.Clear: + foreach (var item in values) + { + sum = subtract(sum, item.Value); + } + + values.Clear(); + break; + + case ListChangeReason.Moved: + { + var item = values[change.Item.PreviousIndex]; + values.RemoveAt(change.Item.PreviousIndex); + values.Insert(change.Item.CurrentIndex, item); + break; + } + + case ListChangeReason.Refresh: + { + var index = change.Item.CurrentIndex; + if (index < 0) + { + index = IndexOf(values, change.Item.Current); + } + + if (index < 0) + { + throw new UnspecifiedIndexException($"Cannot find index of {change.Item.Current}"); + } + + var previousValue = values[index].Value; + var currentValue = valueSelector(change.Item.Current); + values[index] = (change.Item.Current, currentValue); + sum = subtract(sum, previousValue); + sum = add(sum, currentValue); + break; + } + } + } + + return sum; + }); + }); + } + + private static IObservable SumListStatefulNullable( + IObservable> source, + Func valueSelector, + TValue seed, + Func add, + Func subtract) + where TObject : notnull + where TValue : struct + { + valueSelector.ThrowArgumentNullExceptionIfNull(nameof(valueSelector)); + + return SumListStateful(source, item => valueSelector(item).GetValueOrDefault(), seed, add, subtract); + } + + private static int IndexOf(List<(TObject Item, TValue Value)> values, TObject item) + where TObject : notnull + { + for (var index = 0; index < values.Count; ++index) + { + if (ReferenceEquals(values[index].Item, item)) + { + return index; + } + } + + var comparer = EqualityComparer.Default; + for (var index = 0; index < values.Count; ++index) + { + if (comparer.Equals(values[index].Item, item)) + { + return index; + } + } + + return -1; + } + + private static IObservable SumAggregate( + IObservable> source, + Func valueSelector, + TValue seed, + Func add, + Func subtract) { source.ThrowArgumentNullExceptionIfNull(nameof(source)); valueSelector.ThrowArgumentNullExceptionIfNull(nameof(valueSelector)); - return source.Accumulate(0F, t => valueSelector(t).ValueOr(0), (current, value) => current + value, (current, value) => current - value); + return source.Scan(seed, (sum, changes) => + { + foreach (var change in changes) + { + var value = valueSelector(change.Item); + sum = change.Type == AggregateType.Add ? add(sum, value) : subtract(sum, value); + } + + return sum; + }); + } + + private static IObservable SumAggregateNullable( + IObservable> source, + Func valueSelector, + TValue seed, + Func add, + Func subtract) + where TValue : struct + { + valueSelector.ThrowArgumentNullExceptionIfNull(nameof(valueSelector)); + + return SumAggregate(source, item => valueSelector(item).GetValueOrDefault(), seed, add, subtract); } }