From 635691e507f2c632b3fd85882de369e9b1d098ec Mon Sep 17 00:00:00 2001 From: Hazel Date: Thu, 22 Feb 2024 13:35:18 -0600 Subject: [PATCH] Added support for passing `Range` as a parameter to `.Exclude`. (#619) Co-authored-by: Taco <65432314+tradinglounge@users.noreply.github.com> Co-authored-by: Stuart Turner --- Source/SuperLinq/Exclude.cs | 164 ++++++++++++++++-- .../PublicAPI/net6.0/PublicAPI.Unshipped.txt | 1 + .../PublicAPI/net7.0/PublicAPI.Unshipped.txt | 1 + .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 1 + .../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 1 + .../netcoreapp3.1/PublicAPI.Unshipped.txt | 1 + Tests/SuperLinq.Test/ExcludeTest.cs | 69 +++++++- 7 files changed, 220 insertions(+), 18 deletions(-) diff --git a/Source/SuperLinq/Exclude.cs b/Source/SuperLinq/Exclude.cs index 065f6506..ba04e640 100644 --- a/Source/SuperLinq/Exclude.cs +++ b/Source/SuperLinq/Exclude.cs @@ -1,4 +1,4 @@ -namespace SuperLinq; +namespace SuperLinq; public static partial class SuperEnumerable { @@ -35,43 +35,98 @@ public static IEnumerable Exclude(this IEnumerable sequence, int startI ArgumentOutOfRangeException.ThrowIfNegative(startIndex); ArgumentOutOfRangeException.ThrowIfNegative(count); - if (count == 0) - return sequence; - - return sequence switch + return (count, sequence) switch { - IList list => new ExcludeListIterator(list, startIndex, count), - ICollection collection => new ExcludeCollectionIterator(collection, startIndex, count), + (0, _) => sequence, + (_, IList list) => new ExcludeListIterator(list, startIndex, count), + (_, ICollection collection) => new ExcludeCollectionIterator(collection, startIndex, count), _ => ExcludeCore(sequence, startIndex, count) }; } - private sealed class ExcludeCollectionIterator( - ICollection source, - int startIndex, - int count - ) : CollectionIterator + /// + /// Excludes a contiguous number of elements from a sequence starting at a given index. + /// + /// + /// The type of the elements of the sequence + /// + /// + /// The sequence to exclude elements from + /// + /// + /// The zero-based index at which to begin excluding elements + /// + /// + /// A sequence that excludes the specified portion of elements + /// + /// + /// is . + /// + /// + /// or is less than 0. + /// + /// + /// This method uses deferred execution and streams its results. + /// + public static IEnumerable Exclude(this IEnumerable sequence, Range range) + { + ArgumentNullException.ThrowIfNull(sequence); + + var startFromEnd = range.Start.IsFromEnd; + var endFromEnd = range.End.IsFromEnd; + if ((startFromEnd, endFromEnd) == (false, false)) + { + return Exclude(sequence, range.Start.Value, range.End.Value - range.Start.Value); + } + + if (sequence.TryGetCollectionCount() is int count) + { + var (start, length) = range.GetOffsetAndLength(count); + return Exclude(sequence, start, length); + } + + return (startFromEnd, endFromEnd) switch + { + (false, true) => ExcludeEndFromEnd(sequence, range), + (true, false) => ExcludeStartFromEnd(sequence, range), + (true, true) when range.Start.Value < range.End.Value => + ThrowHelper.ThrowArgumentOutOfRangeException>("length"), + _ => ExcludeRange(sequence, range), + }; + } + + /// + /// Represents an iterator for excluding elements from a collection. + /// + /// Specifies the type of elements in the collection. + private sealed class ExcludeCollectionIterator(ICollection source, int startIndex, int count) + : CollectionIterator { + /// + /// Gets the number of elements in the source collection after excluding the specified portion of elements. + /// public override int Count => source.Count < startIndex ? source.Count : source.Count < startIndex + count ? startIndex : source.Count - count; + /// protected override IEnumerable GetEnumerable() => ExcludeCore(source, startIndex, count); } - private sealed class ExcludeListIterator( - IList source, - int startIndex, - int count - ) : ListIterator + private sealed class ExcludeListIterator(IList source, int startIndex, int count) + : ListIterator { + /// + /// Gets the number of elements in the source collection after excluding the specified portion of elements. + /// public override int Count => source.Count < startIndex ? source.Count : source.Count < startIndex + count ? startIndex : source.Count - count; + /// protected override IEnumerable GetEnumerable() { var cnt = (uint)source.Count; @@ -82,6 +137,7 @@ protected override IEnumerable GetEnumerable() yield return source[i]; } + /// protected override T ElementAt(int index) { ArgumentOutOfRangeException.ThrowIfNegative(index); @@ -102,7 +158,81 @@ private static IEnumerable ExcludeCore(IEnumerable sequence, int startI { if (index < startIndex || index >= endIndex) yield return item; + index++; } } + + private static IEnumerable ExcludeRange(IEnumerable sequence, Range range) + { + var start = range.Start.Value; + var queue = new Queue(start + 1); + foreach (var e in sequence) + { + queue.Enqueue(e); + if (queue.Count > start) + yield return queue.Dequeue(); + } + + start = Math.Min(start, queue.Count); + var length = start - range.End.Value; + while (length > 0) + { + if (!queue.TryDequeue(out var _)) + yield break; + length--; + } + + while (queue.TryDequeue(out var element)) + yield return element; + } + + private static IEnumerable ExcludeStartFromEnd(IEnumerable sequence, Range range) + { + var count = 0; + var start = range.Start.Value; + var queue = new Queue(start + 1); + foreach (var e in sequence) + { + count++; + queue.Enqueue(e); + if (queue.Count > start) + yield return queue.Dequeue(); + } + + start = Math.Max(range.Start.GetOffset(count), 0); + var length = range.End.Value - start; + + while (length > 0) + { + if (!queue.TryDequeue(out var _)) + yield break; + length--; + } + + while (queue.TryDequeue(out var element)) + yield return element; + } + + private static IEnumerable ExcludeEndFromEnd(IEnumerable sequence, Range range) + { + var count = 0; + var start = range.Start.Value; + var end = range.End.Value; + var queue = new Queue(end + 1); + foreach (var e in sequence) + { + count++; + queue.Enqueue(e); + if (queue.Count > end) + { + var el = queue.Dequeue(); + if ((count - end) <= start) + yield return el; + } + } + + while (queue.TryDequeue(out var element)) + yield return element; + } } diff --git a/Source/SuperLinq/PublicAPI/net6.0/PublicAPI.Unshipped.txt b/Source/SuperLinq/PublicAPI/net6.0/PublicAPI.Unshipped.txt index c83c6c80..8ac07beb 100644 --- a/Source/SuperLinq/PublicAPI/net6.0/PublicAPI.Unshipped.txt +++ b/Source/SuperLinq/PublicAPI/net6.0/PublicAPI.Unshipped.txt @@ -95,6 +95,7 @@ static SuperLinq.SuperEnumerable.DenseRank(this System.Collections.Gene static SuperLinq.SuperEnumerable.DenseRank(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IComparer! comparer, SuperLinq.OrderByDirection sortDirection) -> System.Collections.Generic.IEnumerable<(TSource item, int rank)>! static SuperLinq.SuperEnumerable.DenseRankBy(this System.Collections.Generic.IEnumerable! source, System.Func! keySelector, SuperLinq.OrderByDirection sortDirection) -> System.Collections.Generic.IEnumerable<(TSource item, int rank)>! static SuperLinq.SuperEnumerable.DenseRankBy(this System.Collections.Generic.IEnumerable! source, System.Func! keySelector, System.Collections.Generic.IComparer! comparer, SuperLinq.OrderByDirection sortDirection) -> System.Collections.Generic.IEnumerable<(TSource item, int rank)>! +static SuperLinq.SuperEnumerable.Exclude(this System.Collections.Generic.IEnumerable! sequence, System.Range range) -> System.Collections.Generic.IEnumerable! static SuperLinq.SuperEnumerable.FullOuterHashJoin(this System.Collections.Generic.IEnumerable! left, System.Collections.Generic.IEnumerable! right, System.Func! leftKeySelector, System.Func! rightKeySelector, System.Func! leftResultSelector, System.Func! rightResultSelector, System.Func! bothResultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) -> System.Collections.Generic.IEnumerable! static SuperLinq.SuperEnumerable.FullOuterHashJoin(this System.Collections.Generic.IEnumerable! left, System.Collections.Generic.IEnumerable! right, System.Func! leftKeySelector, System.Func! rightKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) -> System.Collections.Generic.IEnumerable<(TLeft? Left, TRight? Right)>! static SuperLinq.SuperEnumerable.FullOuterMergeJoin(this System.Collections.Generic.IEnumerable! left, System.Collections.Generic.IEnumerable! right, System.Func! leftKeySelector, System.Func! rightKeySelector, System.Func! leftResultSelector, System.Func! rightResultSelector, System.Func! bothResultSelector, System.Collections.Generic.IComparer? comparer = null) -> System.Collections.Generic.IEnumerable! diff --git a/Source/SuperLinq/PublicAPI/net7.0/PublicAPI.Unshipped.txt b/Source/SuperLinq/PublicAPI/net7.0/PublicAPI.Unshipped.txt index c83c6c80..8ac07beb 100644 --- a/Source/SuperLinq/PublicAPI/net7.0/PublicAPI.Unshipped.txt +++ b/Source/SuperLinq/PublicAPI/net7.0/PublicAPI.Unshipped.txt @@ -95,6 +95,7 @@ static SuperLinq.SuperEnumerable.DenseRank(this System.Collections.Gene static SuperLinq.SuperEnumerable.DenseRank(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IComparer! comparer, SuperLinq.OrderByDirection sortDirection) -> System.Collections.Generic.IEnumerable<(TSource item, int rank)>! static SuperLinq.SuperEnumerable.DenseRankBy(this System.Collections.Generic.IEnumerable! source, System.Func! keySelector, SuperLinq.OrderByDirection sortDirection) -> System.Collections.Generic.IEnumerable<(TSource item, int rank)>! static SuperLinq.SuperEnumerable.DenseRankBy(this System.Collections.Generic.IEnumerable! source, System.Func! keySelector, System.Collections.Generic.IComparer! comparer, SuperLinq.OrderByDirection sortDirection) -> System.Collections.Generic.IEnumerable<(TSource item, int rank)>! +static SuperLinq.SuperEnumerable.Exclude(this System.Collections.Generic.IEnumerable! sequence, System.Range range) -> System.Collections.Generic.IEnumerable! static SuperLinq.SuperEnumerable.FullOuterHashJoin(this System.Collections.Generic.IEnumerable! left, System.Collections.Generic.IEnumerable! right, System.Func! leftKeySelector, System.Func! rightKeySelector, System.Func! leftResultSelector, System.Func! rightResultSelector, System.Func! bothResultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) -> System.Collections.Generic.IEnumerable! static SuperLinq.SuperEnumerable.FullOuterHashJoin(this System.Collections.Generic.IEnumerable! left, System.Collections.Generic.IEnumerable! right, System.Func! leftKeySelector, System.Func! rightKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) -> System.Collections.Generic.IEnumerable<(TLeft? Left, TRight? Right)>! static SuperLinq.SuperEnumerable.FullOuterMergeJoin(this System.Collections.Generic.IEnumerable! left, System.Collections.Generic.IEnumerable! right, System.Func! leftKeySelector, System.Func! rightKeySelector, System.Func! leftResultSelector, System.Func! rightResultSelector, System.Func! bothResultSelector, System.Collections.Generic.IComparer? comparer = null) -> System.Collections.Generic.IEnumerable! diff --git a/Source/SuperLinq/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/Source/SuperLinq/PublicAPI/net8.0/PublicAPI.Unshipped.txt index c83c6c80..8ac07beb 100644 --- a/Source/SuperLinq/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/Source/SuperLinq/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -95,6 +95,7 @@ static SuperLinq.SuperEnumerable.DenseRank(this System.Collections.Gene static SuperLinq.SuperEnumerable.DenseRank(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IComparer! comparer, SuperLinq.OrderByDirection sortDirection) -> System.Collections.Generic.IEnumerable<(TSource item, int rank)>! static SuperLinq.SuperEnumerable.DenseRankBy(this System.Collections.Generic.IEnumerable! source, System.Func! keySelector, SuperLinq.OrderByDirection sortDirection) -> System.Collections.Generic.IEnumerable<(TSource item, int rank)>! static SuperLinq.SuperEnumerable.DenseRankBy(this System.Collections.Generic.IEnumerable! source, System.Func! keySelector, System.Collections.Generic.IComparer! comparer, SuperLinq.OrderByDirection sortDirection) -> System.Collections.Generic.IEnumerable<(TSource item, int rank)>! +static SuperLinq.SuperEnumerable.Exclude(this System.Collections.Generic.IEnumerable! sequence, System.Range range) -> System.Collections.Generic.IEnumerable! static SuperLinq.SuperEnumerable.FullOuterHashJoin(this System.Collections.Generic.IEnumerable! left, System.Collections.Generic.IEnumerable! right, System.Func! leftKeySelector, System.Func! rightKeySelector, System.Func! leftResultSelector, System.Func! rightResultSelector, System.Func! bothResultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) -> System.Collections.Generic.IEnumerable! static SuperLinq.SuperEnumerable.FullOuterHashJoin(this System.Collections.Generic.IEnumerable! left, System.Collections.Generic.IEnumerable! right, System.Func! leftKeySelector, System.Func! rightKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) -> System.Collections.Generic.IEnumerable<(TLeft? Left, TRight? Right)>! static SuperLinq.SuperEnumerable.FullOuterMergeJoin(this System.Collections.Generic.IEnumerable! left, System.Collections.Generic.IEnumerable! right, System.Func! leftKeySelector, System.Func! rightKeySelector, System.Func! leftResultSelector, System.Func! rightResultSelector, System.Func! bothResultSelector, System.Collections.Generic.IComparer? comparer = null) -> System.Collections.Generic.IEnumerable! diff --git a/Source/SuperLinq/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/Source/SuperLinq/PublicAPI/net9.0/PublicAPI.Unshipped.txt index a4a076e5..67f54673 100644 --- a/Source/SuperLinq/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ b/Source/SuperLinq/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -95,6 +95,7 @@ static SuperLinq.SuperEnumerable.DenseRank(this System.Collections.Gene static SuperLinq.SuperEnumerable.DenseRank(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IComparer! comparer, SuperLinq.OrderByDirection sortDirection) -> System.Collections.Generic.IEnumerable<(TSource item, int rank)>! static SuperLinq.SuperEnumerable.DenseRankBy(this System.Collections.Generic.IEnumerable! source, System.Func! keySelector, SuperLinq.OrderByDirection sortDirection) -> System.Collections.Generic.IEnumerable<(TSource item, int rank)>! static SuperLinq.SuperEnumerable.DenseRankBy(this System.Collections.Generic.IEnumerable! source, System.Func! keySelector, System.Collections.Generic.IComparer! comparer, SuperLinq.OrderByDirection sortDirection) -> System.Collections.Generic.IEnumerable<(TSource item, int rank)>! +static SuperLinq.SuperEnumerable.Exclude(this System.Collections.Generic.IEnumerable! sequence, System.Range range) -> System.Collections.Generic.IEnumerable! static SuperLinq.SuperEnumerable.FullOuterHashJoin(this System.Collections.Generic.IEnumerable! left, System.Collections.Generic.IEnumerable! right, System.Func! leftKeySelector, System.Func! rightKeySelector, System.Func! leftResultSelector, System.Func! rightResultSelector, System.Func! bothResultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) -> System.Collections.Generic.IEnumerable! static SuperLinq.SuperEnumerable.FullOuterHashJoin(this System.Collections.Generic.IEnumerable! left, System.Collections.Generic.IEnumerable! right, System.Func! leftKeySelector, System.Func! rightKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) -> System.Collections.Generic.IEnumerable<(TLeft? Left, TRight? Right)>! static SuperLinq.SuperEnumerable.FullOuterMergeJoin(this System.Collections.Generic.IEnumerable! left, System.Collections.Generic.IEnumerable! right, System.Func! leftKeySelector, System.Func! rightKeySelector, System.Func! leftResultSelector, System.Func! rightResultSelector, System.Func! bothResultSelector, System.Collections.Generic.IComparer? comparer = null) -> System.Collections.Generic.IEnumerable! diff --git a/Source/SuperLinq/PublicAPI/netcoreapp3.1/PublicAPI.Unshipped.txt b/Source/SuperLinq/PublicAPI/netcoreapp3.1/PublicAPI.Unshipped.txt index c83c6c80..8ac07beb 100644 --- a/Source/SuperLinq/PublicAPI/netcoreapp3.1/PublicAPI.Unshipped.txt +++ b/Source/SuperLinq/PublicAPI/netcoreapp3.1/PublicAPI.Unshipped.txt @@ -95,6 +95,7 @@ static SuperLinq.SuperEnumerable.DenseRank(this System.Collections.Gene static SuperLinq.SuperEnumerable.DenseRank(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IComparer! comparer, SuperLinq.OrderByDirection sortDirection) -> System.Collections.Generic.IEnumerable<(TSource item, int rank)>! static SuperLinq.SuperEnumerable.DenseRankBy(this System.Collections.Generic.IEnumerable! source, System.Func! keySelector, SuperLinq.OrderByDirection sortDirection) -> System.Collections.Generic.IEnumerable<(TSource item, int rank)>! static SuperLinq.SuperEnumerable.DenseRankBy(this System.Collections.Generic.IEnumerable! source, System.Func! keySelector, System.Collections.Generic.IComparer! comparer, SuperLinq.OrderByDirection sortDirection) -> System.Collections.Generic.IEnumerable<(TSource item, int rank)>! +static SuperLinq.SuperEnumerable.Exclude(this System.Collections.Generic.IEnumerable! sequence, System.Range range) -> System.Collections.Generic.IEnumerable! static SuperLinq.SuperEnumerable.FullOuterHashJoin(this System.Collections.Generic.IEnumerable! left, System.Collections.Generic.IEnumerable! right, System.Func! leftKeySelector, System.Func! rightKeySelector, System.Func! leftResultSelector, System.Func! rightResultSelector, System.Func! bothResultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) -> System.Collections.Generic.IEnumerable! static SuperLinq.SuperEnumerable.FullOuterHashJoin(this System.Collections.Generic.IEnumerable! left, System.Collections.Generic.IEnumerable! right, System.Func! leftKeySelector, System.Func! rightKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) -> System.Collections.Generic.IEnumerable<(TLeft? Left, TRight? Right)>! static SuperLinq.SuperEnumerable.FullOuterMergeJoin(this System.Collections.Generic.IEnumerable! left, System.Collections.Generic.IEnumerable! right, System.Func! leftKeySelector, System.Func! rightKeySelector, System.Func! leftResultSelector, System.Func! rightResultSelector, System.Func! bothResultSelector, System.Collections.Generic.IComparer? comparer = null) -> System.Collections.Generic.IEnumerable! diff --git a/Tests/SuperLinq.Test/ExcludeTest.cs b/Tests/SuperLinq.Test/ExcludeTest.cs index 2aa83322..fb7f204b 100644 --- a/Tests/SuperLinq.Test/ExcludeTest.cs +++ b/Tests/SuperLinq.Test/ExcludeTest.cs @@ -1,4 +1,4 @@ -namespace Test; +namespace Test; /// /// Verify the behavior of the Exclude operator @@ -171,4 +171,71 @@ public void ExcludeCollectionBehavior() result.AssertCollectionErrorChecking(9_000); } + + public static IEnumerable GetExcludeRangeCases() => + [ + [3..7, false, false, new int[] { 0, 1, 2, 7, 8, 9 }], + [3..3, false, false, new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }], + [3..2, true, true, Array.Empty()], + [3..15, false, false, new int[] { 0, 1, 2 }], + + [3..^3, false, false, new int[] { 0, 1, 2, 7, 8, 9 }], + [6..^3, false, false, new int[] { 0, 1, 2, 3, 4, 5, 7, 8, 9 }], + [7..^3, false, false, new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }], + [8..^3, false, true, new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }], + [15..^3, false, true, new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }], + [3..^15, false, true, new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }], + + [^7..2, false, true, new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }], + [^7..3, false, false, new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }], + [^7..4, false, false, new int[] { 0, 1, 2, 4, 5, 6, 7, 8, 9 }], + [^7..7, false, false, new int[] { 0, 1, 2, 7, 8, 9 }], + [^7..15, false, true, new int[] { 0, 1, 2 }], + [^15..7, false, true, new int[] { 7, 8, 9 }], + + [^2..^3, true, true, Array.Empty()], + [^3..^3, false, false, new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }], + [^7..^3, false, false, new int[] { 0, 1, 2, 7, 8, 9 }], + [^15..^3, false, true, new int[] { 7, 8, 9 }], + ]; + + [Theory] + [MemberData(nameof(GetExcludeRangeCases))] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1026:Theory methods should use all of their parameters")] + public void ExcludeRangeBehavior(Range range, bool shouldThrow, bool __, int[] expected) + { + using var ts = Enumerable.Range(0, 10) + .AsTestingSequence(); + + if (shouldThrow) + { + _ = Assert.Throws(() => + ts.Exclude(range)); + return; + } + + var result = ts.Exclude(range); + + result.AssertSequenceEqual(expected); + } + + [Theory] + [MemberData(nameof(GetExcludeRangeCases))] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1026:Theory methods should use all of their parameters")] + public void ExcludeRangeCollectionBehavior(Range range, bool __, bool shouldThrow, int[] expected) + { + using var ts = Enumerable.Range(0, 10) + .AsTestingCollection(); + + if (shouldThrow) + { + _ = Assert.Throws(() => + ts.Exclude(range)); + return; + } + + var result = ts.Exclude(range); + + result.AssertSequenceEqual(expected); + } }