Skip to content

Commit 515bfaf

Browse files
authored
Add System.Linq Chunk extension method (#47965)
* Add System.Linq Chunk extension method Fix #27449 * Use explicit types instead of type inference * Seperate inner and outer loop * Rename parameter maxSize to size * Add missing license header * Remove Chunk.SpeedOpt.cs * Add tests to verify Chunk works after mutations * Add/remove before getting enumerator in tests * Test content of chunk method for IQueryable<T>
1 parent 2b95ec6 commit 515bfaf

File tree

12 files changed

+278
-0
lines changed

12 files changed

+278
-0
lines changed

src/libraries/System.Linq.Queryable/ref/System.Linq.Queryable.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public static partial class Queryable
6565
public static float? Average<TSource>(this System.Linq.IQueryable<TSource> source, System.Linq.Expressions.Expression<System.Func<TSource, float?>> selector) { throw null; }
6666
public static float Average<TSource>(this System.Linq.IQueryable<TSource> source, System.Linq.Expressions.Expression<System.Func<TSource, float>> selector) { throw null; }
6767
public static System.Linq.IQueryable<TResult> Cast<TResult>(this System.Linq.IQueryable source) { throw null; }
68+
public static System.Linq.IQueryable<TSource[]> Chunk<TSource>(this System.Linq.IQueryable<TSource> source, int size) { throw null; }
6869
public static System.Linq.IQueryable<TSource> Concat<TSource>(this System.Linq.IQueryable<TSource> source1, System.Collections.Generic.IEnumerable<TSource> source2) { throw null; }
6970
public static bool Contains<TSource>(this System.Linq.IQueryable<TSource> source, TSource item) { throw null; }
7071
public static bool Contains<TSource>(this System.Linq.IQueryable<TSource> source, TSource item, System.Collections.Generic.IEqualityComparer<TSource>? comparer) { throw null; }

src/libraries/System.Linq.Queryable/src/ILLink/ILLink.Suppressions.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@
109109
<property name="Scope">member</property>
110110
<property name="Target">M:System.Linq.CachedReflectionInfo.Cast_TResult_1(System.Type)</property>
111111
</attribute>
112+
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
113+
<argument>ILLink</argument>
114+
<argument>IL2060</argument>
115+
<property name="Scope">member</property>
116+
<property name="Target">M:System.Linq.CachedReflectionInfo.Chunk_TSource_1(System.Type)</property>
117+
</attribute>
112118
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
113119
<argument>ILLink</argument>
114120
<argument>IL2060</argument>

src/libraries/System.Linq.Queryable/src/System/Linq/CachedReflection.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,13 @@ public static MethodInfo Cast_TResult_1(Type TResult) =>
188188
(s_Cast_TResult_1 = new Func<IQueryable, IQueryable<object>>(Queryable.Cast<object>).GetMethodInfo().GetGenericMethodDefinition()))
189189
.MakeGenericMethod(TResult);
190190

191+
private static MethodInfo? s_Chunk_TSource_1;
192+
193+
public static MethodInfo Chunk_TSource_1(Type TSource) =>
194+
(s_Chunk_TSource_1 ??
195+
(s_Chunk_TSource_1 = new Func<IQueryable<object>, int, IQueryable<object>>(Queryable.Chunk).GetMethodInfo().GetGenericMethodDefinition()))
196+
.MakeGenericMethod(TSource);
197+
191198
private static MethodInfo? s_Concat_TSource_2;
192199

193200
public static MethodInfo Concat_TSource_2(Type TSource) =>

src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,19 @@ public static IQueryable<TSource> Distinct<TSource>(this IQueryable<TSource> sou
614614
));
615615
}
616616

617+
[DynamicDependency("Chunk`1", typeof(Enumerable))]
618+
public static IQueryable<TSource[]> Chunk<TSource>(this IQueryable<TSource> source, int size)
619+
{
620+
if (source == null)
621+
throw Error.ArgumentNull(nameof(source));
622+
return source.Provider.CreateQuery<TSource[]>(
623+
Expression.Call(
624+
null,
625+
CachedReflectionInfo.Chunk_TSource_1(typeof(TSource)),
626+
source.Expression, Expression.Constant(size)
627+
));
628+
}
629+
617630
[DynamicDependency("Concat`1", typeof(Enumerable))]
618631
public static IQueryable<TSource> Concat<TSource>(this IQueryable<TSource> source1, IEnumerable<TSource> source2)
619632
{
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Xunit;
5+
6+
namespace System.Linq.Tests
7+
{
8+
public class ChunkTests : EnumerableBasedTests
9+
{
10+
[Fact]
11+
public void ThrowsOnNullSource()
12+
{
13+
IQueryable<int> source = null;
14+
AssertExtensions.Throws<ArgumentNullException>("source", () => source.Chunk(5));
15+
}
16+
17+
[Fact]
18+
public void Chunk()
19+
{
20+
var chunked = new[] {0, 1, 2}.AsQueryable().Chunk(2);
21+
22+
Assert.Equal(2, chunked.Count());
23+
Assert.Equal(new[] {new[] {0, 1}, new[] {2}}, chunked);
24+
}
25+
}
26+
}

src/libraries/System.Linq.Queryable/tests/System.Linq.Queryable.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<Compile Include="AppendPrependTests.cs" />
1010
<Compile Include="AverageTests.cs" />
1111
<Compile Include="CastTests.cs" />
12+
<Compile Include="ChunkTests.cs" />
1213
<Compile Include="ConcatTests.cs" />
1314
<Compile Include="ContainsTests.cs" />
1415
<Compile Include="CountTests.cs" />

src/libraries/System.Linq/ref/System.Linq.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public static System.Collections.Generic.IEnumerable<
4141
TResult
4242
#nullable restore
4343
> Cast<TResult>(this System.Collections.IEnumerable source) { throw null; }
44+
public static System.Collections.Generic.IEnumerable<TSource[]> Chunk<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, int size) { throw null; }
4445
public static System.Collections.Generic.IEnumerable<TSource> Concat<TSource>(this System.Collections.Generic.IEnumerable<TSource> first, System.Collections.Generic.IEnumerable<TSource> second) { throw null; }
4546
public static bool Contains<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, TSource value) { throw null; }
4647
public static bool Contains<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, TSource value, System.Collections.Generic.IEqualityComparer<TSource>? comparer) { throw null; }

src/libraries/System.Linq/src/System.Linq.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
<Compile Include="System\Linq\Average.cs" />
5151
<Compile Include="System\Linq\Buffer.cs" />
5252
<Compile Include="System\Linq\Cast.cs" />
53+
<Compile Include="System\Linq\Chunk.cs" />
5354
<Compile Include="System\Linq\Concat.cs" />
5455
<Compile Include="System\Linq\Contains.cs" />
5556
<Compile Include="System\Linq\Count.cs" />
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
6+
namespace System.Linq
7+
{
8+
public static partial class Enumerable
9+
{
10+
/// <summary>
11+
/// Split the elements of a sequence into chunks of size at most <paramref name="size"/>.
12+
/// </summary>
13+
/// <remarks>
14+
/// Every chunk except the last will be of size <paramref name="size"/>.
15+
/// The last chunk will contain the remaining elements and may be of a smaller size.
16+
/// </remarks>
17+
/// <param name="source">
18+
/// An <see cref="IEnumerable{T}"/> whose elements to chunk.
19+
/// </param>
20+
/// <param name="size">
21+
/// Maximum size of each chunk.
22+
/// </param>
23+
/// <typeparam name="TSource">
24+
/// The type of the elements of source.
25+
/// </typeparam>
26+
/// <returns>
27+
/// An <see cref="IEnumerable{T}"/> that contains the elements the input sequence split into chunks of size <paramref name="size"/>.
28+
/// </returns>
29+
/// <exception cref="ArgumentNullException">
30+
/// <paramref name="source"/> is null.
31+
/// </exception>
32+
/// <exception cref="ArgumentOutOfRangeException">
33+
/// <paramref name="size"/> is below 1.
34+
/// </exception>
35+
public static IEnumerable<TSource[]> Chunk<TSource>(this IEnumerable<TSource> source, int size)
36+
{
37+
if (source == null)
38+
{
39+
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
40+
}
41+
42+
if (size < 1)
43+
{
44+
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.size);
45+
}
46+
47+
return ChunkIterator(source, size);
48+
}
49+
50+
private static IEnumerable<TSource[]> ChunkIterator<TSource>(IEnumerable<TSource> source, int size)
51+
{
52+
using IEnumerator<TSource> e = source.GetEnumerator();
53+
while (e.MoveNext())
54+
{
55+
TSource[] chunk = new TSource[size];
56+
chunk[0] = e.Current;
57+
58+
for (int i = 1; i < size; i++)
59+
{
60+
if (!e.MoveNext())
61+
{
62+
Array.Resize(ref chunk, i);
63+
yield return chunk;
64+
yield break;
65+
}
66+
67+
chunk[i] = e.Current;
68+
}
69+
70+
yield return chunk;
71+
}
72+
}
73+
}
74+
}

src/libraries/System.Linq/src/System/Linq/ThrowHelper.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ private static string GetArgumentString(ExceptionArgument argument)
5151
case ExceptionArgument.selector: return nameof(ExceptionArgument.selector);
5252
case ExceptionArgument.source: return nameof(ExceptionArgument.source);
5353
case ExceptionArgument.third: return nameof(ExceptionArgument.third);
54+
case ExceptionArgument.size: return nameof(ExceptionArgument.size);
5455
default:
5556
Debug.Fail("The ExceptionArgument value is not defined.");
5657
return string.Empty;
@@ -78,5 +79,6 @@ internal enum ExceptionArgument
7879
selector,
7980
source,
8081
third,
82+
size
8183
}
8284
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
using System.Collections.Generic;
4+
using Xunit;
5+
6+
namespace System.Linq.Tests
7+
{
8+
public class ChunkTests : EnumerableTests
9+
{
10+
[Fact]
11+
public void ThrowsOnNullSource()
12+
{
13+
int[] source = null;
14+
AssertExtensions.Throws<ArgumentNullException>("source", () => source.Chunk(5));
15+
}
16+
17+
[Theory]
18+
[InlineData(0)]
19+
[InlineData(-1)]
20+
public void ThrowsWhenSizeIsNonPositive(int size)
21+
{
22+
int[] source = {1};
23+
AssertExtensions.Throws<ArgumentOutOfRangeException>("size", () => source.Chunk(size));
24+
}
25+
26+
[Fact]
27+
public void ChunkSourceLazily()
28+
{
29+
using IEnumerator<int[]> chunks = new FastInfiniteEnumerator<int>().Chunk(5).GetEnumerator();
30+
chunks.MoveNext();
31+
Assert.Equal(new[] {0, 0, 0, 0, 0}, chunks.Current);
32+
Assert.True(chunks.MoveNext());
33+
}
34+
35+
private static IEnumerable<T> ConvertToType<T>(T[] array, Type type)
36+
{
37+
return type switch
38+
{
39+
{} x when x == typeof(TestReadOnlyCollection<T>) => new TestReadOnlyCollection<T>(array),
40+
{} x when x == typeof(TestCollection<T>) => new TestCollection<T>(array),
41+
{} x when x == typeof(TestEnumerable<T>) => new TestEnumerable<T>(array),
42+
_ => throw new Exception()
43+
};
44+
}
45+
46+
[Theory]
47+
[InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2, -12345}, typeof(TestReadOnlyCollection<int>))]
48+
[InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2, -12345}, typeof(TestCollection<int>))]
49+
[InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2, -12345}, typeof(TestEnumerable<int>))]
50+
public void ChunkSourceRepeatCalls(int[] array, Type type)
51+
{
52+
IEnumerable<int> source = ConvertToType(array, type);
53+
54+
Assert.Equal(source.Chunk(3), source.Chunk(3));
55+
}
56+
57+
[Theory]
58+
[InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2, -12345}, typeof(TestReadOnlyCollection<int>))]
59+
[InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2, -12345}, typeof(TestCollection<int>))]
60+
[InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2, -12345}, typeof(TestEnumerable<int>))]
61+
public void ChunkSourceEvenly(int[] array, Type type)
62+
{
63+
IEnumerable<int> source = ConvertToType(array, type);
64+
65+
using IEnumerator<int[]> chunks = source.Chunk(3).GetEnumerator();
66+
chunks.MoveNext();
67+
Assert.Equal(new[] {9999, 0, 888}, chunks.Current);
68+
chunks.MoveNext();
69+
Assert.Equal(new[] {-1, 66, -777}, chunks.Current);
70+
chunks.MoveNext();
71+
Assert.Equal(new[] {1, 2, -12345}, chunks.Current);
72+
Assert.False(chunks.MoveNext());
73+
}
74+
75+
[Theory]
76+
[InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2}, typeof(TestReadOnlyCollection<int>))]
77+
[InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2}, typeof(TestCollection<int>))]
78+
[InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2}, typeof(TestEnumerable<int>))]
79+
public void ChunkSourceUnevenly(int[] array, Type type)
80+
{
81+
IEnumerable<int> source = ConvertToType(array, type);
82+
83+
using IEnumerator<int[]> chunks = source.Chunk(3).GetEnumerator();
84+
chunks.MoveNext();
85+
Assert.Equal(new[] {9999, 0, 888}, chunks.Current);
86+
chunks.MoveNext();
87+
Assert.Equal(new[] {-1, 66, -777}, chunks.Current);
88+
chunks.MoveNext();
89+
Assert.Equal(new[] {1, 2}, chunks.Current);
90+
Assert.False(chunks.MoveNext());
91+
}
92+
93+
[Theory]
94+
[InlineData(new[] {9999, 0}, typeof(TestReadOnlyCollection<int>))]
95+
[InlineData(new[] {9999, 0}, typeof(TestCollection<int>))]
96+
[InlineData(new[] {9999, 0}, typeof(TestEnumerable<int>))]
97+
public void ChunkSourceSmallerThanMaxSize(int[] array, Type type)
98+
{
99+
IEnumerable<int> source = ConvertToType(array, type);
100+
101+
using IEnumerator<int[]> chunks = source.Chunk(3).GetEnumerator();
102+
chunks.MoveNext();
103+
Assert.Equal(new[] {9999, 0}, chunks.Current);
104+
Assert.False(chunks.MoveNext());
105+
}
106+
107+
[Theory]
108+
[InlineData(new int[] {}, typeof(TestReadOnlyCollection<int>))]
109+
[InlineData(new int[] {}, typeof(TestCollection<int>))]
110+
[InlineData(new int[] {}, typeof(TestEnumerable<int>))]
111+
public void EmptySourceYieldsNoChunks(int[] array, Type type)
112+
{
113+
IEnumerable<int> source = ConvertToType(array, type);
114+
115+
using IEnumerator<int[]> chunks = source.Chunk(3).GetEnumerator();
116+
Assert.False(chunks.MoveNext());
117+
}
118+
119+
[Fact]
120+
public void RemovingFromSourceBeforeIterating()
121+
{
122+
var list = new List<int>
123+
{
124+
9999, 0, 888, -1, 66, -777, 1, 2, -12345
125+
};
126+
IEnumerable<int[]> chunks = list.Chunk(3);
127+
list.Remove(66);
128+
129+
Assert.Equal(new[] {new[] {9999, 0, 888}, new[] {-1, -777, 1}, new[] {2, -12345}}, chunks);
130+
}
131+
132+
[Fact]
133+
public void AddingToSourceBeforeIterating()
134+
{
135+
var list = new List<int>
136+
{
137+
9999, 0, 888, -1, 66, -777, 1, 2, -12345
138+
};
139+
IEnumerable<int[]> chunks = list.Chunk(3);
140+
list.Add(10);
141+
142+
Assert.Equal(new[] {new[] {9999, 0, 888}, new[] {-1, 66, -777}, new[] {1, 2, -12345}, new[] {10}}, chunks);
143+
}
144+
}
145+
}

src/libraries/System.Linq/tests/System.Linq.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<Compile Include="AsEnumerableTests.cs" />
1111
<Compile Include="AverageTests.cs" />
1212
<Compile Include="CastTests.cs" />
13+
<Compile Include="ChunkTests.cs" />
1314
<Compile Include="ConcatTests.cs" />
1415
<Compile Include="ConsistencyTests.cs" />
1516
<Compile Include="ContainsTests.cs" />

0 commit comments

Comments
 (0)