Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add System.Linq Chunk extension method #47965

Merged
merged 9 commits into from
Feb 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public static partial class Queryable
public static float? Average<TSource>(this System.Linq.IQueryable<TSource> source, System.Linq.Expressions.Expression<System.Func<TSource, float?>> selector) { throw null; }
public static float Average<TSource>(this System.Linq.IQueryable<TSource> source, System.Linq.Expressions.Expression<System.Func<TSource, float>> selector) { throw null; }
public static System.Linq.IQueryable<TResult> Cast<TResult>(this System.Linq.IQueryable source) { throw null; }
public static System.Linq.IQueryable<TSource[]> Chunk<TSource>(this System.Linq.IQueryable<TSource> source, int size) { throw null; }
public static System.Linq.IQueryable<TSource> Concat<TSource>(this System.Linq.IQueryable<TSource> source1, System.Collections.Generic.IEnumerable<TSource> source2) { throw null; }
public static bool Contains<TSource>(this System.Linq.IQueryable<TSource> source, TSource item) { throw null; }
public static bool Contains<TSource>(this System.Linq.IQueryable<TSource> source, TSource item, System.Collections.Generic.IEqualityComparer<TSource>? comparer) { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@
<property name="Scope">member</property>
<property name="Target">M:System.Linq.CachedReflectionInfo.Cast_TResult_1(System.Type)</property>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2060</argument>
<property name="Scope">member</property>
<property name="Target">M:System.Linq.CachedReflectionInfo.Chunk_TSource_1(System.Type)</property>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2060</argument>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,13 @@ internal static class CachedReflectionInfo
(s_Cast_TResult_1 = new Func<IQueryable, IQueryable<object>>(Queryable.Cast<object>).GetMethodInfo().GetGenericMethodDefinition()))
.MakeGenericMethod(TResult);

private static MethodInfo? s_Chunk_TSource_1;

public static MethodInfo Chunk_TSource_1(Type TSource) =>
(s_Chunk_TSource_1 ??
(s_Chunk_TSource_1 = new Func<IQueryable<object>, int, IQueryable<object>>(Queryable.Chunk).GetMethodInfo().GetGenericMethodDefinition()))
.MakeGenericMethod(TSource);

private static MethodInfo? s_Concat_TSource_2;

public static MethodInfo Concat_TSource_2(Type TSource) =>
Expand Down
13 changes: 13 additions & 0 deletions src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,19 @@ public static IQueryable<TSource> Distinct<TSource>(this IQueryable<TSource> sou
));
}

[DynamicDependency("Chunk`1", typeof(Enumerable))]
public static IQueryable<TSource[]> Chunk<TSource>(this IQueryable<TSource> source, int size)
{
if (source == null)
throw Error.ArgumentNull(nameof(source));
return source.Provider.CreateQuery<TSource[]>(
Expression.Call(
null,
CachedReflectionInfo.Chunk_TSource_1(typeof(TSource)),
source.Expression, Expression.Constant(size)
));
}

[DynamicDependency("Concat`1", typeof(Enumerable))]
public static IQueryable<TSource> Concat<TSource>(this IQueryable<TSource> source1, IEnumerable<TSource> source2)
{
Expand Down
26 changes: 26 additions & 0 deletions src/libraries/System.Linq.Queryable/tests/ChunkTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Xunit;

namespace System.Linq.Tests
{
public class ChunkTests : EnumerableBasedTests
{
[Fact]
public void ThrowsOnNullSource()
{
IQueryable<int> source = null;
AssertExtensions.Throws<ArgumentNullException>("source", () => source.Chunk(5));
}

[Fact]
public void Chunk()
{
var chunked = new[] {0, 1, 2}.AsQueryable().Chunk(2);

Assert.Equal(2, chunked.Count());
Assert.Equal(new[] {new[] {0, 1}, new[] {2}}, chunked);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<Compile Include="AppendPrependTests.cs" />
<Compile Include="AverageTests.cs" />
<Compile Include="CastTests.cs" />
<Compile Include="ChunkTests.cs" />
<Compile Include="ConcatTests.cs" />
<Compile Include="ContainsTests.cs" />
<Compile Include="CountTests.cs" />
Expand Down
1 change: 1 addition & 0 deletions src/libraries/System.Linq/ref/System.Linq.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public static partial class Enumerable
TResult
#nullable restore
> Cast<TResult>(this System.Collections.IEnumerable source) { throw null; }
public static System.Collections.Generic.IEnumerable<TSource[]> Chunk<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, int size) { throw null; }
public static System.Collections.Generic.IEnumerable<TSource> Concat<TSource>(this System.Collections.Generic.IEnumerable<TSource> first, System.Collections.Generic.IEnumerable<TSource> second) { throw null; }
public static bool Contains<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, TSource value) { throw null; }
public static bool Contains<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, TSource value, System.Collections.Generic.IEqualityComparer<TSource>? comparer) { throw null; }
Expand Down
1 change: 1 addition & 0 deletions src/libraries/System.Linq/src/System.Linq.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<Compile Include="System\Linq\Average.cs" />
<Compile Include="System\Linq\Buffer.cs" />
<Compile Include="System\Linq\Cast.cs" />
<Compile Include="System\Linq\Chunk.cs" />
<Compile Include="System\Linq\Concat.cs" />
<Compile Include="System\Linq\Contains.cs" />
<Compile Include="System\Linq\Count.cs" />
Expand Down
74 changes: 74 additions & 0 deletions src/libraries/System.Linq/src/System/Linq/Chunk.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;

namespace System.Linq
{
public static partial class Enumerable
{
/// <summary>
/// Split the elements of a sequence into chunks of size at most <paramref name="size"/>.
/// </summary>
/// <remarks>
/// Every chunk except the last will be of size <paramref name="size"/>.
/// The last chunk will contain the remaining elements and may be of a smaller size.
/// </remarks>
/// <param name="source">
/// An <see cref="IEnumerable{T}"/> whose elements to chunk.
/// </param>
/// <param name="size">
/// Maximum size of each chunk.
/// </param>
/// <typeparam name="TSource">
/// The type of the elements of source.
/// </typeparam>
/// <returns>
/// An <see cref="IEnumerable{T}"/> that contains the elements the input sequence split into chunks of size <paramref name="size"/>.
/// </returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="source"/> is null.
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="size"/> is below 1.
/// </exception>
public static IEnumerable<TSource[]> Chunk<TSource>(this IEnumerable<TSource> source, int size)
{
if (source == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}

if (size < 1)
{
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.size);
}

return ChunkIterator(source, size);
}

private static IEnumerable<TSource[]> ChunkIterator<TSource>(IEnumerable<TSource> source, int size)
{
using IEnumerator<TSource> e = source.GetEnumerator();
while (e.MoveNext())
{
TSource[] chunk = new TSource[size];
chunk[0] = e.Current;

for (int i = 1; i < size; i++)
{
if (!e.MoveNext())
{
Array.Resize(ref chunk, i);
yield return chunk;
yield break;
}

chunk[i] = e.Current;
}

yield return chunk;
}
}
}
}
2 changes: 2 additions & 0 deletions src/libraries/System.Linq/src/System/Linq/ThrowHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ private static string GetArgumentString(ExceptionArgument argument)
case ExceptionArgument.selector: return nameof(ExceptionArgument.selector);
case ExceptionArgument.source: return nameof(ExceptionArgument.source);
case ExceptionArgument.third: return nameof(ExceptionArgument.third);
case ExceptionArgument.size: return nameof(ExceptionArgument.size);
default:
Debug.Fail("The ExceptionArgument value is not defined.");
return string.Empty;
Expand Down Expand Up @@ -78,5 +79,6 @@ internal enum ExceptionArgument
selector,
source,
third,
size
}
}
145 changes: 145 additions & 0 deletions src/libraries/System.Linq/tests/ChunkTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using Xunit;

namespace System.Linq.Tests
{
public class ChunkTests : EnumerableTests
{
[Fact]
public void ThrowsOnNullSource()
{
int[] source = null;
AssertExtensions.Throws<ArgumentNullException>("source", () => source.Chunk(5));
}

[Theory]
[InlineData(0)]
[InlineData(-1)]
public void ThrowsWhenSizeIsNonPositive(int size)
{
int[] source = {1};
AssertExtensions.Throws<ArgumentOutOfRangeException>("size", () => source.Chunk(size));
}

[Fact]
public void ChunkSourceLazily()
{
using IEnumerator<int[]> chunks = new FastInfiniteEnumerator<int>().Chunk(5).GetEnumerator();
chunks.MoveNext();
Assert.Equal(new[] {0, 0, 0, 0, 0}, chunks.Current);
Assert.True(chunks.MoveNext());
}

private static IEnumerable<T> ConvertToType<T>(T[] array, Type type)
{
return type switch
{
{} x when x == typeof(TestReadOnlyCollection<T>) => new TestReadOnlyCollection<T>(array),
{} x when x == typeof(TestCollection<T>) => new TestCollection<T>(array),
{} x when x == typeof(TestEnumerable<T>) => new TestEnumerable<T>(array),
_ => throw new Exception()
};
}

[Theory]
[InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2, -12345}, typeof(TestReadOnlyCollection<int>))]
[InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2, -12345}, typeof(TestCollection<int>))]
[InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2, -12345}, typeof(TestEnumerable<int>))]
public void ChunkSourceRepeatCalls(int[] array, Type type)
{
IEnumerable<int> source = ConvertToType(array, type);

Assert.Equal(source.Chunk(3), source.Chunk(3));
}

[Theory]
[InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2, -12345}, typeof(TestReadOnlyCollection<int>))]
[InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2, -12345}, typeof(TestCollection<int>))]
[InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2, -12345}, typeof(TestEnumerable<int>))]
public void ChunkSourceEvenly(int[] array, Type type)
{
IEnumerable<int> source = ConvertToType(array, type);

using IEnumerator<int[]> chunks = source.Chunk(3).GetEnumerator();
chunks.MoveNext();
Assert.Equal(new[] {9999, 0, 888}, chunks.Current);
chunks.MoveNext();
Assert.Equal(new[] {-1, 66, -777}, chunks.Current);
chunks.MoveNext();
Assert.Equal(new[] {1, 2, -12345}, chunks.Current);
Assert.False(chunks.MoveNext());
}

[Theory]
[InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2}, typeof(TestReadOnlyCollection<int>))]
[InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2}, typeof(TestCollection<int>))]
[InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2}, typeof(TestEnumerable<int>))]
public void ChunkSourceUnevenly(int[] array, Type type)
{
IEnumerable<int> source = ConvertToType(array, type);

using IEnumerator<int[]> chunks = source.Chunk(3).GetEnumerator();
chunks.MoveNext();
Assert.Equal(new[] {9999, 0, 888}, chunks.Current);
chunks.MoveNext();
Assert.Equal(new[] {-1, 66, -777}, chunks.Current);
chunks.MoveNext();
Assert.Equal(new[] {1, 2}, chunks.Current);
Assert.False(chunks.MoveNext());
}

[Theory]
[InlineData(new[] {9999, 0}, typeof(TestReadOnlyCollection<int>))]
[InlineData(new[] {9999, 0}, typeof(TestCollection<int>))]
[InlineData(new[] {9999, 0}, typeof(TestEnumerable<int>))]
public void ChunkSourceSmallerThanMaxSize(int[] array, Type type)
{
IEnumerable<int> source = ConvertToType(array, type);

using IEnumerator<int[]> chunks = source.Chunk(3).GetEnumerator();
chunks.MoveNext();
Assert.Equal(new[] {9999, 0}, chunks.Current);
Assert.False(chunks.MoveNext());
}

[Theory]
[InlineData(new int[] {}, typeof(TestReadOnlyCollection<int>))]
[InlineData(new int[] {}, typeof(TestCollection<int>))]
[InlineData(new int[] {}, typeof(TestEnumerable<int>))]
public void EmptySourceYieldsNoChunks(int[] array, Type type)
{
IEnumerable<int> source = ConvertToType(array, type);

using IEnumerator<int[]> chunks = source.Chunk(3).GetEnumerator();
Assert.False(chunks.MoveNext());
}

[Fact]
public void RemovingFromSourceBeforeIterating()
{
var list = new List<int>
{
9999, 0, 888, -1, 66, -777, 1, 2, -12345
};
IEnumerable<int[]> chunks = list.Chunk(3);
list.Remove(66);

Assert.Equal(new[] {new[] {9999, 0, 888}, new[] {-1, -777, 1}, new[] {2, -12345}}, chunks);
}

[Fact]
public void AddingToSourceBeforeIterating()
{
var list = new List<int>
{
9999, 0, 888, -1, 66, -777, 1, 2, -12345
};
IEnumerable<int[]> chunks = list.Chunk(3);
list.Add(10);

Assert.Equal(new[] {new[] {9999, 0, 888}, new[] {-1, 66, -777}, new[] {1, 2, -12345}, new[] {10}}, chunks);
}
}
}
1 change: 1 addition & 0 deletions src/libraries/System.Linq/tests/System.Linq.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<Compile Include="AsEnumerableTests.cs" />
<Compile Include="AverageTests.cs" />
<Compile Include="CastTests.cs" />
<Compile Include="ChunkTests.cs" />
<Compile Include="ConcatTests.cs" />
<Compile Include="ConsistencyTests.cs" />
<Compile Include="ContainsTests.cs" />
Expand Down