diff --git a/README.md b/README.md index da49271..1524e17 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,8 @@ finally - `Collect` - collect items into a custom collection and emit the collection at the end - `ConcatMap` - concatenate in order the inner async sequences mapped from the main sequence - `ConcatWith` - concatenate in order with another async sequence +- `Distinct` - makes sure only distinct elements get relayed +- `DistinctUntilChanged` - relays an element only if it is distinct from the previous item - `Debounce` - wait a bit after each item and emit them if no newer item arrived from the source - `DefaultIfEmpty` - return a fallback value if the source async sequence turns out to be empty - `DoOnNext` - execute an action when an item becomes available diff --git a/async-enumerable-dotnet-test/AsyncEnumerableTest.cs b/async-enumerable-dotnet-test/AsyncEnumerableTest.cs index 7978480..056f387 100644 --- a/async-enumerable-dotnet-test/AsyncEnumerableTest.cs +++ b/async-enumerable-dotnet-test/AsyncEnumerableTest.cs @@ -212,6 +212,7 @@ static AsyncEnumerableTest() Defaults.Add(typeof(Func), (Func)((v, w) => false)); Defaults.Add(typeof(Func>), (Func>)((v, w) => Task.FromResult(false))); Defaults.Add(typeof(Func>), (Func>)(v => Task.FromResult(false))); + Defaults.Add(typeof(Func>), (Func>)(() => null)); Defaults.Add(typeof(Func, IAsyncEnumerable>), (Func, IAsyncEnumerable>)(w => w)); diff --git a/async-enumerable-dotnet-test/DistinctTest.cs b/async-enumerable-dotnet-test/DistinctTest.cs new file mode 100644 index 0000000..3d1c237 --- /dev/null +++ b/async-enumerable-dotnet-test/DistinctTest.cs @@ -0,0 +1,69 @@ +// Copyright (c) David Karnok & Contributors. +// Licensed under the Apache 2.0 License. +// See LICENSE file in the project root for full license information. + +using Xunit; +using async_enumerable_dotnet; +using System.Collections.Generic; + +namespace async_enumerable_dotnet_test +{ + public class DistinctTest + { + [Fact] + public async void Empty() + { + await AsyncEnumerable.Empty() + .Distinct() + .AssertResult(); + } + + [Fact] + public async void Normal() + { + await AsyncEnumerable.Range(1, 5) + .Distinct() + .AssertResult(1, 2, 3, 4, 5); + } + + [Fact] + public async void Redundant() + { + await AsyncEnumerable.FromArray(1, 2, 3, 2, 1, 4, 5, 1, 5) + .Distinct() + .AssertResult(1, 2, 3, 4, 5); + } + + [Fact] + public async void KeySelector() + { + await AsyncEnumerable.Range(1, 5) + .Distinct(v => v % 3) + .AssertResult(1, 2, 3); + } + + [Fact] + public async void EqualityComparer() + { + await AsyncEnumerable.Range(1, 5) + .Distinct(EqualityComparer.Default) + .AssertResult(1, 2, 3, 4, 5); + } + + [Fact] + public async void KeySelector_EqualityComparer() + { + await AsyncEnumerable.Range(1, 5) + .Distinct(v => v % 3, EqualityComparer.Default) + .AssertResult(1, 2, 3); + } + + [Fact] + public async void Custom_Set() + { + await AsyncEnumerable.Range(1, 5) + .Distinct(v => (v % 3), () => new HashSet()) + .AssertResult(1, 2, 3); + } + } +} diff --git a/async-enumerable-dotnet/AsyncEnumerable.cs b/async-enumerable-dotnet/AsyncEnumerable.cs index c061a0b..5922382 100644 --- a/async-enumerable-dotnet/AsyncEnumerable.cs +++ b/async-enumerable-dotnet/AsyncEnumerable.cs @@ -1616,5 +1616,85 @@ public static IAsyncEnumerable Merge(this IAsyncEnumerable(source, boundary, collectionSupplier, maxSize); } + /// + /// Checks if the source items have been seen before by checking if + /// they are already successfully added to a HashSet or not, ensuring + /// only distinct source items pass through. + /// + /// The element type of the source and result. + /// The source to have distinct items only of. + /// The new IAsyncEnumerable sequence. + public static IAsyncEnumerable Distinct(this IAsyncEnumerable source) + { + return Distinct(source, v => v, () => new HashSet()); + } + + /// + /// Checks if the source items have been seen before by checking if + /// they are already successfully added to a HashSet or not, ensuring + /// only distinct source items pass through. + /// + /// The element type of the source and result. + /// The source to have distinct items only of. + /// The comparer for comparing items in the HashSet + /// The new IAsyncEnumerable sequence. + public static IAsyncEnumerable Distinct(this IAsyncEnumerable source, IEqualityComparer comparer) + { + RequireNonNull(comparer, nameof(comparer)); + return Distinct(source, v => v, () => new HashSet(comparer)); + } + + /// + /// Checks if the source items have been seen before by checking if + /// a key extracted from such items is in a HashSet or not, ensuring + /// only distinct source items pass through. + /// + /// The element type of the source and result. + /// The key type used for comparing items. + /// The source to have distinct items only of. + /// The function that receives the source item and should return a key value for distinct comparison. + /// The new IAsyncEnumerable sequence. + public static IAsyncEnumerable Distinct(this IAsyncEnumerable source, Func keySelector) + { + return Distinct(source, keySelector, () => new HashSet()); + } + + /// + /// Checks if the source items have been seen before by checking if + /// a key extracted from such items is in a HashSet or not, ensuring + /// only distinct source items pass through. + /// + /// The element type of the source and result. + /// The key type used for comparing items. + /// The source to have distinct items only of. + /// The function that receives the source item and should return a key value for distinct comparison. + /// The comparer for comparing keys in the HashSet + /// The new IAsyncEnumerable sequence. + public static IAsyncEnumerable Distinct(this IAsyncEnumerable source, Func keySelector, IEqualityComparer keyComparer) + { + RequireNonNull(keyComparer, nameof(keyComparer)); + return Distinct(source, keySelector, () => new HashSet(keyComparer)); + } + + /// + /// Checks if the source items have been seen before by checking + /// a key extracted from such items agains a set of keys and + /// drops such items, ensuring that only distinct source items pass + /// through. + /// + /// The element type of the source and result. + /// The key type used for comparing items. + /// The source to have distinct items only of. + /// The function that receives the source item and should return a key value for distinct comparison. + /// The function that should provide a set implementation whose method's return + /// value decided is the source item should pass or not. + /// The new IAsyncEnumerable sequence + public static IAsyncEnumerable Distinct(this IAsyncEnumerable source, Func keySelector, Func> setSupplier) + { + RequireNonNull(source, nameof(source)); + RequireNonNull(keySelector, nameof(keySelector)); + RequireNonNull(setSupplier, nameof(setSupplier)); + return new Distinct(source, keySelector, setSupplier); + } } } diff --git a/async-enumerable-dotnet/impl/Distinct.cs b/async-enumerable-dotnet/impl/Distinct.cs new file mode 100644 index 0000000..105983c --- /dev/null +++ b/async-enumerable-dotnet/impl/Distinct.cs @@ -0,0 +1,83 @@ +// Copyright (c) David Karnok & Contributors. +// Licensed under the Apache 2.0 License. +// See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace async_enumerable_dotnet.impl +{ + internal sealed class Distinct : IAsyncEnumerable + { + private readonly IAsyncEnumerable _source; + + private readonly Func _keySelector; + + private readonly Func> _collectionSupplier; + + public Distinct(IAsyncEnumerable source, Func keySelector, Func> collectionSupplier) + { + _source = source; + _keySelector = keySelector; + _collectionSupplier = collectionSupplier; + } + + public IAsyncEnumerator GetAsyncEnumerator() + { + ISet collection; + + try + { + collection = _collectionSupplier(); + } + catch (Exception ex) + { + return new Error.ErrorEnumerator(ex); + } + return new DistinctEnumerator(_source.GetAsyncEnumerator(), _keySelector, collection); + } + + private sealed class DistinctEnumerator : IAsyncEnumerator + { + private readonly IAsyncEnumerator _source; + + private readonly Func _keySelector; + + private ISet _collection; + + public TSource Current => _source.Current; + + public DistinctEnumerator(IAsyncEnumerator source, Func keySelector, ISet collection) + { + _source = source; + _keySelector = keySelector; + _collection = collection; + } + + public ValueTask DisposeAsync() + { + _collection = default; + return _source.DisposeAsync(); + } + + public async ValueTask MoveNextAsync() + { + for (; ; ) + { + if (await _source.MoveNextAsync()) + { + if (_collection.Add(_keySelector(_source.Current))) + { + return true; + } + } + else + { + return false; + } + } + } + } + } +}