Skip to content

Commit

Permalink
Add Take(Range) and TakeLast (#229)
Browse files Browse the repository at this point in the history
  • Loading branch information
SimonCropp authored Sep 27, 2024
1 parent 1360d65 commit b7c1d4e
Show file tree
Hide file tree
Showing 7 changed files with 294 additions and 3 deletions.
2 changes: 1 addition & 1 deletion apiCount.include.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
**API count: 343**
**API count: 345**
2 changes: 2 additions & 0 deletions api_list.include.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@
* `TSource SingleOrDefault<TSource>(Func<TSource,Boolean>, TSource)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.singleordefault#system-linq-enumerable-singleordefault-1(system-collections-generic-ienumerable((-0))-system-func((-0-system-boolean))-0))
* `TSource SingleOrDefault<TSource>(TSource)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.singleordefault#system-linq-enumerable-singleordefault-1(system-collections-generic-ienumerable((-0))-0))
* `IEnumerable<TSource> SkipLast<TSource>(Int32)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.skiplast)
* `IEnumerable<TSource> Take<TSource>(Range)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.take?view=net-8.0#system-linq-enumerable-take-1(system-collections-generic-ienumerable((-0))-system-range))
* `IEnumerable<TSource> TakeLast<TSource>(Int32)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.takelast)
* `HashSet<TSource> ToHashSet<TSource>(IEqualityComparer<TSource>)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.tohashset#system-linq-enumerable-tohashset-1(system-collections-generic-ienumerable((-0))-system-collections-generic-iequalitycomparer((-0))))
* `Boolean TryGetNonEnumeratedCount<TSource>(Int32&)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.trygetnonenumeratedcount)

Expand Down
4 changes: 3 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The package targets `netstandard2.0` and is designed to support the following ru
* `net5.0`, `net6.0`, `net7.0`, `net8.0`, `net9.0`


**API count: 343**<!-- singleLineInclude: apiCount. path: /apiCount.include.md -->
**API count: 345**<!-- singleLineInclude: apiCount. path: /apiCount.include.md -->


**See [Milestones](../../milestones?state=closed) for release notes.**
Expand Down Expand Up @@ -586,6 +586,8 @@ The class `Polyfill` includes the following extension methods:
* `TSource SingleOrDefault<TSource>(Func<TSource,Boolean>, TSource)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.singleordefault#system-linq-enumerable-singleordefault-1(system-collections-generic-ienumerable((-0))-system-func((-0-system-boolean))-0))
* `TSource SingleOrDefault<TSource>(TSource)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.singleordefault#system-linq-enumerable-singleordefault-1(system-collections-generic-ienumerable((-0))-0))
* `IEnumerable<TSource> SkipLast<TSource>(Int32)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.skiplast)
* `IEnumerable<TSource> Take<TSource>(Range)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.take?view=net-8.0#system-linq-enumerable-take-1(system-collections-generic-ienumerable((-0))-system-range))
* `IEnumerable<TSource> TakeLast<TSource>(Int32)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.takelast)
* `HashSet<TSource> ToHashSet<TSource>(IEqualityComparer<TSource>)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.tohashset#system-linq-enumerable-tohashset-1(system-collections-generic-ienumerable((-0))-system-collections-generic-iequalitycomparer((-0))))
* `Boolean TryGetNonEnumeratedCount<TSource>(Int32&)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.trygetnonenumeratedcount)

Expand Down
6 changes: 5 additions & 1 deletion src/Consume/Consume.cs
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ void IDictionary_Methods()

void IEnumerable_Methods()
{
var enumerable = (IEnumerable<string>)new List<string>
IEnumerable<string> enumerable = new List<string>
{
"a",
"b"
Expand All @@ -272,6 +272,10 @@ void IEnumerable_Methods()
var minBy = enumerable.MinBy(_ => _);
var distinctBy = enumerable.DistinctBy(_ => _);
var skipLast = enumerable.SkipLast(1);
#if FeatureValueTuple
var take = enumerable.Take(1..3);
#endif
var takeLast = enumerable.TakeLast(3);
}

void IList_Methods()
Expand Down
212 changes: 212 additions & 0 deletions src/Polyfill/Polyfill_IEnumerable_Take.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
// <auto-generated />
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#pragma warning disable

namespace Polyfills;
using System;
using System.Runtime.CompilerServices;
using System.Collections.Generic;
using Link = System.ComponentModel.DescriptionAttribute;

static partial class Polyfill
{
#if !NET6_0_OR_GREATER

[MethodImpl(MethodImplOptions.AggressiveInlining)]
static bool IsEmptyArray<TSource>(IEnumerable<TSource> source) =>
source is TSource[] { Length: 0 };

static IEnumerable<TSource> TakeRangeFromEndIterator<TSource>(IEnumerable<TSource> source, bool isStartIndexFromEnd, int startIndex, bool isEndIndexFromEnd, int endIndex)
{
// Attempt to extract the count of the source enumerator,
// in order to convert fromEnd indices to regular indices.
// Enumerable counts can change over time, so it is very
// important that this check happens at enumeration time;
// do not move it outside of the iterator method.
if (source.TryGetNonEnumeratedCount(out int count))
{
startIndex = CalculateStartIndex(isStartIndexFromEnd, startIndex, count);
endIndex = CalculateEndIndex(isEndIndexFromEnd, endIndex, count);

if (startIndex < endIndex)
{
foreach (TSource element in TakeRangeIterator(source, startIndex, endIndex))
{
yield return element;
}
}

yield break;
}

Queue<TSource> queue;

if (isStartIndexFromEnd)
{
// TakeLast compat: enumerator should be disposed before yielding the first element.
using (IEnumerator<TSource> e = source.GetEnumerator())
{
if (!e.MoveNext())
{
yield break;
}

queue = new Queue<TSource>();
queue.Enqueue(e.Current);
count = 1;

while (e.MoveNext())
{
if (count < startIndex)
{
queue.Enqueue(e.Current);
++count;
}
else
{
do
{
queue.Dequeue();
queue.Enqueue(e.Current);
checked
{
++count;
}
} while (e.MoveNext());

break;
}
}
}

startIndex = CalculateStartIndex(isStartIndexFromEnd: true, startIndex, count);
endIndex = CalculateEndIndex(isEndIndexFromEnd, endIndex, count);

for (int rangeIndex = startIndex; rangeIndex < endIndex; rangeIndex++)
{
yield return queue.Dequeue();
}
}
else
{
// SkipLast compat: the enumerator should be disposed at the end of the enumeration.
using IEnumerator<TSource> e = source.GetEnumerator();

count = 0;
while (count < startIndex && e.MoveNext())
{
++count;
}

if (count == startIndex)
{
queue = new Queue<TSource>();
while (e.MoveNext())
{
if (queue.Count == endIndex)
{
do
{
queue.Enqueue(e.Current);
yield return queue.Dequeue();
} while (e.MoveNext());

break;
}
else
{
queue.Enqueue(e.Current);
}
}
}
}

static int CalculateStartIndex(bool isStartIndexFromEnd, int startIndex, int count) =>
Math.Max(0, isStartIndexFromEnd ? count - startIndex : startIndex);

static int CalculateEndIndex(bool isEndIndexFromEnd, int endIndex, int count) =>
Math.Min(count, isEndIndexFromEnd ? count - endIndex : endIndex);
}

static IEnumerable<TSource> TakeRangeIterator<TSource>(IEnumerable<TSource> source, int startIndex, int endIndex)
{
using IEnumerator<TSource> e = source.GetEnumerator();

int index = 0;
while (index < startIndex && e.MoveNext())
{
++index;
}

if (index < startIndex)
{
yield break;
}

while (index < endIndex && e.MoveNext())
{
yield return e.Current;
++index;
}
}

#if FeatureValueTuple

//https://github.com/dotnet/runtime/blob/main/src/libraries/System.Linq/src/System/Linq/Take.cs
/// <summary>Returns a specified range of contiguous elements from a sequence.</summary>
/// <typeparam name="TSource">The type of the elements of <paramref name="source" />.</typeparam>
/// <param name="source">The sequence to return elements from.</param>
/// <param name="range">The range of elements to return, which has start and end indexes either from the start or the end.</param>
/// <exception cref="ArgumentNullException"><paramref name="source" /> is <see langword="null" />.</exception>
/// <returns>An <see cref="IEnumerable{T}" /> that contains the specified <paramref name="range" /> of elements from the <paramref name="source" /> sequence.</returns>
/// <remarks>
/// <para>This method is implemented by using deferred execution. The immediate return value is an object that stores all the information that is required to perform the action. The query represented by this method is not executed until the object is enumerated either by calling its `GetEnumerator` method directly or by using `foreach` in Visual C# or `For Each` in Visual Basic.</para>
/// <para><see cref="O:Enumerable.Take" /> enumerates <paramref name="source" /> and yields elements whose indices belong to the specified <paramref name="range"/>.</para>
/// </remarks>
[Link("https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.take?view=net-8.0#system-linq-enumerable-take-1(system-collections-generic-ienumerable((-0))-system-range)")]
public static IEnumerable<TSource> Take<TSource>(
this IEnumerable<TSource> target,
Range range)
{
if (IsEmptyArray(target))
{
return [];
}

Index start = range.Start;
Index end = range.End;
bool isStartIndexFromEnd = start.IsFromEnd;
bool isEndIndexFromEnd = end.IsFromEnd;
int startIndex = start.Value;
int endIndex = end.Value;

if (isStartIndexFromEnd)
{
if (startIndex == 0 || (isEndIndexFromEnd && endIndex >= startIndex))
{
return [];
}
}
else if (!isEndIndexFromEnd)
{
return startIndex >= endIndex ? [] : TakeRangeIterator(target, startIndex, endIndex);
}

return TakeRangeFromEndIterator(target, isStartIndexFromEnd, startIndex, isEndIndexFromEnd, endIndex);
}

static IEnumerable<TSource> TakeIterator<TSource>(IEnumerable<TSource> source, int count)
{
foreach (TSource element in source)
{
yield return element;
if (--count == 0) break;
}
}

#endif
#endif

}
39 changes: 39 additions & 0 deletions src/Polyfill/Polyfill_IEnumerable_TakeLast.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// <auto-generated />
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#pragma warning disable

namespace Polyfills;
using System;
using System.Runtime.CompilerServices;
using System.Collections.Generic;
using Link = System.ComponentModel.DescriptionAttribute;

static partial class Polyfill
{

#if NETSTANDARD2_0 || NETFRAMEWORK

/// <summary>
/// Returns a new enumerable collection that contains the last count elements from source.
/// </summary>
/// <param name="source">An enumerable collection instance.</param>
/// <param name="count">The number of elements to take from the end of the collection.</param>
/// <typeparam name="TSource">The type of the elements in the enumerable collection.</typeparam>
/// <returns>A new enumerable collection that contains the last count elements from source.</returns>
[Link("https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.takelast")]
public static IEnumerable<TSource> TakeLast<TSource>(this IEnumerable<TSource> source, int count)
{
if (count <= 0 || IsEmptyArray(source))
{
return [];
}

return TakeRangeFromEndIterator(
source,
isStartIndexFromEnd: true, startIndex: count,
isEndIndexFromEnd: true, endIndex: 0);
}
#endif
}
32 changes: 32 additions & 0 deletions src/Tests/PolyfillTests_IEnumerable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,38 @@ public int Compare(int x, int y) =>
y.CompareTo(x);
}

[Test]
public void TakeRange()
{
var letters = new List<char>
{
'a',
'b',
'c',
'd',
'e',
};

var taken = letters.Take(1..3).ToList();
Assert.IsTrue(taken.SequenceEqual(['b', 'c']));
}

[Test]
public void TakeLast()
{
var letters = new List<char>
{
'a',
'b',
'c',
'd',
'e',
};

var taken = letters.TakeLast(2).ToList();
Assert.IsTrue(taken.SequenceEqual(['d', 'e']));
}

[Test]
public void Except()
{
Expand Down

0 comments on commit b7c1d4e

Please sign in to comment.