diff --git a/src/libraries/System.Linq.Queryable/ref/System.Linq.Queryable.cs b/src/libraries/System.Linq.Queryable/ref/System.Linq.Queryable.cs index e93d91af3dadcb..55290e7106aa9c 100644 --- a/src/libraries/System.Linq.Queryable/ref/System.Linq.Queryable.cs +++ b/src/libraries/System.Linq.Queryable/ref/System.Linq.Queryable.cs @@ -65,6 +65,7 @@ public static partial class Queryable public static float? Average(this System.Linq.IQueryable source, System.Linq.Expressions.Expression> selector) { throw null; } public static float Average(this System.Linq.IQueryable source, System.Linq.Expressions.Expression> selector) { throw null; } public static System.Linq.IQueryable Cast(this System.Linq.IQueryable source) { throw null; } + public static System.Linq.IQueryable Chunk(this System.Linq.IQueryable source, int size) { throw null; } public static System.Linq.IQueryable Concat(this System.Linq.IQueryable source1, System.Collections.Generic.IEnumerable source2) { throw null; } public static bool Contains(this System.Linq.IQueryable source, TSource item) { throw null; } public static bool Contains(this System.Linq.IQueryable source, TSource item, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } diff --git a/src/libraries/System.Linq.Queryable/src/ILLink/ILLink.Suppressions.xml b/src/libraries/System.Linq.Queryable/src/ILLink/ILLink.Suppressions.xml index 81bfe8dd961229..2393a6a50d10a1 100644 --- a/src/libraries/System.Linq.Queryable/src/ILLink/ILLink.Suppressions.xml +++ b/src/libraries/System.Linq.Queryable/src/ILLink/ILLink.Suppressions.xml @@ -109,6 +109,12 @@ member M:System.Linq.CachedReflectionInfo.Cast_TResult_1(System.Type) + + ILLink + IL2060 + member + M:System.Linq.CachedReflectionInfo.Chunk_TSource_1(System.Type) + ILLink IL2060 diff --git a/src/libraries/System.Linq.Queryable/src/System/Linq/CachedReflection.cs b/src/libraries/System.Linq.Queryable/src/System/Linq/CachedReflection.cs index 943a72b0b6d488..11b615fd44b216 100644 --- a/src/libraries/System.Linq.Queryable/src/System/Linq/CachedReflection.cs +++ b/src/libraries/System.Linq.Queryable/src/System/Linq/CachedReflection.cs @@ -188,6 +188,13 @@ public static MethodInfo Cast_TResult_1(Type TResult) => (s_Cast_TResult_1 = new Func>(Queryable.Cast).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, int, IQueryable>(Queryable.Chunk).GetMethodInfo().GetGenericMethodDefinition())) + .MakeGenericMethod(TSource); + private static MethodInfo? s_Concat_TSource_2; public static MethodInfo Concat_TSource_2(Type TSource) => diff --git a/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs b/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs index 7827249013cd0e..16befe027e4d63 100644 --- a/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs +++ b/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs @@ -614,6 +614,19 @@ public static IQueryable Distinct(this IQueryable sou )); } + [DynamicDependency("Chunk`1", typeof(Enumerable))] + public static IQueryable Chunk(this IQueryable source, int size) + { + if (source == null) + throw Error.ArgumentNull(nameof(source)); + return source.Provider.CreateQuery( + Expression.Call( + null, + CachedReflectionInfo.Chunk_TSource_1(typeof(TSource)), + source.Expression, Expression.Constant(size) + )); + } + [DynamicDependency("Concat`1", typeof(Enumerable))] public static IQueryable Concat(this IQueryable source1, IEnumerable source2) { diff --git a/src/libraries/System.Linq.Queryable/tests/ChunkTests.cs b/src/libraries/System.Linq.Queryable/tests/ChunkTests.cs new file mode 100644 index 00000000000000..c2ceec7397d1f4 --- /dev/null +++ b/src/libraries/System.Linq.Queryable/tests/ChunkTests.cs @@ -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 source = null; + AssertExtensions.Throws("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); + } + } +} \ No newline at end of file diff --git a/src/libraries/System.Linq.Queryable/tests/System.Linq.Queryable.Tests.csproj b/src/libraries/System.Linq.Queryable/tests/System.Linq.Queryable.Tests.csproj index 55af483dac0596..bceba421dbd455 100644 --- a/src/libraries/System.Linq.Queryable/tests/System.Linq.Queryable.Tests.csproj +++ b/src/libraries/System.Linq.Queryable/tests/System.Linq.Queryable.Tests.csproj @@ -9,6 +9,7 @@ + diff --git a/src/libraries/System.Linq/ref/System.Linq.cs b/src/libraries/System.Linq/ref/System.Linq.cs index 306886754a23d8..4c8007cda53bc6 100644 --- a/src/libraries/System.Linq/ref/System.Linq.cs +++ b/src/libraries/System.Linq/ref/System.Linq.cs @@ -41,6 +41,7 @@ public static System.Collections.Generic.IEnumerable< TResult #nullable restore > Cast(this System.Collections.IEnumerable source) { throw null; } + public static System.Collections.Generic.IEnumerable Chunk(this System.Collections.Generic.IEnumerable source, int size) { throw null; } public static System.Collections.Generic.IEnumerable Concat(this System.Collections.Generic.IEnumerable first, System.Collections.Generic.IEnumerable second) { throw null; } public static bool Contains(this System.Collections.Generic.IEnumerable source, TSource value) { throw null; } public static bool Contains(this System.Collections.Generic.IEnumerable source, TSource value, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } diff --git a/src/libraries/System.Linq/src/System.Linq.csproj b/src/libraries/System.Linq/src/System.Linq.csproj index ea585685f3e609..14573dec7f7aed 100644 --- a/src/libraries/System.Linq/src/System.Linq.csproj +++ b/src/libraries/System.Linq/src/System.Linq.csproj @@ -42,6 +42,7 @@ + diff --git a/src/libraries/System.Linq/src/System/Linq/Chunk.cs b/src/libraries/System.Linq/src/System/Linq/Chunk.cs new file mode 100644 index 00000000000000..661edaab5694a9 --- /dev/null +++ b/src/libraries/System.Linq/src/System/Linq/Chunk.cs @@ -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 + { + /// + /// Split the elements of a sequence into chunks of size at most . + /// + /// + /// Every chunk except the last will be of size . + /// The last chunk will contain the remaining elements and may be of a smaller size. + /// + /// + /// An whose elements to chunk. + /// + /// + /// Maximum size of each chunk. + /// + /// + /// The type of the elements of source. + /// + /// + /// An that contains the elements the input sequence split into chunks of size . + /// + /// + /// is null. + /// + /// + /// is below 1. + /// + public static IEnumerable Chunk(this IEnumerable source, int size) + { + if (source == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); + } + + if (size < 1) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.size); + } + + return ChunkIterator(source, size); + } + + private static IEnumerable ChunkIterator(IEnumerable source, int size) + { + using IEnumerator 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; + } + } + } +} \ No newline at end of file diff --git a/src/libraries/System.Linq/src/System/Linq/ThrowHelper.cs b/src/libraries/System.Linq/src/System/Linq/ThrowHelper.cs index de3c1300f1a94a..0b03039c8c8aea 100644 --- a/src/libraries/System.Linq/src/System/Linq/ThrowHelper.cs +++ b/src/libraries/System.Linq/src/System/Linq/ThrowHelper.cs @@ -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; @@ -78,5 +79,6 @@ internal enum ExceptionArgument selector, source, third, + size } } diff --git a/src/libraries/System.Linq/tests/ChunkTests.cs b/src/libraries/System.Linq/tests/ChunkTests.cs new file mode 100644 index 00000000000000..89cdcecc81342d --- /dev/null +++ b/src/libraries/System.Linq/tests/ChunkTests.cs @@ -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("source", () => source.Chunk(5)); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + public void ThrowsWhenSizeIsNonPositive(int size) + { + int[] source = {1}; + AssertExtensions.Throws("size", () => source.Chunk(size)); + } + + [Fact] + public void ChunkSourceLazily() + { + using IEnumerator chunks = new FastInfiniteEnumerator().Chunk(5).GetEnumerator(); + chunks.MoveNext(); + Assert.Equal(new[] {0, 0, 0, 0, 0}, chunks.Current); + Assert.True(chunks.MoveNext()); + } + + private static IEnumerable ConvertToType(T[] array, Type type) + { + return type switch + { + {} x when x == typeof(TestReadOnlyCollection) => new TestReadOnlyCollection(array), + {} x when x == typeof(TestCollection) => new TestCollection(array), + {} x when x == typeof(TestEnumerable) => new TestEnumerable(array), + _ => throw new Exception() + }; + } + + [Theory] + [InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2, -12345}, typeof(TestReadOnlyCollection))] + [InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2, -12345}, typeof(TestCollection))] + [InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2, -12345}, typeof(TestEnumerable))] + public void ChunkSourceRepeatCalls(int[] array, Type type) + { + IEnumerable 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))] + [InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2, -12345}, typeof(TestCollection))] + [InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2, -12345}, typeof(TestEnumerable))] + public void ChunkSourceEvenly(int[] array, Type type) + { + IEnumerable source = ConvertToType(array, type); + + using IEnumerator 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))] + [InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2}, typeof(TestCollection))] + [InlineData(new[] {9999, 0, 888, -1, 66, -777, 1, 2}, typeof(TestEnumerable))] + public void ChunkSourceUnevenly(int[] array, Type type) + { + IEnumerable source = ConvertToType(array, type); + + using IEnumerator 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))] + [InlineData(new[] {9999, 0}, typeof(TestCollection))] + [InlineData(new[] {9999, 0}, typeof(TestEnumerable))] + public void ChunkSourceSmallerThanMaxSize(int[] array, Type type) + { + IEnumerable source = ConvertToType(array, type); + + using IEnumerator chunks = source.Chunk(3).GetEnumerator(); + chunks.MoveNext(); + Assert.Equal(new[] {9999, 0}, chunks.Current); + Assert.False(chunks.MoveNext()); + } + + [Theory] + [InlineData(new int[] {}, typeof(TestReadOnlyCollection))] + [InlineData(new int[] {}, typeof(TestCollection))] + [InlineData(new int[] {}, typeof(TestEnumerable))] + public void EmptySourceYieldsNoChunks(int[] array, Type type) + { + IEnumerable source = ConvertToType(array, type); + + using IEnumerator chunks = source.Chunk(3).GetEnumerator(); + Assert.False(chunks.MoveNext()); + } + + [Fact] + public void RemovingFromSourceBeforeIterating() + { + var list = new List + { + 9999, 0, 888, -1, 66, -777, 1, 2, -12345 + }; + IEnumerable 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 + { + 9999, 0, 888, -1, 66, -777, 1, 2, -12345 + }; + IEnumerable 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); + } + } +} \ No newline at end of file diff --git a/src/libraries/System.Linq/tests/System.Linq.Tests.csproj b/src/libraries/System.Linq/tests/System.Linq.Tests.csproj index 595a687a07f5d6..4e4a90f7f2f0d8 100644 --- a/src/libraries/System.Linq/tests/System.Linq.Tests.csproj +++ b/src/libraries/System.Linq/tests/System.Linq.Tests.csproj @@ -10,6 +10,7 @@ +