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 c3253aada63af..e93d91af3dadc 100644 --- a/src/libraries/System.Linq.Queryable/ref/System.Linq.Queryable.cs +++ b/src/libraries/System.Linq.Queryable/ref/System.Linq.Queryable.cs @@ -162,6 +162,7 @@ public static partial class Queryable public static System.Linq.IQueryable Where(this System.Linq.IQueryable source, System.Linq.Expressions.Expression> predicate) { throw null; } public static System.Linq.IQueryable Where(this System.Linq.IQueryable source, System.Linq.Expressions.Expression> predicate) { throw null; } public static System.Linq.IQueryable<(TFirst First, TSecond Second)> Zip(this System.Linq.IQueryable source1, System.Collections.Generic.IEnumerable source2) { throw null; } + public static System.Linq.IQueryable<(TFirst First, TSecond Second, TThird Third)> Zip(this System.Linq.IQueryable source1, System.Collections.Generic.IEnumerable source2, System.Collections.Generic.IEnumerable source3) { throw null; } public static System.Linq.IQueryable Zip(this System.Linq.IQueryable source1, System.Collections.Generic.IEnumerable source2, System.Linq.Expressions.Expression> resultSelector) { 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 49a5c2e445c19..81bfe8dd96122 100644 --- a/src/libraries/System.Linq.Queryable/src/ILLink/ILLink.Suppressions.xml +++ b/src/libraries/System.Linq.Queryable/src/ILLink/ILLink.Suppressions.xml @@ -637,6 +637,12 @@ member M:System.Linq.CachedReflectionInfo.Zip_TFirst_TSecond_TResult_3(System.Type,System.Type,System.Type) + + ILLink + IL2060 + member + M:System.Linq.CachedReflectionInfo.Zip_TFirst_TSecond_TThird_3(System.Type,System.Type,System.Type) + ILLink IL2060 @@ -662,4 +668,4 @@ M:System.Linq.TypeHelper.GetStaticMethods(System.Type) - \ No newline at end of file + 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 8f345d0a86bb4..943a72b0b6d48 100644 --- a/src/libraries/System.Linq.Queryable/src/System/Linq/CachedReflection.cs +++ b/src/libraries/System.Linq.Queryable/src/System/Linq/CachedReflection.cs @@ -843,6 +843,13 @@ public static MethodInfo Zip_TFirst_TSecond_TResult_3(Type TFirst, Type TSecond, (s_Zip_TFirst_TSecond_TResult_3 = new Func, IEnumerable, Expression>, IQueryable>(Queryable.Zip).GetMethodInfo().GetGenericMethodDefinition())) .MakeGenericMethod(TFirst, TSecond, TResult); + private static MethodInfo? s_Zip_TFirst_TSecond_TThird_3; + + public static MethodInfo Zip_TFirst_TSecond_TThird_3(Type TFirst, Type TSecond, Type TThird) => + (s_Zip_TFirst_TSecond_TThird_3 ?? + (s_Zip_TFirst_TSecond_TThird_3 = new Func, IEnumerable, IEnumerable, IQueryable<(object, object, object)>>(Queryable.Zip).GetMethodInfo().GetGenericMethodDefinition())) + .MakeGenericMethod(TFirst, TSecond, TThird); + private static MethodInfo? s_SkipLast_TSource_2; 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 dab995cba60f0..7827249013cd0 100644 --- a/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs +++ b/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs @@ -666,6 +666,33 @@ public static IQueryable Zip(this IQueryable< )); } + /// + /// Produces a sequence of tuples with elements from the three specified sequences. + /// + /// The type of the elements of the first input sequence. + /// The type of the elements of the second input sequence. + /// The type of the elements of the third input sequence. + /// The first sequence to merge. + /// The second sequence to merge. + /// The third sequence to merge. + /// A sequence of tuples with elements taken from the first, second and third sequences, in that order. + [DynamicDependency("Zip`3", typeof(Enumerable))] + public static IQueryable<(TFirst First, TSecond Second, TThird Third)> Zip(this IQueryable source1, IEnumerable source2, IEnumerable source3) + { + if (source1 == null) + throw Error.ArgumentNull(nameof(source1)); + if (source2 == null) + throw Error.ArgumentNull(nameof(source2)); + if (source3 == null) + throw Error.ArgumentNull(nameof(source3)); + return source1.Provider.CreateQuery<(TFirst, TSecond, TThird)>( + Expression.Call( + null, + CachedReflectionInfo.Zip_TFirst_TSecond_TThird_3(typeof(TFirst), typeof(TSecond), typeof(TThird)), + source1.Expression, GetSourceExpression(source2), GetSourceExpression(source3) + )); + } + [DynamicDependency("Union`1", typeof(Enumerable))] public static IQueryable Union(this IQueryable source1, IEnumerable source2) { diff --git a/src/libraries/System.Linq.Queryable/tests/ZipTests.cs b/src/libraries/System.Linq.Queryable/tests/ZipTests.cs index 186ef07c41908..26498693a2988 100644 --- a/src/libraries/System.Linq.Queryable/tests/ZipTests.cs +++ b/src/libraries/System.Linq.Queryable/tests/ZipTests.cs @@ -90,5 +90,55 @@ public void TupleNames() Assert.Equal(tuple.Item1, tuple.First); Assert.Equal(tuple.Item2, tuple.Second); } + + [Fact] + public void Zip3_CorrectResults() + { + int[] first = new int[] { 1, 3, 5 }; + int[] second = new int[] { 2, 6, 8 }; + int[] third = new int[] { 1, 7, 2 }; + var expected = new (int, int, int)[] { (1, 2, 1), (3, 6, 7), (5, 8, 2) }; + Assert.Equal(expected, first.AsQueryable().Zip(second.AsQueryable(), third.AsQueryable())); + } + + + [Fact] + public void Zip3_FirstIsNull() + { + IQueryable first = null; + int[] second = new int[] { 2, 6, 8 }; + int[] third = new int[] { 1, 7, 2 }; + AssertExtensions.Throws("source1", () => first.Zip(second.AsQueryable(), third.AsQueryable())); + } + + [Fact] + public void Zip3_SecondIsNull() + { + int[] first = new int[] { 1, 3, 5 }; + IQueryable second = null; + int[] third = new int[] { 1, 7, 2 }; + AssertExtensions.Throws("source2", () => first.AsQueryable().Zip(second, third.AsQueryable())); + } + + [Fact] + public void Zip3_ThirdIsNull() + { + int[] first = new int[] { 1, 3, 5 }; + int[] second = new int[] { 2, 6, 8 }; + IQueryable third = null; + AssertExtensions.Throws("source3", () => first.AsQueryable().Zip(second.AsQueryable(), third)); + } + + [Fact] + public void Zip3_TupleNames() + { + int[] first = new int[] { 1 }; + int[] second = new int[] { 2 }; + int[] third = new int[] { 3 }; + var tuple = first.AsQueryable().Zip(second.AsQueryable(), third.AsQueryable()).First(); + Assert.Equal(tuple.Item1, tuple.First); + Assert.Equal(tuple.Item2, tuple.Second); + Assert.Equal(tuple.Item3, tuple.Third); + } } } diff --git a/src/libraries/System.Linq/ref/System.Linq.cs b/src/libraries/System.Linq/ref/System.Linq.cs index 3c2a7ec8b24c6..306886754a23d 100644 --- a/src/libraries/System.Linq/ref/System.Linq.cs +++ b/src/libraries/System.Linq/ref/System.Linq.cs @@ -193,6 +193,7 @@ public static System.Collections.Generic.IEnumerable< public static System.Collections.Generic.IEnumerable Where(this System.Collections.Generic.IEnumerable source, System.Func predicate) { throw null; } public static System.Collections.Generic.IEnumerable Where(this System.Collections.Generic.IEnumerable source, System.Func predicate) { throw null; } public static System.Collections.Generic.IEnumerable<(TFirst First, TSecond Second)> Zip(this System.Collections.Generic.IEnumerable first, System.Collections.Generic.IEnumerable second) { throw null; } + public static System.Collections.Generic.IEnumerable<(TFirst First, TSecond Second, TThird Third)> Zip(this System.Collections.Generic.IEnumerable first, System.Collections.Generic.IEnumerable second, System.Collections.Generic.IEnumerable third) { throw null; } public static System.Collections.Generic.IEnumerable Zip(this System.Collections.Generic.IEnumerable first, System.Collections.Generic.IEnumerable second, System.Func resultSelector) { throw null; } } public partial interface IGrouping : System.Collections.Generic.IEnumerable, System.Collections.IEnumerable diff --git a/src/libraries/System.Linq/src/System/Linq/ThrowHelper.cs b/src/libraries/System.Linq/src/System/Linq/ThrowHelper.cs index 48a1052a277eb..de3c1300f1a94 100644 --- a/src/libraries/System.Linq/src/System/Linq/ThrowHelper.cs +++ b/src/libraries/System.Linq/src/System/Linq/ThrowHelper.cs @@ -50,6 +50,7 @@ private static string GetArgumentString(ExceptionArgument argument) case ExceptionArgument.second: return nameof(ExceptionArgument.second); case ExceptionArgument.selector: return nameof(ExceptionArgument.selector); case ExceptionArgument.source: return nameof(ExceptionArgument.source); + case ExceptionArgument.third: return nameof(ExceptionArgument.third); default: Debug.Fail("The ExceptionArgument value is not defined."); return string.Empty; @@ -76,5 +77,6 @@ internal enum ExceptionArgument second, selector, source, + third, } } diff --git a/src/libraries/System.Linq/src/System/Linq/Zip.cs b/src/libraries/System.Linq/src/System/Linq/Zip.cs index 804f98b5db11e..d0a380086d228 100644 --- a/src/libraries/System.Linq/src/System/Linq/Zip.cs +++ b/src/libraries/System.Linq/src/System/Linq/Zip.cs @@ -42,6 +42,36 @@ public static IEnumerable Zip(this IEnumerabl return ZipIterator(first, second); } + /// + /// Produces a sequence of tuples with elements from the three specified sequences. + /// + /// The type of the elements of the first input sequence. + /// The type of the elements of the second input sequence. + /// The type of the elements of the third input sequence. + /// The first sequence to merge. + /// The second sequence to merge. + /// The third sequence to merge. + /// A sequence of tuples with elements taken from the first, second, and third sequences, in that order. + public static IEnumerable<(TFirst First, TSecond Second, TThird Third)> Zip(this IEnumerable first, IEnumerable second, IEnumerable third) + { + if (first is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.first); + } + + if (second is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.second); + } + + if (third is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.third); + } + + return ZipIterator(first, second, third); + } + private static IEnumerable<(TFirst First, TSecond Second)> ZipIterator(IEnumerable first, IEnumerable second) { using (IEnumerator e1 = first.GetEnumerator()) @@ -65,5 +95,18 @@ private static IEnumerable ZipIterator(IEnume } } } + + private static IEnumerable<(TFirst First, TSecond Second, TThird Third)> ZipIterator(IEnumerable first, IEnumerable second, IEnumerable third) + { + using (IEnumerator e1 = first.GetEnumerator()) + using (IEnumerator e2 = second.GetEnumerator()) + using (IEnumerator e3 = third.GetEnumerator()) + { + while (e1.MoveNext() && e2.MoveNext() && e3.MoveNext()) + { + yield return (e1.Current, e2.Current, e3.Current); + } + } + } } } diff --git a/src/libraries/System.Linq/tests/ZipTests.cs b/src/libraries/System.Linq/tests/ZipTests.cs index 628201460666c..cfaa8bade6577 100644 --- a/src/libraries/System.Linq/tests/ZipTests.cs +++ b/src/libraries/System.Linq/tests/ZipTests.cs @@ -393,7 +393,7 @@ public void Zip2_ImplicitTypeParameters() { IEnumerable first = new int[] { 1, 2, 3 }; IEnumerable second = new int[] { 2, 5, 9 }; - IEnumerable<(int, int)> expected = new (int,int)[] { (1,2), (2,5), (3,9) }; + IEnumerable<(int, int)> expected = new (int, int)[] { (1, 2), (2, 5), (3, 9) }; Assert.Equal(expected, first.Zip(second)); } @@ -403,7 +403,7 @@ public void Zip2_ExplicitTypeParameters() { IEnumerable first = new int[] { 1, 2, 3 }; IEnumerable second = new int[] { 2, 5, 9 }; - IEnumerable<(int, int)> expected = new (int,int)[] { (1,2), (2,5), (3,9) }; + IEnumerable<(int, int)> expected = new (int, int)[] { (1, 2), (2, 5), (3, 9) }; Assert.Equal(expected, first.Zip(second)); } @@ -431,7 +431,7 @@ public void Zip2_ExceptionThrownFromFirstsEnumerator() { ThrowsOnMatchEnumerable first = new ThrowsOnMatchEnumerable(new int[] { 1, 3, 3 }, 2); IEnumerable second = new int[] { 2, 4, 6 }; - IEnumerable<(int, int)> expected = new (int,int)[] { (1,2), (3,4), (3,6) }; + IEnumerable<(int, int)> expected = new (int, int)[] { (1, 2), (3, 4), (3, 6) }; Assert.Equal(expected, first.Zip(second)); @@ -447,7 +447,7 @@ public void Zip2_ExceptionThrownFromSecondsEnumerator() { ThrowsOnMatchEnumerable second = new ThrowsOnMatchEnumerable(new int[] { 1, 3, 3 }, 2); IEnumerable first = new int[] { 2, 4, 6 }; - IEnumerable<(int, int)> expected = new (int,int)[] { (2,1), (4,3), (6,3) }; + IEnumerable<(int, int)> expected = new (int, int)[] { (2, 1), (4, 3), (6, 3) }; Assert.Equal(expected, first.Zip(second)); @@ -601,5 +601,123 @@ public void Zip2_TupleNames() Assert.Equal(t.Item1, t.First); Assert.Equal(t.Item2, t.Second); } + + [Fact] + public void Zip3_FirstIsNull() + { + IEnumerable first = null; + IEnumerable second = new[] { 4, 5, 6 }; + IEnumerable third = new[] { 7, 8, 9 }; + + AssertExtensions.Throws("first", () => first.Zip(second, third)); + } + + [Fact] + public void Zip3_SecondIsNull() + { + IEnumerable first = new[] { 1, 2, 3 }; + IEnumerable second = null; + IEnumerable third = new[] { 4, 5, 6 }; + + AssertExtensions.Throws("second", () => first.Zip(second, third)); + } + + [Fact] + public void Zip3_ThirdIsNull() + { + IEnumerable first = new[] { 1, 2, 3 }; + IEnumerable second = new[] { 4, 5, 6 }; + IEnumerable third = null; + + AssertExtensions.Throws("third", () => first.Zip(second, third)); + } + + [Fact] + public void Zip3_ThirdEmpty() + { + IEnumerable first = new[] { 1, 2, 3 }; + IEnumerable second = new[] { 4, 5, 6 }; + IEnumerable third = new int[] { }; + IEnumerable<(int, int, int)> expected = new (int, int, int)[] { }; + + Assert.Equal(expected, first.Zip(second, third)); + } + + [Fact] + public void Zip3_ImplicitTypeParameters() + { + IEnumerable first = new[] { 1, 2 }; + IEnumerable second = new[] { 3, 4 }; + IEnumerable third = new[] { 5, 6 }; + IEnumerable<(int, int, int)> expected = new[] { (1, 3, 5), (2, 4, 6) }; + + Assert.Equal(expected, first.Zip(second, third)); + } + + [Fact] + public void Zip3_ExplicitTypeParameters() + { + IEnumerable first = new[] { 1, 2 }; + IEnumerable second = new[] { 3, 4 }; + IEnumerable third = new[] { 5, 6 }; + IEnumerable<(int, int, int)> expected = new[] { (1, 3, 5), (2, 4, 6) }; + + Assert.Equal(expected, first.Zip(second, third)); + } + + [Fact] + public void Zip3_ThirdOneMore() + { + IEnumerable first = new[] { 1, 2 }; + IEnumerable second = new[] { 3, 4 }; + IEnumerable third = new[] { 5, 6, 7 }; + IEnumerable<(int, int, int)> expected = new[] { (1, 3, 5), (2, 4, 6) }; + + Assert.Equal(expected, first.Zip(second, third)); + } + + [Fact] + public void Zip3_ThirdManyMore() + { + IEnumerable first = new[] { 1, 2 }; + IEnumerable second = new[] { 3, 4 }; + IEnumerable third = new[] { 5, 6, 7, 8 }; + IEnumerable<(int, int, int)> expected = new[] { (1, 3, 5), (2, 4, 6) }; + + Assert.Equal(expected, first.Zip(second, third)); + } + + [Fact] + public void Zip3_ThirdOneLess() + { + IEnumerable first = new[] { 1, 2 }; + IEnumerable second = new[] { 3, 4 }; + IEnumerable third = new[] { 5 }; + IEnumerable<(int, int, int)> expected = new[] { (1, 3, 5) }; + + Assert.Equal(expected, first.Zip(second, third)); + } + + [Fact] + public void Zip3_ThirdManyLess() + { + IEnumerable first = new[] { 1, 2, 3 }; + IEnumerable second = new[] { 3, 4, 5 }; + IEnumerable third = new[] { 5 }; + IEnumerable<(int, int, int)> expected = new[] { (1, 3, 5) }; + + Assert.Equal(expected, first.Zip(second, third)); + } + + [Fact] + public void Zip3_RunOnce() + { + IEnumerable first = new[] { 1, 2 }; + IEnumerable second = new[] { 3, 4 }; + IEnumerable third = new[] { 5, 6 }; + IEnumerable<(int, int, int)> expected = new[] { (1, 3, 5), (2, 4, 6) }; + + Assert.Equal(expected, first.RunOnce().Zip(second.RunOnce(), third.RunOnce())); + } } }