diff --git a/src/UniTask.NetCore/Linq/DistinctUntilChanged.cs b/src/UniTask.NetCore/Linq/DistinctUntilChanged.cs new file mode 100644 index 0000000..4f25336 --- /dev/null +++ b/src/UniTask.NetCore/Linq/DistinctUntilChanged.cs @@ -0,0 +1,297 @@ +using Cysharp.Threading.Tasks.Internal; +using System; +using System.Collections.Generic; +using System.Threading; + +namespace Cysharp.Threading.Tasks.Linq +{ + public static partial class UniTaskAsyncEnumerable + { + public static IUniTaskAsyncEnumerable DistinctUntilChanged(this IUniTaskAsyncEnumerable source) + { + return DistinctUntilChanged(source, EqualityComparer.Default); + } + + public static IUniTaskAsyncEnumerable DistinctUntilChanged(this IUniTaskAsyncEnumerable source, IEqualityComparer comparer) + { + Error.ThrowArgumentNullException(source, nameof(source)); + Error.ThrowArgumentNullException(comparer, nameof(comparer)); + + return new DistinctUntilChanged(source, comparer); + } + + public static IUniTaskAsyncEnumerable DistinctUntilChanged(this IUniTaskAsyncEnumerable source, Func keySelector) + { + return DistinctUntilChanged(source, keySelector, EqualityComparer.Default); + } + + public static IUniTaskAsyncEnumerable DistinctUntilChanged(this IUniTaskAsyncEnumerable source, Func keySelector, IEqualityComparer comparer) + { + Error.ThrowArgumentNullException(source, nameof(source)); + Error.ThrowArgumentNullException(keySelector, nameof(keySelector)); + Error.ThrowArgumentNullException(comparer, nameof(comparer)); + + return new DistinctUntilChanged(source, keySelector, comparer); + } + + public static IUniTaskAsyncEnumerable DistinctUntilChangedAwait(this IUniTaskAsyncEnumerable source, Func> keySelector) + { + return DistinctUntilChangedAwait(source, keySelector, EqualityComparer.Default); + } + + public static IUniTaskAsyncEnumerable DistinctUntilChangedAwait(this IUniTaskAsyncEnumerable source, Func> keySelector, IEqualityComparer comparer) + { + Error.ThrowArgumentNullException(source, nameof(source)); + Error.ThrowArgumentNullException(keySelector, nameof(keySelector)); + Error.ThrowArgumentNullException(comparer, nameof(comparer)); + + return new DistinctUntilChangedAwait(source, keySelector, comparer); + } + + public static IUniTaskAsyncEnumerable DistinctUntilChangedAwaitWithCancellation(this IUniTaskAsyncEnumerable source, Func> keySelector) + { + return DistinctUntilChangedAwaitWithCancellation(source, keySelector, EqualityComparer.Default); + } + + public static IUniTaskAsyncEnumerable DistinctUntilChangedAwaitWithCancellation(this IUniTaskAsyncEnumerable source, Func> keySelector, IEqualityComparer comparer) + { + Error.ThrowArgumentNullException(source, nameof(source)); + Error.ThrowArgumentNullException(keySelector, nameof(keySelector)); + Error.ThrowArgumentNullException(comparer, nameof(comparer)); + + return new DistinctUntilChangedAwaitCancellation(source, keySelector, comparer); + } + } + + internal sealed class DistinctUntilChanged : IUniTaskAsyncEnumerable + { + readonly IUniTaskAsyncEnumerable source; + readonly IEqualityComparer comparer; + + public DistinctUntilChanged(IUniTaskAsyncEnumerable source, IEqualityComparer comparer) + { + this.source = source; + this.comparer = comparer; + } + + public IUniTaskAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + { + return new Enumerator(source, comparer, cancellationToken); + } + + class Enumerator : AsyncEnumeratorBase + { + readonly IEqualityComparer comparer; + TSource prev; + bool first; + + public Enumerator(IUniTaskAsyncEnumerable source, IEqualityComparer comparer, CancellationToken cancellationToken) + + : base(source, cancellationToken) + { + this.comparer = comparer; + this.first = true; + } + + protected override bool TryMoveNextCore(bool sourceHasCurrent, out bool result) + { + if (sourceHasCurrent) + { + var v = SourceCurrent; + if (first || !comparer.Equals(prev, v)) + { + first = false; + Current = prev = v; + result = true; + return true; + } + else + { + result = default; + return false; + } + } + + result = false; + return true; + } + } + } + + internal sealed class DistinctUntilChanged : IUniTaskAsyncEnumerable + { + readonly IUniTaskAsyncEnumerable source; + readonly Func keySelector; + readonly IEqualityComparer comparer; + + public DistinctUntilChanged(IUniTaskAsyncEnumerable source, Func keySelector, IEqualityComparer comparer) + { + this.source = source; + this.keySelector = keySelector; + this.comparer = comparer; + } + + public IUniTaskAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + { + return new Enumerator(source, keySelector, comparer, cancellationToken); + } + + class Enumerator : AsyncEnumeratorBase + { + readonly IEqualityComparer comparer; + readonly Func keySelector; + TKey prev; + bool first; + + public Enumerator(IUniTaskAsyncEnumerable source, Func keySelector, IEqualityComparer comparer, CancellationToken cancellationToken) + + : base(source, cancellationToken) + { + this.comparer = comparer; + this.keySelector = keySelector; + this.first = true; + } + + protected override bool TryMoveNextCore(bool sourceHasCurrent, out bool result) + { + if (sourceHasCurrent) + { + var v = SourceCurrent; + var key = keySelector(v); + if (first || !comparer.Equals(prev, key)) + { + first = false; + prev = key; + Current = v; + result = true; + return true; + } + else + { + result = default; + return false; + } + } + + result = false; + return true; + } + } + } + + internal sealed class DistinctUntilChangedAwait : IUniTaskAsyncEnumerable + { + readonly IUniTaskAsyncEnumerable source; + readonly Func> keySelector; + readonly IEqualityComparer comparer; + + public DistinctUntilChangedAwait(IUniTaskAsyncEnumerable source, Func> keySelector, IEqualityComparer comparer) + { + this.source = source; + this.keySelector = keySelector; + this.comparer = comparer; + } + + public IUniTaskAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + { + return new Enumerator(source, keySelector, comparer, cancellationToken); + } + + class Enumerator : AsyncEnumeratorAwaitSelectorBase + { + readonly IEqualityComparer comparer; + readonly Func> keySelector; + TKey prev; + bool first; + + public Enumerator(IUniTaskAsyncEnumerable source, Func> keySelector, IEqualityComparer comparer, CancellationToken cancellationToken) + + : base(source, cancellationToken) + { + this.comparer = comparer; + this.keySelector = keySelector; + this.first = true; + } + + protected override UniTask TransformAsync(TSource sourceCurrent) + { + return keySelector(sourceCurrent); + } + + protected override bool TrySetCurrentCore(TKey key, out bool terminateIteration) + { + if (first || !comparer.Equals(prev, key)) + { + first = false; + prev = key; + Current = SourceCurrent; + terminateIteration = false; + return true; + } + else + { + terminateIteration = false; + return false; + } + } + } + } + + internal sealed class DistinctUntilChangedAwaitCancellation : IUniTaskAsyncEnumerable + { + readonly IUniTaskAsyncEnumerable source; + readonly Func> keySelector; + readonly IEqualityComparer comparer; + + public DistinctUntilChangedAwaitCancellation(IUniTaskAsyncEnumerable source, Func> keySelector, IEqualityComparer comparer) + { + this.source = source; + this.keySelector = keySelector; + this.comparer = comparer; + } + + public IUniTaskAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + { + return new Enumerator(source, keySelector, comparer, cancellationToken); + } + + class Enumerator : AsyncEnumeratorAwaitSelectorBase + { + readonly IEqualityComparer comparer; + readonly Func> keySelector; + TKey prev; + bool first; + + public Enumerator(IUniTaskAsyncEnumerable source, Func> keySelector, IEqualityComparer comparer, CancellationToken cancellationToken) + + : base(source, cancellationToken) + { + this.comparer = comparer; + this.keySelector = keySelector; + this.first = true; + } + + protected override UniTask TransformAsync(TSource sourceCurrent) + { + return keySelector(sourceCurrent, cancellationToken); + } + + protected override bool TrySetCurrentCore(TKey key, out bool terminateIteration) + { + if (first || !comparer.Equals(prev, key)) + { + first = false; + prev = key; + Current = SourceCurrent; + terminateIteration = false; + return true; + } + else + { + terminateIteration = false; + return false; + } + } + } + } +} \ No newline at end of file diff --git a/src/UniTask.NetCoreTests/Linq/Sets.cs b/src/UniTask.NetCoreTests/Linq/Sets.cs index e730f61..6d3c45c 100644 --- a/src/UniTask.NetCoreTests/Linq/Sets.cs +++ b/src/UniTask.NetCoreTests/Linq/Sets.cs @@ -65,6 +65,44 @@ namespace NetCoreTests.Linq } } + [Theory] + [MemberData(nameof(array1))] + public async Task DistinctUntilChanged(int[] array) + { + var ys = await array.ToAsyncEnumerable().DistinctUntilChanged().ToArrayAsync(); + { + (await array.ToUniTaskAsyncEnumerable().DistinctUntilChanged().ToArrayAsync()).Should().BeEquivalentTo(ys); + (await array.ToUniTaskAsyncEnumerable().DistinctUntilChanged(x => x).ToArrayAsync()).Should().BeEquivalentTo(ys); + (await array.ToUniTaskAsyncEnumerable().DistinctUntilChangedAwait(x => UniTask.Run(() => x)).ToArrayAsync()).Should().BeEquivalentTo(ys); + (await array.ToUniTaskAsyncEnumerable().DistinctUntilChangedAwaitWithCancellation((x, _) => UniTask.Run(() => x)).ToArrayAsync()).Should().BeEquivalentTo(ys); + } + } + + [Fact] + public async Task DistinctUntilChangedThrow() + { + foreach (var item in UniTaskTestException.Throws()) + { + { + var xs = item.DistinctUntilChanged().ToArrayAsync(); + await Assert.ThrowsAsync(async () => await xs); + } + { + var xs = item.DistinctUntilChanged(x => x).ToArrayAsync(); + await Assert.ThrowsAsync(async () => await xs); + } + { + var xs = item.DistinctUntilChangedAwait(x => UniTask.Run(() => x)).ToArrayAsync(); + await Assert.ThrowsAsync(async () => await xs); + } + { + var xs = item.DistinctUntilChangedAwaitWithCancellation((x, _) => UniTask.Run(() => x)).ToArrayAsync(); + await Assert.ThrowsAsync(async () => await xs); + } + } + } + + [Fact] public async Task Except() { diff --git a/src/UniTask.NetCoreTests/UniTask.NetCoreTests.csproj b/src/UniTask.NetCoreTests/UniTask.NetCoreTests.csproj index 240d795..aa0765b 100644 --- a/src/UniTask.NetCoreTests/UniTask.NetCoreTests.csproj +++ b/src/UniTask.NetCoreTests/UniTask.NetCoreTests.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.1 @@ -11,6 +11,8 @@ + +