diff --git a/src/libraries/System.Linq.AsyncEnumerable/ref/System.Linq.AsyncEnumerable.cs b/src/libraries/System.Linq.AsyncEnumerable/ref/System.Linq.AsyncEnumerable.cs index 881b679a19f3ae..5ab4bf6eb0522b 100644 --- a/src/libraries/System.Linq.AsyncEnumerable/ref/System.Linq.AsyncEnumerable.cs +++ b/src/libraries/System.Linq.AsyncEnumerable/ref/System.Linq.AsyncEnumerable.cs @@ -65,6 +65,10 @@ public static partial class AsyncEnumerable public static System.Threading.Tasks.ValueTask FirstOrDefaultAsync(this System.Collections.Generic.IAsyncEnumerable source, System.Func> predicate, TSource defaultValue, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static System.Threading.Tasks.ValueTask FirstOrDefaultAsync(this System.Collections.Generic.IAsyncEnumerable source, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static System.Threading.Tasks.ValueTask FirstOrDefaultAsync(this System.Collections.Generic.IAsyncEnumerable source, TSource defaultValue, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Collections.Generic.IAsyncEnumerable<(TOuter? Outer, TInner? Inner)> FullJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func> outerKeySelector, System.Func> innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } + public static System.Collections.Generic.IAsyncEnumerable<(TOuter? Outer, TInner? Inner)> FullJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } + public static System.Collections.Generic.IAsyncEnumerable FullJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func> outerKeySelector, System.Func> innerKeySelector, System.Func> resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } + public static System.Collections.Generic.IAsyncEnumerable FullJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IAsyncEnumerable> GroupBy(this System.Collections.Generic.IAsyncEnumerable source, System.Func> keySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IAsyncEnumerable> GroupBy(this System.Collections.Generic.IAsyncEnumerable source, System.Func keySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IAsyncEnumerable> GroupBy(this System.Collections.Generic.IAsyncEnumerable source, System.Func> keySelector, System.Func> elementSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } diff --git a/src/libraries/System.Linq.AsyncEnumerable/src/System.Linq.AsyncEnumerable.csproj b/src/libraries/System.Linq.AsyncEnumerable/src/System.Linq.AsyncEnumerable.csproj index 62dda55139c872..75da8be90d810e 100644 --- a/src/libraries/System.Linq.AsyncEnumerable/src/System.Linq.AsyncEnumerable.csproj +++ b/src/libraries/System.Linq.AsyncEnumerable/src/System.Linq.AsyncEnumerable.csproj @@ -40,6 +40,7 @@ + diff --git a/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/FullJoin.cs b/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/FullJoin.cs new file mode 100644 index 00000000000000..0e7c7ecb228484 --- /dev/null +++ b/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/FullJoin.cs @@ -0,0 +1,242 @@ +// 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 System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Linq +{ + public static partial class AsyncEnumerable + { + /// Correlates the elements of two async sequences based on matching keys, producing a result for matched and unmatched elements. + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// A function to create a result element from two matching elements. + /// An to use to hash and compare keys. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// The type of the result elements. + /// An that has elements of type that are obtained by performing a full outer join on two sequences. + /// is . + /// is . + /// is . + /// is . + /// is . + public static IAsyncEnumerable FullJoin( + this IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func outerKeySelector, + Func innerKeySelector, + Func resultSelector, + IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + ArgumentNullException.ThrowIfNull(resultSelector); + + return Impl(outer, inner, outerKeySelector, innerKeySelector, resultSelector, comparer, default); + + static async IAsyncEnumerable Impl( + IAsyncEnumerable outer, IAsyncEnumerable inner, + Func outerKeySelector, + Func innerKeySelector, + Func resultSelector, + IEqualityComparer? comparer, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + AsyncLookup innerLookup = await AsyncLookup.CreateForJoinAsync(inner, innerKeySelector, comparer, cancellationToken); + + HashSet>? matchedGroupings = innerLookup.Count != 0 + ? new HashSet>() + : null; + + await foreach (TOuter item in outer.WithCancellation(cancellationToken)) + { + Grouping? g = innerLookup.GetGrouping(outerKeySelector(item), create: false); + if (g is null) + { + yield return resultSelector(item, default); + } + else + { + matchedGroupings!.Add(g); + int count = g._count; + TInner[] elements = g._elements; + for (int i = 0; i != count; ++i) + { + yield return resultSelector(item, elements[i]); + } + } + } + + if (matchedGroupings is null || matchedGroupings.Count < innerLookup.Count) + { + Grouping? g = innerLookup._lastGrouping; + if (g is not null) + { + do + { + g = g._next!; + if (matchedGroupings is null || !matchedGroupings.Contains(g)) + { + int count = g._count; + TInner[] elements = g._elements; + for (int i = 0; i != count; ++i) + { + yield return resultSelector(default, elements[i]); + } + } + } + while (g != innerLookup._lastGrouping); + } + } + } + } + + /// Correlates the elements of two async sequences based on matching keys, producing a result for matched and unmatched elements. + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// A function to create a result element from two matching elements. + /// An to use to hash and compare keys. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// The type of the result elements. + /// An that has elements of type that are obtained by performing a full outer join on two sequences. + /// is . + /// is . + /// is . + /// is . + /// is . + public static IAsyncEnumerable FullJoin( + this IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func> outerKeySelector, + Func> innerKeySelector, + Func> resultSelector, + IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + ArgumentNullException.ThrowIfNull(resultSelector); + + return Impl(outer, inner, outerKeySelector, innerKeySelector, resultSelector, comparer, default); + + static async IAsyncEnumerable Impl( + IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func> outerKeySelector, + Func> innerKeySelector, + Func> resultSelector, + IEqualityComparer? comparer, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + AsyncLookup innerLookup = await AsyncLookup.CreateForJoinAsync(inner, innerKeySelector, comparer, cancellationToken); + + HashSet>? matchedGroupings = innerLookup.Count != 0 + ? new HashSet>() + : null; + + await foreach (TOuter item in outer.WithCancellation(cancellationToken)) + { + Grouping? g = innerLookup.GetGrouping(await outerKeySelector(item, cancellationToken), create: false); + if (g is null) + { + yield return await resultSelector(item, default, cancellationToken); + } + else + { + matchedGroupings!.Add(g); + int count = g._count; + TInner[] elements = g._elements; + for (int i = 0; i != count; ++i) + { + yield return await resultSelector(item, elements[i], cancellationToken); + } + } + } + + if (matchedGroupings is null || matchedGroupings.Count < innerLookup.Count) + { + Grouping? g = innerLookup._lastGrouping; + if (g is not null) + { + do + { + g = g._next!; + if (matchedGroupings is null || !matchedGroupings.Contains(g)) + { + int count = g._count; + TInner[] elements = g._elements; + for (int i = 0; i != count; ++i) + { + yield return await resultSelector(default, elements[i], cancellationToken); + } + } + } + while (g != innerLookup._lastGrouping); + } + } + } + } + + /// Correlates the elements of two async sequences based on matching keys, producing a tuple for matched and unmatched elements. + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to use to hash and compare keys. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// An that has elements of type (TOuter?, TInner?) that are obtained by performing a full outer join on two sequences. + /// is . + /// is . + /// is . + /// is . + public static IAsyncEnumerable<(TOuter? Outer, TInner? Inner)> FullJoin( + this IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func outerKeySelector, + Func innerKeySelector, + IEqualityComparer? comparer = null) + { + return FullJoin(outer, inner, outerKeySelector, innerKeySelector, static (outer, inner) => (outer, inner), comparer); + } + + /// Correlates the elements of two async sequences based on matching keys, producing a tuple for matched and unmatched elements. + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to use to hash and compare keys. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// An that has elements of type (TOuter?, TInner?) that are obtained by performing a full outer join on two sequences. + /// is . + /// is . + /// is . + /// is . + public static IAsyncEnumerable<(TOuter? Outer, TInner? Inner)> FullJoin( + this IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func> outerKeySelector, + Func> innerKeySelector, + IEqualityComparer? comparer = null) + { + return FullJoin(outer, inner, outerKeySelector, innerKeySelector, static (outer, inner, ct) => new ValueTask<(TOuter?, TInner?)>((outer, inner)), comparer); + } + } +} diff --git a/src/libraries/System.Linq.AsyncEnumerable/tests/FullJoinTests.cs b/src/libraries/System.Linq.AsyncEnumerable/tests/FullJoinTests.cs new file mode 100644 index 00000000000000..e858d6530997a9 --- /dev/null +++ b/src/libraries/System.Linq.AsyncEnumerable/tests/FullJoinTests.cs @@ -0,0 +1,175 @@ +// 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 System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace System.Linq.Tests +{ + public class FullJoinTests : AsyncEnumerableTests + { + [Fact] + public void InvalidInputs_Throws() + { + AssertExtensions.Throws("outer", () => AsyncEnumerable.FullJoin((IAsyncEnumerable)null, AsyncEnumerable.Empty(), outer => outer, inner => inner, (outer, inner) => outer + inner)); + AssertExtensions.Throws("inner", () => AsyncEnumerable.FullJoin(AsyncEnumerable.Empty(), (IAsyncEnumerable)null, outer => outer, inner => inner, (outer, inner) => outer + inner)); + AssertExtensions.Throws("outerKeySelector", () => AsyncEnumerable.FullJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), (Func)null, inner => inner, (outer, inner) => outer + inner)); + AssertExtensions.Throws("innerKeySelector", () => AsyncEnumerable.FullJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), outer => outer, (Func)null, (outer, inner) => outer + inner)); + AssertExtensions.Throws("resultSelector", () => AsyncEnumerable.FullJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), outer => outer, inner => inner, (Func)null)); + + AssertExtensions.Throws("outer", () => AsyncEnumerable.FullJoin((IAsyncEnumerable)null, AsyncEnumerable.Empty(), async (outer, ct) => outer, async (inner, ct) => inner, async (outer, inner, ct) => outer + inner)); + AssertExtensions.Throws("inner", () => AsyncEnumerable.FullJoin(AsyncEnumerable.Empty(), (IAsyncEnumerable)null, async (outer, ct) => outer, async (inner, ct) => inner, async (outer, inner, ct) => outer + inner)); + AssertExtensions.Throws("outerKeySelector", () => AsyncEnumerable.FullJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), (Func>)null, async (inner, ct) => inner, async (outer, inner, ct) => outer + inner)); + AssertExtensions.Throws("innerKeySelector", () => AsyncEnumerable.FullJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), async (outer, ct) => outer, (Func>)null, async (outer, inner, ct) => outer + inner)); + AssertExtensions.Throws("resultSelector", () => AsyncEnumerable.FullJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), async (outer, ct) => outer, async (inner, ct) => inner, (Func>)null)); + } + + [Fact] + public async Task BothEmpty_ProducesEmpty() + { + IAsyncEnumerable empty = AsyncEnumerable.Empty(); + + Assert.Empty(await empty.FullJoin(empty, s => s, s => s, (s1, s2) => s1).ToListAsync()); + Assert.Empty(await empty.FullJoin(empty, async (s, ct) => s, async (s, ct) => s, async (s1, s2, ct) => s1).ToListAsync()); + } + +#if NET + [Fact] + public async Task VariousValues_MatchesEnumerable_String() + { + Random rand = new(42); + foreach (int length in new[] { 0, 1, 2, 1000 }) + { + string[] values = new string[length]; + FillRandom(rand, values); + + foreach (IAsyncEnumerable source in CreateSources(values)) + { + await AssertEqual( + values.FullJoin(values, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ', (outer, inner) => outer + inner), + source.FullJoin(source, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ', (outer, inner) => outer + inner)); + + await AssertEqual( + values.FullJoin(values, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ', (outer, inner) => outer + inner), + source.FullJoin(source, async (s, ct) => s.Length > 0 ? s[0] : ' ', async (s, ct) => s.Length > 1 ? s[1] : ' ', async (outer, inner, ct) => outer + inner)); + } + } + } +#endif + + [Fact] + public async Task Cancellation_Cancels() + { + IAsyncEnumerable source = CreateSource(2, 4, 8, 16); + + await Assert.ThrowsAsync(async () => + { + CancellationTokenSource cts = new(); + await ConsumeAsync(source.FullJoin(source, outer => + { + cts.Cancel(); + return outer; + }, + inner => + { + return inner; + }, + (outer, inner) => + { + return outer + inner; + }).WithCancellation(cts.Token)); + }); + + await Assert.ThrowsAsync(async () => + { + CancellationTokenSource cts = new(); + await ConsumeAsync(source.FullJoin(source, + async (outer, ct) => + { + Assert.Equal(cts.Token, ct); + await Task.Yield(); + cts.Cancel(); + return outer; + }, + async (inner, ct) => + { + return inner; + }, + async (outer, inner, ct) => + { + return outer + inner; + }).WithCancellation(cts.Token)); + }); + + await Assert.ThrowsAsync(async () => + { + CancellationTokenSource cts = new(); + await ConsumeAsync(source.FullJoin(source, + async (outer, ct) => + { + return outer; + }, + async (inner, ct) => + { + Assert.Equal(cts.Token, ct); + await Task.Yield(); + cts.Cancel(); + return inner; + }, + async (outer, inner, ct) => + { + return outer + inner; + }).WithCancellation(cts.Token)); + }); + + await Assert.ThrowsAsync(async () => + { + CancellationTokenSource cts = new(); + await ConsumeAsync(source.FullJoin(source, + async (outer, ct) => + { + return outer; + }, + async (inner, ct) => + { + return inner; + }, + async (outer, inner, ct) => + { + Assert.Equal(cts.Token, ct); + await Task.Yield(); + cts.Cancel(); + return outer + inner; + }).WithCancellation(cts.Token)); + }); + } + + [Fact] + public async Task InterfaceCalls_ExpectedCounts() + { + TrackingAsyncEnumerable outer, inner; + + outer = CreateSource(2, 4, 8, 16).Track(); + inner = CreateSource(1, 2, 3, 4).Track(); + await ConsumeAsync(outer.FullJoin(inner, outer => outer, inner => inner, (outer, inner) => outer + inner)); + Assert.Equal(5, outer.MoveNextAsyncCount); + Assert.Equal(4, outer.CurrentCount); + Assert.Equal(1, outer.DisposeAsyncCount); + Assert.Equal(5, inner.MoveNextAsyncCount); + Assert.Equal(4, inner.CurrentCount); + Assert.Equal(1, inner.DisposeAsyncCount); + + outer = CreateSource(2, 4, 8, 16).Track(); + inner = CreateSource(1, 2, 3, 4).Track(); + await ConsumeAsync(outer.FullJoin(inner, async (outer, ct) => outer, async (inner, ct) => inner, async (outer, inner, ct) => outer + inner)); + Assert.Equal(5, outer.MoveNextAsyncCount); + Assert.Equal(4, outer.CurrentCount); + Assert.Equal(1, outer.DisposeAsyncCount); + Assert.Equal(5, inner.MoveNextAsyncCount); + Assert.Equal(4, inner.CurrentCount); + Assert.Equal(1, inner.DisposeAsyncCount); + } + } +} diff --git a/src/libraries/System.Linq.AsyncEnumerable/tests/System.Linq.AsyncEnumerable.Tests.csproj b/src/libraries/System.Linq.AsyncEnumerable/tests/System.Linq.AsyncEnumerable.Tests.csproj index db26a34f5d4dd1..184897f9038f40 100644 --- a/src/libraries/System.Linq.AsyncEnumerable/tests/System.Linq.AsyncEnumerable.Tests.csproj +++ b/src/libraries/System.Linq.AsyncEnumerable/tests/System.Linq.AsyncEnumerable.Tests.csproj @@ -29,6 +29,7 @@ + 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 2d63b858f0bd26..ad760708a59573 100644 --- a/src/libraries/System.Linq.Queryable/ref/System.Linq.Queryable.cs +++ b/src/libraries/System.Linq.Queryable/ref/System.Linq.Queryable.cs @@ -100,6 +100,8 @@ public static partial class Queryable public static TSource FirstOrDefault(this System.Linq.IQueryable source, TSource defaultValue) { throw null; } public static TSource First(this System.Linq.IQueryable source) { throw null; } public static TSource First(this System.Linq.IQueryable source, System.Linq.Expressions.Expression> predicate) { throw null; } + public static System.Linq.IQueryable<(TOuter? Outer, TInner? Inner)> FullJoin(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } + public static System.Linq.IQueryable FullJoin(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Linq.Expressions.Expression> resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Linq.IQueryable> GroupBy(this System.Linq.IQueryable source, System.Linq.Expressions.Expression> keySelector) { throw null; } public static System.Linq.IQueryable> GroupBy(this System.Linq.IQueryable source, System.Linq.Expressions.Expression> keySelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } public static System.Linq.IQueryable> GroupBy(this System.Linq.IQueryable source, System.Linq.Expressions.Expression> keySelector, System.Linq.Expressions.Expression> elementSelector) { throw null; } 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 1290b8f6819e23..3f4b9db8bd0691 100644 --- a/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs +++ b/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs @@ -1094,6 +1094,89 @@ public static IQueryable RightJoin(this outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector), Expression.Quote(resultSelector), Expression.Constant(comparer, typeof(IEqualityComparer)))); } + /// + /// Correlates the elements of two sequences based on matching keys, producing a result for each element + /// in either sequence that has a match as well as for elements that have no match. + /// A default or specified equality comparer is used to compare keys. + /// + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// A function to create a result element from two matching elements. + /// An to hash and compare keys, or to use the default equality comparer. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// The type of the result elements. + /// An that has elements of type that are obtained by performing a full outer join on two sequences. + /// or or or or is . + /// + /// If is , the default equality comparer, , is used to hash and compare keys. + /// + /// This method has at least one parameter of type whose type argument is one of the types. + /// For these parameters, you can pass in a lambda expression and it will be compiled to an . + /// + /// + /// The method + /// generates a that represents calling + /// + /// itself as a constructed generic method. + /// It then passes the to the method of the represented by the property of the parameter. + /// + /// + /// The query behavior that occurs as a result of executing an expression tree that represents calling + /// + /// depends on the implementation of the type of the parameter. + /// The expected behavior is that of a full outer join. + /// + /// + [DynamicDependency("FullJoin`4", typeof(Enumerable))] + public static IQueryable FullJoin(this IQueryable outer, IEnumerable inner, Expression> outerKeySelector, Expression> innerKeySelector, Expression> resultSelector, IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + ArgumentNullException.ThrowIfNull(resultSelector); + + return outer.Provider.CreateQuery( + Expression.Call( + null, + new Func, IEnumerable, Expression>, Expression>, Expression>, IEqualityComparer, IQueryable>(FullJoin).Method, + outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector), Expression.Quote(resultSelector), Expression.Constant(comparer, typeof(IEqualityComparer)))); + } + + /// + /// Correlates the elements of two sequences based on matching keys, producing a tuple for each element + /// in either sequence that has a match as well as for elements that have no match. + /// A default or specified equality comparer is used to compare keys. + /// + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to hash and compare keys, or to use the default equality comparer. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// An that has elements of type (TOuter?, TInner?) that are obtained by performing a full outer join on two sequences. + /// or or or is . + [DynamicDependency("FullJoin`3", typeof(Enumerable))] + public static IQueryable<(TOuter? Outer, TInner? Inner)> FullJoin(this IQueryable outer, IEnumerable inner, Expression> outerKeySelector, Expression> innerKeySelector, IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return outer.Provider.CreateQuery<(TOuter? Outer, TInner? Inner)>( + Expression.Call( + null, + new Func, IEnumerable, Expression>, Expression>, IEqualityComparer, IQueryable<(TOuter? Outer, TInner? Inner)>>(FullJoin).Method, + outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector), Expression.Constant(comparer, typeof(IEqualityComparer)))); + } + [DynamicDependency("ThenBy`2", typeof(Enumerable))] public static IOrderedQueryable ThenBy(this IOrderedQueryable source, Expression> keySelector) { diff --git a/src/libraries/System.Linq.Queryable/tests/FullJoinTests.cs b/src/libraries/System.Linq.Queryable/tests/FullJoinTests.cs new file mode 100644 index 00000000000000..860e6e5f61f31b --- /dev/null +++ b/src/libraries/System.Linq.Queryable/tests/FullJoinTests.cs @@ -0,0 +1,298 @@ +// 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 System.Linq.Expressions; +using Xunit; + +namespace System.Linq.Tests +{ + public class FullJoinTests : EnumerableBasedTests + { + public struct CustomerRec + { + public string name; + public int custID; + } + + public struct OrderRec + { + public int orderID; + public int custID; + public int total; + } + + public struct AnagramRec + { + public string name; + public int orderID; + public int total; + } + + public struct JoinRec + { + public string name; + public int orderID; + public int total; + } + + [Fact] + public void FirstOuterMatchesLastInnerLastOuterMatchesFirstInnerSameNumberElements() + { + CustomerRec[] outer = { + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + }; + OrderRec[] inner = { + new OrderRec{ orderID = 45321, custID = 99022, total = 50 }, + new OrderRec{ orderID = 43421, custID = 29022, total = 20 }, + new OrderRec{ orderID = 95421, custID = 98022, total = 9 } + }; + JoinRec[] expected = { + new JoinRec{ name = "Prakash", orderID = 95421, total = 9 }, + new JoinRec{ name = "Tim", orderID = 0, total = 0 }, + new JoinRec{ name = "Robert", orderID = 45321, total = 50 }, + new JoinRec{ name = null, orderID = 43421, total = 20 } + }; + + Assert.Equal(expected, outer.AsQueryable().FullJoin(inner.AsQueryable(), e => e.custID, e => e.custID, (cr, or) => new JoinRec { name = cr.name, orderID = or.orderID, total = or.total })); + } + + [Fact] + public void NullComparer() + { + CustomerRec[] outer = { + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + }; + AnagramRec[] inner = { + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + }; + JoinRec[] expected = { + new JoinRec{ name = "Prakash", orderID = 323232, total = 9 }, + new JoinRec{ name = "Tim", orderID = 0, total = 0 }, + new JoinRec{ name = "Robert", orderID = 0, total = 0 }, + new JoinRec{ name = null, orderID = 43455, total = 10 } + }; + + Assert.Equal(expected, outer.AsQueryable().FullJoin(inner.AsQueryable(), e => e.name, e => e.name, (cr, or) => new JoinRec { name = cr.name, orderID = or.orderID, total = or.total }, null)); + } + + [Fact] + public void CustomComparer() + { + CustomerRec[] outer = { + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + }; + AnagramRec[] inner = { + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + }; + JoinRec[] expected = { + new JoinRec{ name = "Prakash", orderID = 323232, total = 9 }, + new JoinRec{ name = "Tim", orderID = 43455, total = 10 }, + new JoinRec{ name = "Robert", orderID = 0, total = 0 } + }; + + Assert.Equal(expected, outer.AsQueryable().FullJoin(inner.AsQueryable(), e => e.name, e => e.name, (cr, or) => new JoinRec { name = cr.name, orderID = or.orderID, total = or.total }, new AnagramEqualityComparer())); + } + + [Fact] + public void OuterNull() + { + IQueryable outer = null; + AnagramRec[] inner = { + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + }; + + AssertExtensions.Throws("outer", () => outer.FullJoin(inner.AsQueryable(), e => e.name, e => e.name, (cr, or) => new JoinRec { name = cr.name, orderID = or.orderID, total = or.total }, new AnagramEqualityComparer())); + } + + [Fact] + public void InnerNull() + { + CustomerRec[] outer = { + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + }; + IQueryable inner = null; + + AssertExtensions.Throws("inner", () => outer.AsQueryable().FullJoin(inner, e => e.name, e => e.name, (cr, or) => new JoinRec { name = cr.name, orderID = or.orderID, total = or.total }, new AnagramEqualityComparer())); + } + + [Fact] + public void OuterKeySelectorNull() + { + CustomerRec[] outer = { + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + }; + AnagramRec[] inner = { + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + }; + + AssertExtensions.Throws("outerKeySelector", () => outer.AsQueryable().FullJoin(inner.AsQueryable(), null, e => e.name, (cr, or) => new JoinRec { name = cr.name, orderID = or.orderID, total = or.total }, new AnagramEqualityComparer())); + } + + [Fact] + public void InnerKeySelectorNull() + { + CustomerRec[] outer = { + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + }; + AnagramRec[] inner = { + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + }; + + AssertExtensions.Throws("innerKeySelector", () => outer.AsQueryable().FullJoin(inner.AsQueryable(), e => e.name, null, (cr, or) => new JoinRec { name = cr.name, orderID = or.orderID, total = or.total }, new AnagramEqualityComparer())); + } + + [Fact] + public void ResultSelectorNull() + { + CustomerRec[] outer = { + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + }; + AnagramRec[] inner = { + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + }; + + AssertExtensions.Throws("resultSelector", () => Queryable.FullJoin(outer.AsQueryable(), inner.AsQueryable(), e => e.name, e => e.name, (Expression>)null, new AnagramEqualityComparer())); + } + + [Fact] + public void OuterNullNoComparer() + { + IQueryable outer = null; + AnagramRec[] inner = { + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + }; + + AssertExtensions.Throws("outer", () => outer.FullJoin(inner.AsQueryable(), e => e.name, e => e.name, (cr, or) => new JoinRec { name = cr.name, orderID = or.orderID, total = or.total })); + } + + [Fact] + public void InnerNullNoComparer() + { + CustomerRec[] outer = { + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + }; + IQueryable inner = null; + + AssertExtensions.Throws("inner", () => outer.AsQueryable().FullJoin(inner, e => e.name, e => e.name, (cr, or) => new JoinRec { name = cr.name, orderID = or.orderID, total = or.total })); + } + + [Fact] + public void OuterKeySelectorNullNoComparer() + { + CustomerRec[] outer = { + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + }; + AnagramRec[] inner = { + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + }; + + AssertExtensions.Throws("outerKeySelector", () => outer.AsQueryable().FullJoin(inner.AsQueryable(), null, e => e.name, (cr, or) => new JoinRec { name = cr.name, orderID = or.orderID, total = or.total })); + } + + [Fact] + public void InnerKeySelectorNullNoComparer() + { + CustomerRec[] outer = { + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + }; + AnagramRec[] inner = { + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + }; + + AssertExtensions.Throws("innerKeySelector", () => outer.AsQueryable().FullJoin(inner.AsQueryable(), e => e.name, null, (cr, or) => new JoinRec { name = cr.name, orderID = or.orderID, total = or.total })); + } + + [Fact] + public void ResultSelectorNullNoComparer() + { + CustomerRec[] outer = { + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + }; + AnagramRec[] inner = { + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + }; + + AssertExtensions.Throws("resultSelector", () => Queryable.FullJoin(outer.AsQueryable(), inner.AsQueryable(), e => e.name, e => e.name, (Expression>)null)); + } + + [Fact] + public void SelectorsReturnNull() + { + int?[] outer = { null, null }; + int?[] inner = { null, null, null }; + int?[] expected = { null, null }; + + Assert.Equal(expected, outer.AsQueryable().FullJoin(inner.AsQueryable(), e => e, e => e, (x, y) => x)); + Assert.Equal(expected, outer.AsQueryable().FullJoin(inner.AsQueryable(), e => e, e => e, (x, y) => y)); + } + + [Fact] + public void Join1() + { + var count = new[] { 0, 1, 2 }.AsQueryable().FullJoin(new[] { 1, 2, 3 }, n1 => n1, n2 => n2, (n1, n2) => n1 + n2).Count(); + Assert.Equal(4, count); + } + + [Fact] + public void Join2() + { + var count = new[] { 0, 1, 2 }.AsQueryable().FullJoin(new[] { 1, 2, 3 }, n1 => n1, n2 => n2, (n1, n2) => n1 + n2, EqualityComparer.Default).Count(); + Assert.Equal(4, count); + } + + [Fact] + public void TupleOverload() + { + var result = new[] { 0, 1, 2 }.AsQueryable().FullJoin(new[] { 1, 2, 3 }.AsQueryable(), n1 => n1, n2 => n2).ToArray(); + Assert.Equal(4, result.Length); + Assert.Equal((0, 0), result[0]); + Assert.Equal((1, 1), result[1]); + Assert.Equal((2, 2), result[2]); + Assert.Equal((0, 3), result[3]); + } + + [Fact] + public void TupleOverloadWithComparer() + { + var result = new[] { 0, 1, 2 }.AsQueryable().FullJoin(new[] { 1, 2, 3 }.AsQueryable(), n1 => n1, n2 => n2, EqualityComparer.Default).ToArray(); + Assert.Equal(4, result.Length); + Assert.Equal((0, 0), result[0]); + Assert.Equal((1, 1), result[1]); + Assert.Equal((2, 2), result[2]); + Assert.Equal((0, 3), result[3]); + } + } +} 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 9fcf767d3e81b0..8b6c03c7a5a7e0 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 @@ -23,6 +23,7 @@ + diff --git a/src/libraries/System.Linq/ref/System.Linq.cs b/src/libraries/System.Linq/ref/System.Linq.cs index 0c2d82b2111766..e7119079e9f4b0 100644 --- a/src/libraries/System.Linq/ref/System.Linq.cs +++ b/src/libraries/System.Linq/ref/System.Linq.cs @@ -73,6 +73,8 @@ public static System.Collections.Generic.IEnumerable< public static TSource FirstOrDefault(this System.Collections.Generic.IEnumerable source, System.Func predicate, TSource defaultValue) { throw null; } public static TSource First(this System.Collections.Generic.IEnumerable source) { throw null; } public static TSource First(this System.Collections.Generic.IEnumerable source, System.Func predicate) { throw null; } + public static System.Collections.Generic.IEnumerable<(TOuter? Outer, TInner? Inner)> FullJoin(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } + public static System.Collections.Generic.IEnumerable FullJoin(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IEnumerable> GroupBy(this System.Collections.Generic.IEnumerable source, System.Func keySelector) { throw null; } public static System.Collections.Generic.IEnumerable> GroupBy(this System.Collections.Generic.IEnumerable source, System.Func keySelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } public static System.Collections.Generic.IEnumerable> GroupBy(this System.Collections.Generic.IEnumerable source, System.Func keySelector, System.Func elementSelector) { throw null; } diff --git a/src/libraries/System.Linq/src/System.Linq.csproj b/src/libraries/System.Linq/src/System.Linq.csproj index f032db1a303ee1..d6157849ad26df 100644 --- a/src/libraries/System.Linq/src/System.Linq.csproj +++ b/src/libraries/System.Linq/src/System.Linq.csproj @@ -29,6 +29,7 @@ + diff --git a/src/libraries/System.Linq/src/System/Linq/FullJoin.cs b/src/libraries/System.Linq/src/System/Linq/FullJoin.cs new file mode 100644 index 00000000000000..ce8d9e3591c7f3 --- /dev/null +++ b/src/libraries/System.Linq/src/System/Linq/FullJoin.cs @@ -0,0 +1,149 @@ +// 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 + { + /// + /// Correlates the elements of two sequences based on matching keys, producing a result for each element + /// in either sequence that has a match as well as for elements that have no match. + /// A default or specified equality comparer is used to compare keys. + /// + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// A function to create a result element from two matching elements. + /// An to hash and compare keys, or to use the default equality comparer. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// The type of the result elements. + /// An that has elements of type that are obtained by performing a full outer join on two sequences. + /// or or or or is . + /// + /// + /// 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 C# or For Each in Visual Basic. + /// + /// If is , the default equality comparer, , is used to hash and compare keys. + /// + /// In relational database terms, the method implements a full outer equijoin. + /// 'Full outer' means that elements of both sequences are returned regardless of whether matching elements are found in the other sequence. + /// An 'equijoin' is a join in which the keys are compared for equality. + /// + /// + public static IEnumerable FullJoin(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func resultSelector, IEqualityComparer? comparer = null) + { + if (outer is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.outer); + } + + if (inner is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.inner); + } + + if (outerKeySelector is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.outerKeySelector); + } + + if (innerKeySelector is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.innerKeySelector); + } + + if (resultSelector is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.resultSelector); + } + + return FullJoinIterator(outer, inner, outerKeySelector, innerKeySelector, resultSelector, comparer); + } + + /// + /// Correlates the elements of two sequences based on matching keys, producing a tuple for each element + /// in either sequence that has a match as well as for elements that have no match. + /// A default or specified equality comparer is used to compare keys. + /// + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to hash and compare keys, or to use the default equality comparer. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// An that has elements of type (TOuter?, TInner?) that are obtained by performing a full outer join on two sequences. + /// or or or is . + /// + /// + /// 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 C# or For Each in Visual Basic. + /// + /// If is , the default equality comparer, , is used to hash and compare keys. + /// + public static IEnumerable<(TOuter? Outer, TInner? Inner)> FullJoin(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer = null) => + FullJoin(outer, inner, outerKeySelector, innerKeySelector, static (outer, inner) => (outer, inner), comparer); + + private static IEnumerable FullJoinIterator(IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func resultSelector, IEqualityComparer? comparer) + { + Lookup innerLookup = Lookup.CreateForJoin(inner, innerKeySelector, comparer); + + HashSet>? matchedGroupings = innerLookup.Count != 0 + ? new HashSet>() + : null; + + foreach (TOuter item in outer) + { + Grouping? g = innerLookup.GetGrouping(outerKeySelector(item), create: false); + if (g is null) + { + yield return resultSelector(item, default); + } + else + { + matchedGroupings!.Add(g); + int count = g._count; + TInner[] elements = g._elements; + for (int i = 0; i != count; ++i) + { + yield return resultSelector(item, elements[i]); + } + } + } + + // Yield inner elements that had no matching outer element. + if (matchedGroupings is null || matchedGroupings.Count < innerLookup.Count) + { + Grouping? g = innerLookup._lastGrouping; + if (g is not null) + { + do + { + g = g._next!; + if (matchedGroupings is null || !matchedGroupings.Contains(g)) + { + int count = g._count; + TInner[] elements = g._elements; + for (int i = 0; i != count; ++i) + { + yield return resultSelector(default, elements[i]); + } + } + } + while (g != innerLookup._lastGrouping); + } + } + } + } +} diff --git a/src/libraries/System.Linq/tests/FullJoinTests.cs b/src/libraries/System.Linq/tests/FullJoinTests.cs new file mode 100644 index 00000000000000..1166745e422e45 --- /dev/null +++ b/src/libraries/System.Linq/tests/FullJoinTests.cs @@ -0,0 +1,498 @@ +// 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 FullJoinTests : EnumerableTests + { + public struct CustomerRec + { + public string name; + public int custID; + } + + public struct OrderRec + { + public int orderID; + public int custID; + public int total; + } + + public struct AnagramRec + { + public string name; + public int orderID; + public int total; + } + + public struct JoinRec + { + public string name; + public int orderID; + public int total; + } + + public static JoinRec createJoinRec(CustomerRec cr, OrderRec or) + { + return new JoinRec { name = cr.name, orderID = or.orderID, total = or.total }; + } + + public static JoinRec createJoinRec(CustomerRec cr, AnagramRec or) + { + return new JoinRec { name = cr.name, orderID = or.orderID, total = or.total }; + } + + [Fact] + public void BothEmpty() + { + CustomerRec[] outer = []; + OrderRec[] inner = []; + + Assert.Empty(outer.FullJoin(inner, e => e.custID, e => e.custID, createJoinRec)); + } + + [Fact] + public void OuterEmptyInnerNonEmpty() + { + CustomerRec[] outer = []; + OrderRec[] inner = + [ + new OrderRec{ orderID = 45321, custID = 98022, total = 50 }, + new OrderRec{ orderID = 97865, custID = 32103, total = 25 } + ]; + JoinRec[] expected = + [ + new JoinRec{ name = null, orderID = 45321, total = 50 }, + new JoinRec{ name = null, orderID = 97865, total = 25 } + ]; + + Assert.Equal(expected, outer.FullJoin(inner, e => e.custID, e => e.custID, createJoinRec)); + } + + [Fact] + public void OuterNonEmptyInnerEmpty() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Tim", custID = 43434 }, + new CustomerRec{ name = "Bob", custID = 34093 } + ]; + OrderRec[] inner = []; + JoinRec[] expected = + [ + new JoinRec{ name = "Tim", orderID = 0, total = 0 }, + new JoinRec{ name = "Bob", orderID = 0, total = 0 } + ]; + + Assert.Equal(expected, outer.FullJoin(inner, e => e.custID, e => e.custID, createJoinRec)); + } + + [Fact] + public void SingleElementEachAndMatches() + { + CustomerRec[] outer = [new CustomerRec { name = "Prakash", custID = 98022 }]; + OrderRec[] inner = [new OrderRec { orderID = 45321, custID = 98022, total = 50 }]; + JoinRec[] expected = [new JoinRec { name = "Prakash", orderID = 45321, total = 50 }]; + + Assert.Equal(expected, outer.FullJoin(inner, e => e.custID, e => e.custID, createJoinRec)); + } + + [Fact] + public void SingleElementEachAndDoesntMatch() + { + CustomerRec[] outer = [new CustomerRec { name = "Prakash", custID = 98922 }]; + OrderRec[] inner = [new OrderRec { orderID = 45321, custID = 98022, total = 50 }]; + JoinRec[] expected = + [ + new JoinRec{ name = "Prakash", orderID = 0, total = 0 }, + new JoinRec{ name = null, orderID = 45321, total = 50 } + ]; + + Assert.Equal(expected, outer.FullJoin(inner, e => e.custID, e => e.custID, createJoinRec)); + } + + [Fact] + public void FirstOuterMatchesLastInnerLastOuterMatchesFirstInnerSameNumberElements() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + ]; + OrderRec[] inner = + [ + new OrderRec{ orderID = 45321, custID = 99022, total = 50 }, + new OrderRec{ orderID = 43421, custID = 29022, total = 20 }, + new OrderRec{ orderID = 95421, custID = 98022, total = 9 } + ]; + JoinRec[] expected = + [ + new JoinRec{ name = "Prakash", orderID = 95421, total = 9 }, + new JoinRec{ name = "Tim", orderID = 0, total = 0 }, + new JoinRec{ name = "Robert", orderID = 45321, total = 50 }, + new JoinRec{ name = null, orderID = 43421, total = 20 } + ]; + + Assert.Equal(expected, outer.FullJoin(inner, e => e.custID, e => e.custID, createJoinRec)); + } + + [Fact] + public void NoMatches() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Bob", custID = 99022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + ]; + OrderRec[] inner = + [ + new OrderRec{ orderID = 45321, custID = 18022, total = 50 }, + new OrderRec{ orderID = 43421, custID = 29022, total = 20 }, + new OrderRec{ orderID = 95421, custID = 39021, total = 9 } + ]; + JoinRec[] expected = + [ + new JoinRec{ name = "Prakash", orderID = 0, total = 0 }, + new JoinRec{ name = "Bob", orderID = 0, total = 0 }, + new JoinRec{ name = "Tim", orderID = 0, total = 0 }, + new JoinRec{ name = "Robert", orderID = 0, total = 0 }, + new JoinRec{ name = null, orderID = 45321, total = 50 }, + new JoinRec{ name = null, orderID = 43421, total = 20 }, + new JoinRec{ name = null, orderID = 95421, total = 9 } + ]; + + Assert.Equal(expected, outer.FullJoin(inner, e => e.custID, e => e.custID, createJoinRec)); + } + + [Fact] + public void InnerSameKeyMoreThanOneElementAndMatches() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + ]; + OrderRec[] inner = + [ + new OrderRec{ orderID = 45321, custID = 98022, total = 50 }, + new OrderRec{ orderID = 45421, custID = 98022, total = 10 }, + new OrderRec{ orderID = 43421, custID = 99022, total = 20 }, + new OrderRec{ orderID = 85421, custID = 98022, total = 18 }, + new OrderRec{ orderID = 95421, custID = 99021, total = 9 } + ]; + JoinRec[] expected = + [ + new JoinRec{ name = "Prakash", orderID = 45321, total = 50 }, + new JoinRec{ name = "Prakash", orderID = 45421, total = 10 }, + new JoinRec{ name = "Prakash", orderID = 85421, total = 18 }, + new JoinRec{ name = "Tim", orderID = 95421, total = 9 }, + new JoinRec{ name = "Robert", orderID = 43421, total = 20 } + ]; + + Assert.Equal(expected, outer.FullJoin(inner, e => e.custID, e => e.custID, createJoinRec)); + } + + [Fact] + public void OuterSameKeyMoreThanOneElementAndMatches() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Bob", custID = 99022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + ]; + OrderRec[] inner = + [ + new OrderRec{ orderID = 45321, custID = 98022, total = 50 }, + new OrderRec{ orderID = 43421, custID = 99022, total = 20 }, + new OrderRec{ orderID = 95421, custID = 99021, total = 9 } + ]; + JoinRec[] expected = + [ + new JoinRec{ name = "Prakash", orderID = 45321, total = 50 }, + new JoinRec{ name = "Bob", orderID = 43421, total = 20 }, + new JoinRec{ name = "Tim", orderID = 95421, total = 9 }, + new JoinRec{ name = "Robert", orderID = 43421, total = 20 } + ]; + + Assert.Equal(expected, outer.FullJoin(inner, e => e.custID, e => e.custID, createJoinRec)); + } + + [Fact] + public void NullComparer() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + ]; + AnagramRec[] inner = + [ + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + ]; + JoinRec[] expected = + [ + new JoinRec{ name = "Prakash", orderID = 323232, total = 9 }, + new JoinRec{ name = "Tim", orderID = 0, total = 0 }, + new JoinRec{ name = "Robert", orderID = 0, total = 0 }, + new JoinRec{ name = null, orderID = 43455, total = 10 } + ]; + + Assert.Equal(expected, outer.FullJoin(inner, e => e.name, e => e.name, createJoinRec, null)); + } + + [Fact] + public void CustomComparer() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + ]; + AnagramRec[] inner = + [ + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + ]; + JoinRec[] expected = + [ + new JoinRec{ name = "Prakash", orderID = 323232, total = 9 }, + new JoinRec{ name = "Tim", orderID = 43455, total = 10 }, + new JoinRec{ name = "Robert", orderID = 0, total = 0 } + ]; + + Assert.Equal(expected, outer.FullJoin(inner, e => e.name, e => e.name, createJoinRec, new AnagramEqualityComparer())); + } + + [Fact] + public void SelectorsReturnNull() + { + int?[] outer = [null, null]; + int?[] inner = [null, null, null]; + int?[] expected = [null, null]; + + Assert.Equal(expected, outer.FullJoin(inner, e => e, e => e, (x, y) => x)); + Assert.Equal(expected, outer.FullJoin(inner, e => e, e => e, (x, y) => y)); + } + + [Fact] + public void NullElements() + { + string[] outer = [null, string.Empty]; + string[] inner = [null, string.Empty]; + string[] expected = [null, string.Empty]; + + Assert.Equal(expected, outer.FullJoin(inner, e => e, e => e, (x, y) => y, EqualityComparer.Default)); + } + + [Fact] + public void OuterNull() + { + CustomerRec[] outer = null; + AnagramRec[] inner = + [ + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + ]; + + AssertExtensions.Throws("outer", () => outer.FullJoin(inner, e => e.name, e => e.name, createJoinRec, new AnagramEqualityComparer())); + } + + [Fact] + public void InnerNull() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + ]; + AnagramRec[] inner = null; + + AssertExtensions.Throws("inner", () => outer.FullJoin(inner, e => e.name, e => e.name, createJoinRec, new AnagramEqualityComparer())); + } + + [Fact] + public void OuterKeySelectorNull() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + ]; + AnagramRec[] inner = + [ + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + ]; + + AssertExtensions.Throws("outerKeySelector", () => outer.FullJoin(inner, null, e => e.name, createJoinRec, new AnagramEqualityComparer())); + } + + [Fact] + public void InnerKeySelectorNull() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + ]; + AnagramRec[] inner = + [ + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + ]; + + AssertExtensions.Throws("innerKeySelector", () => outer.FullJoin(inner, e => e.name, null, createJoinRec, new AnagramEqualityComparer())); + } + + [Fact] + public void ResultSelectorNull() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + ]; + AnagramRec[] inner = + [ + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + ]; + + AssertExtensions.Throws("resultSelector", () => outer.FullJoin(inner, e => e.name, e => e.name, (Func)null, new AnagramEqualityComparer())); + } + + [Fact] + public void OuterNullNoComparer() + { + CustomerRec[] outer = null; + AnagramRec[] inner = + [ + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + ]; + + AssertExtensions.Throws("outer", () => outer.FullJoin(inner, e => e.name, e => e.name, createJoinRec)); + } + + [Fact] + public void InnerNullNoComparer() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + ]; + AnagramRec[] inner = null; + + AssertExtensions.Throws("inner", () => outer.FullJoin(inner, e => e.name, e => e.name, createJoinRec)); + } + + [Fact] + public void OuterKeySelectorNullNoComparer() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + ]; + AnagramRec[] inner = + [ + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + ]; + + AssertExtensions.Throws("outerKeySelector", () => outer.FullJoin(inner, null, e => e.name, createJoinRec)); + } + + [Fact] + public void InnerKeySelectorNullNoComparer() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + ]; + AnagramRec[] inner = + [ + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + ]; + + AssertExtensions.Throws("innerKeySelector", () => outer.FullJoin(inner, e => e.name, null, createJoinRec)); + } + + [Fact] + public void ResultSelectorNullNoComparer() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + ]; + AnagramRec[] inner = + [ + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + ]; + + AssertExtensions.Throws("resultSelector", () => outer.FullJoin(inner, e => e.name, e => e.name, (Func)null)); + } + + [Fact] + public void TupleOverloadMatchesResultSelector() + { + int[] outer = [1, 2, 3]; + int[] inner = [2, 3, 4]; + + var result = outer.FullJoin(inner, o => o, i => i).ToArray(); + + Assert.Equal(4, result.Length); + Assert.Equal((1, 0), result[0]); + Assert.Equal((2, 2), result[1]); + Assert.Equal((3, 3), result[2]); + Assert.Equal((0, 4), result[3]); + } + + [Fact] + public void TupleOverloadWithComparer() + { + string[] outer = ["a", "B"]; + string[] inner = ["A", "c"]; + + var result = outer.FullJoin(inner, o => o, i => i, StringComparer.OrdinalIgnoreCase).ToArray(); + + Assert.Equal(3, result.Length); + Assert.Equal(("a", "A"), result[0]); + Assert.Equal(("B", null), result[1]); + Assert.Equal((null, "c"), result[2]); + } + + [Fact] + public void ForcedToEnumeratorDoesntEnumerate() + { + var iterator = NumberRangeGuaranteedNotCollectionType(0, 3).FullJoin(Enumerable.Empty(), i => i, i => i, (o, i) => i); + var en = iterator as IEnumerator; + Assert.False(en is not null && en.MoveNext()); + } + } +} diff --git a/src/libraries/System.Linq/tests/System.Linq.Tests.csproj b/src/libraries/System.Linq/tests/System.Linq.Tests.csproj index 207dda3b8c0050..27d95117f37f51 100644 --- a/src/libraries/System.Linq/tests/System.Linq.Tests.csproj +++ b/src/libraries/System.Linq/tests/System.Linq.Tests.csproj @@ -32,6 +32,7 @@ +