From cf2b725a43710fe211ab13298e624c89fa56a087 Mon Sep 17 00:00:00 2001 From: ArmoryNode <22787155+ArmoryNode@users.noreply.github.com> Date: Mon, 30 Dec 2024 15:23:12 -0800 Subject: [PATCH] Add "SkipLastWhile" extension This is a squashed merge of PR #1085 that closes #1036. --------- Co-authored-by: Atif Aziz --- MoreLinq.Test/SkipLastWhileTest.cs | 101 ++++++++++++++++++ MoreLinq/Extensions.g.cs | 26 +++++ MoreLinq/MoreLinq.csproj | 1 + .../PublicAPI/net6.0/PublicAPI.Unshipped.txt | 3 + .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 3 + .../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 3 + .../netstandard2.0/PublicAPI.Unshipped.txt | 3 + .../netstandard2.1/PublicAPI.Unshipped.txt | 3 + MoreLinq/SkipLastWhile.cs | 88 +++++++++++++++ README.md | 4 + bld/Copyright.props | 1 + 11 files changed, 236 insertions(+) create mode 100644 MoreLinq.Test/SkipLastWhileTest.cs create mode 100644 MoreLinq/SkipLastWhile.cs diff --git a/MoreLinq.Test/SkipLastWhileTest.cs b/MoreLinq.Test/SkipLastWhileTest.cs new file mode 100644 index 000000000..01f045a7b --- /dev/null +++ b/MoreLinq.Test/SkipLastWhileTest.cs @@ -0,0 +1,101 @@ +#region License and Terms +// MoreLINQ - Extensions to LINQ to Objects +// Copyright (c) 2024 Andy Romero (armorynode). All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#endregion + +namespace MoreLinq.Test +{ + using System.Collections.Generic; + using NUnit.Framework; + + [TestFixture] + public class SkipLastWhileTest + { + [Test] + public void IsLazy() + { + _ = new BreakingSequence().SkipLastWhile(BreakingFunc.Of()); + } + + [TestCase(SourceKind.Sequence)] + [TestCase(SourceKind.BreakingList)] + [TestCase(SourceKind.BreakingReadOnlyList)] + public void PredicateNeverFalse(SourceKind sourceKind) + { + using var sequence = TestingSequence.Of(0, 1, 2, 3, 4); + + Assert.That(sequence.ToSourceKind(sourceKind).SkipLastWhile(x => x < 5), Is.Empty); + } + + [TestCase(SourceKind.Sequence)] + [TestCase(SourceKind.BreakingList)] + [TestCase(SourceKind.BreakingReadOnlyList)] + public void PredicateNeverTrue(SourceKind sourceKind) + { + using var sequence = TestingSequence.Of(0, 1, 2, 3, 4); + + sequence.ToSourceKind(sourceKind) + .SkipLastWhile(x => x == 100) + .AssertSequenceEqual(0, 1, 2, 3, 4); + } + + [TestCase(SourceKind.Sequence)] + [TestCase(SourceKind.BreakingList)] + [TestCase(SourceKind.BreakingReadOnlyList)] + public void PredicateBecomesTruePartWay(SourceKind sourceKind) + { + using var sequence = TestingSequence.Of(0, 1, 2, 3, 4); + + sequence.ToSourceKind(sourceKind) + .SkipLastWhile(x => x > 2) + .AssertSequenceEqual(0, 1, 2); + } + + [TestCase(SourceKind.Sequence)] + [TestCase(SourceKind.BreakingList)] + [TestCase(SourceKind.BreakingReadOnlyList)] + public void NeverEvaluatesPredicateWhenSourceIsEmpty(SourceKind sourceKind) + { + using var sequence = TestingSequence.Of(); + + Assert.That(sequence.ToSourceKind(sourceKind) + .SkipLastWhile(BreakingFunc.Of()), + Is.Empty); + } + + [TestCase(SourceKind.Sequence)] + [TestCase(SourceKind.BreakingList)] + [TestCase(SourceKind.BreakingReadOnlyList)] + public void UsesCollectionCountAtIterationTime(SourceKind sourceKind) + { + var list = new List { 1, 2, 3, 4 }; + var result = list.ToSourceKind(sourceKind).SkipLastWhile(x => x > 2); + list.Add(5); + result.AssertSequenceEqual(1, 2); + } + + [TestCase(SourceKind.Sequence)] + [TestCase(SourceKind.BreakingList)] + [TestCase(SourceKind.BreakingReadOnlyList)] + public void KeepsNonTrailingItemsThatMatchPredicate(SourceKind sourceKind) + { + using var sequence = TestingSequence.Of(1, 2, 0, 0, 3, 4, 0, 0); + + sequence.ToSourceKind(sourceKind) + .SkipLastWhile(x => x == 0) + .AssertSequenceEqual(1, 2, 0, 0, 3, 4); + } + } +} diff --git a/MoreLinq/Extensions.g.cs b/MoreLinq/Extensions.g.cs index 6d1802e77..52f76097c 100644 --- a/MoreLinq/Extensions.g.cs +++ b/MoreLinq/Extensions.g.cs @@ -5460,6 +5460,32 @@ public static IEnumerable SkipLast(this IEnumerable source, int count) } + /// SkipLastWhile extension. + + [GeneratedCode("MoreLinq.ExtensionsGenerator", "1.0.0.0")] + public static partial class SkipLastWhileExtension + { + /// + /// Removes elements from the end of a sequence as long as a specified condition is true. + /// + /// Type of the source sequence. + /// The source sequence. + /// The predicate to use to remove items from the tail of the sequence. + /// + /// An containing the source sequence elements except for the bypassed ones at the end. + /// + /// The source sequence is . + /// The predicate is . + /// + /// This operator uses deferred execution and streams its results. At any given time, it + /// will buffer as many consecutive elements as satisfied by . + /// + + public static IEnumerable SkipLastWhile(this IEnumerable source, Func predicate) + => MoreEnumerable.SkipLastWhile(source, predicate); + + } + /// SkipUntil extension. [GeneratedCode("MoreLinq.ExtensionsGenerator", "1.0.0.0")] diff --git a/MoreLinq/MoreLinq.csproj b/MoreLinq/MoreLinq.csproj index fa1b9c68d..90df1ed45 100644 --- a/MoreLinq/MoreLinq.csproj +++ b/MoreLinq/MoreLinq.csproj @@ -86,6 +86,7 @@ - Sequence - Shuffle - SkipLast + - SkipLastWhile - SkipUntil - Slice - SortedMerge diff --git a/MoreLinq/PublicAPI/net6.0/PublicAPI.Unshipped.txt b/MoreLinq/PublicAPI/net6.0/PublicAPI.Unshipped.txt index 7dc5c5811..6adcc5bb5 100644 --- a/MoreLinq/PublicAPI/net6.0/PublicAPI.Unshipped.txt +++ b/MoreLinq/PublicAPI/net6.0/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +MoreLinq.Extensions.SkipLastWhileExtension +static MoreLinq.Extensions.SkipLastWhileExtension.SkipLastWhile(this System.Collections.Generic.IEnumerable! source, System.Func! predicate) -> System.Collections.Generic.IEnumerable! +static MoreLinq.MoreEnumerable.SkipLastWhile(this System.Collections.Generic.IEnumerable! source, System.Func! predicate) -> System.Collections.Generic.IEnumerable! diff --git a/MoreLinq/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/MoreLinq/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 5b1195af3..bab15dae1 100644 --- a/MoreLinq/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/MoreLinq/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -1,4 +1,7 @@ #nullable enable +MoreLinq.Extensions.SkipLastWhileExtension +static MoreLinq.Extensions.SkipLastWhileExtension.SkipLastWhile(this System.Collections.Generic.IEnumerable! source, System.Func! predicate) -> System.Collections.Generic.IEnumerable! static MoreLinq.MoreEnumerable.Sequence(T start) -> System.Collections.Generic.IEnumerable! static MoreLinq.MoreEnumerable.Sequence(T start, T stop) -> System.Collections.Generic.IEnumerable! static MoreLinq.MoreEnumerable.Sequence(T start, T stop, T step) -> System.Collections.Generic.IEnumerable! +static MoreLinq.MoreEnumerable.SkipLastWhile(this System.Collections.Generic.IEnumerable! source, System.Func! predicate) -> System.Collections.Generic.IEnumerable! diff --git a/MoreLinq/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/MoreLinq/PublicAPI/net9.0/PublicAPI.Unshipped.txt index d59ac1739..00c6971c9 100644 --- a/MoreLinq/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ b/MoreLinq/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -94,6 +94,7 @@ MoreLinq.Extensions.ShuffleExtension MoreLinq.Extensions.SingleExtension MoreLinq.Extensions.SingleOrDefaultExtension MoreLinq.Extensions.SkipLastExtension +MoreLinq.Extensions.SkipLastWhileExtension MoreLinq.Extensions.SkipUntilExtension MoreLinq.Extensions.SliceExtension MoreLinq.Extensions.SortedMergeExtension @@ -339,6 +340,7 @@ static MoreLinq.Extensions.ShuffleExtension.Shuffle(this System.Collections.G static MoreLinq.Extensions.SingleExtension.Single(this MoreLinq.IExtremaEnumerable! source) -> T static MoreLinq.Extensions.SingleOrDefaultExtension.SingleOrDefault(this MoreLinq.IExtremaEnumerable! source) -> T? static MoreLinq.Extensions.SkipLastExtension.SkipLast(this System.Collections.Generic.IEnumerable! source, int count) -> System.Collections.Generic.IEnumerable! +static MoreLinq.Extensions.SkipLastWhileExtension.SkipLastWhile(this System.Collections.Generic.IEnumerable! source, System.Func! predicate) -> System.Collections.Generic.IEnumerable! static MoreLinq.Extensions.SkipUntilExtension.SkipUntil(this System.Collections.Generic.IEnumerable! source, System.Func! predicate) -> System.Collections.Generic.IEnumerable! static MoreLinq.Extensions.SliceExtension.Slice(this System.Collections.Generic.IEnumerable! sequence, int startIndex, int count) -> System.Collections.Generic.IEnumerable! static MoreLinq.Extensions.SortedMergeExtension.SortedMerge(this System.Collections.Generic.IEnumerable! source, MoreLinq.OrderByDirection direction, params System.Collections.Generic.IEnumerable![]! otherSequences) -> System.Collections.Generic.IEnumerable! @@ -619,6 +621,7 @@ static MoreLinq.MoreEnumerable.Shuffle(this System.Collections.Generic.IEnume static MoreLinq.MoreEnumerable.Single(this MoreLinq.IExtremaEnumerable! source) -> T static MoreLinq.MoreEnumerable.SingleOrDefault(this MoreLinq.IExtremaEnumerable! source) -> T? static MoreLinq.MoreEnumerable.SkipLast(System.Collections.Generic.IEnumerable! source, int count) -> System.Collections.Generic.IEnumerable! +static MoreLinq.MoreEnumerable.SkipLastWhile(this System.Collections.Generic.IEnumerable! source, System.Func! predicate) -> System.Collections.Generic.IEnumerable! static MoreLinq.MoreEnumerable.SkipUntil(this System.Collections.Generic.IEnumerable! source, System.Func! predicate) -> System.Collections.Generic.IEnumerable! static MoreLinq.MoreEnumerable.Slice(this System.Collections.Generic.IEnumerable! sequence, int startIndex, int count) -> System.Collections.Generic.IEnumerable! static MoreLinq.MoreEnumerable.SortedMerge(this System.Collections.Generic.IEnumerable! source, MoreLinq.OrderByDirection direction, params System.Collections.Generic.IEnumerable![]! otherSequences) -> System.Collections.Generic.IEnumerable! diff --git a/MoreLinq/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/MoreLinq/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 7dc5c5811..6adcc5bb5 100644 --- a/MoreLinq/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/MoreLinq/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +MoreLinq.Extensions.SkipLastWhileExtension +static MoreLinq.Extensions.SkipLastWhileExtension.SkipLastWhile(this System.Collections.Generic.IEnumerable! source, System.Func! predicate) -> System.Collections.Generic.IEnumerable! +static MoreLinq.MoreEnumerable.SkipLastWhile(this System.Collections.Generic.IEnumerable! source, System.Func! predicate) -> System.Collections.Generic.IEnumerable! diff --git a/MoreLinq/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt b/MoreLinq/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt index 7dc5c5811..6adcc5bb5 100644 --- a/MoreLinq/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt +++ b/MoreLinq/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +MoreLinq.Extensions.SkipLastWhileExtension +static MoreLinq.Extensions.SkipLastWhileExtension.SkipLastWhile(this System.Collections.Generic.IEnumerable! source, System.Func! predicate) -> System.Collections.Generic.IEnumerable! +static MoreLinq.MoreEnumerable.SkipLastWhile(this System.Collections.Generic.IEnumerable! source, System.Func! predicate) -> System.Collections.Generic.IEnumerable! diff --git a/MoreLinq/SkipLastWhile.cs b/MoreLinq/SkipLastWhile.cs new file mode 100644 index 000000000..215f04755 --- /dev/null +++ b/MoreLinq/SkipLastWhile.cs @@ -0,0 +1,88 @@ +#region License and Terms +// MoreLINQ - Extensions to LINQ to Objects +// Copyright (c) 2024 Andy Romero (armorynode). All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#endregion + +namespace MoreLinq +{ + using System; + using System.Collections.Generic; + + static partial class MoreEnumerable + { + /// + /// Removes elements from the end of a sequence as long as a specified condition is true. + /// + /// Type of the source sequence. + /// The source sequence. + /// The predicate to use to remove items from the tail of the sequence. + /// + /// An containing the source sequence elements except for the bypassed ones at the end. + /// + /// The source sequence is . + /// The predicate is . + /// + /// This operator uses deferred execution and streams its results. At any given time, it + /// will buffer as many consecutive elements as satisfied by . + /// + + public static IEnumerable SkipLastWhile(this IEnumerable source, Func predicate) + { + if (source == null) throw new ArgumentNullException(nameof(source)); + if (predicate == null) throw new ArgumentNullException(nameof(predicate)); + + return source.TryAsListLike() switch + { + { } list => IterateList(list, predicate), + _ => IterateSequence(source, predicate), + }; + + static IEnumerable IterateList(ListLike list, Func predicate) + { + var i = list.Count - 1; + while (i >= 0 && predicate(list[i])) + { + i--; + } + + for (var j = 0; j <= i; j++) + { + yield return list[j]; + } + } + + static IEnumerable IterateSequence(IEnumerable source, Func predicate) + { + Queue? queue = null; + foreach (var item in source) + { + if (predicate(item)) + { + queue ??= new Queue(); + queue.Enqueue(item); + } + else + { + while (queue?.Count > 0) + { + yield return queue.Dequeue(); + } + yield return item; + } + } + } + } + } +} diff --git a/README.md b/README.md index 2a897f211..f15277b21 100644 --- a/README.md +++ b/README.md @@ -576,6 +576,10 @@ This method has 2 overloads. Bypasses a specified number of elements at the end of the sequence. +### SkipLastWhile + +Removes elements from the end of a sequence as long as a specified condition is true. + ### SkipUntil Skips items from the input sequence until the given predicate returns true diff --git a/bld/Copyright.props b/bld/Copyright.props index 96a381457..d37246fe3 100644 --- a/bld/Copyright.props +++ b/bld/Copyright.props @@ -8,6 +8,7 @@ Portions © 2016 Andreas Gullberg Larsen, Leandro F. Vieira (leandromoh). Portions © 2017 Jonas Nyrup (jnyrup). Portions © 2023 Julien Aspirot (julienasp). + Portions © 2024 Andy Romero (armorynode). Portions © Microsoft. All rights reserved.