using System;
using System.Collections.Generic;
using System.Linq;
static class Program
{
static void Main(string[] args)
{
var ax = new[] {
new { id = 1, name = "John" },
new { id = 2, name = "Sue" } };
var bx = new[] {
new { id = 1, surname = "Doe" },
new { id = 3, surname = "Smith" } };
ax.FullOuterJoin(
bx, a => a.id, b => b.id,
(a, b, id) => new { a.name, b.surname },
new { id = -1, name = "(no firstname)" },
new { id = -2, surname = "(no surname)" }
)
.ToList().ForEach(Console.WriteLine);
}
}
internal static class MyExtensions
{
internal static IList<TR> FullOuterGroupJoin<TA, TB, TK, TR>(
this IEnumerable<TA> a,
IEnumerable<TB> b,
Func<TA, TK> selectKeyA,
Func<TB, TK> selectKeyB,
Func<IEnumerable<TA>, IEnumerable<TB>, TK, TR> projection,
IEqualityComparer<TK> cmp = null)
{
cmp = cmp?? EqualityComparer<TK>.Default;
var alookup = a.ToLookup(selectKeyA, cmp);
var blookup = b.ToLookup(selectKeyB, cmp);
var keys = new HashSet<TK>(alookup.Select(p => p.Key), cmp);
keys.UnionWith(blookup.Select(p => p.Key));
var join = from key in keys
let xa = alookup[key]
let xb = blookup[key]
select projection(xa, xb, key);
return join.ToList();
}
internal static IList<TR> FullOuterJoin<TA, TB, TK, TR>(
this IEnumerable<TA> a,
IEnumerable<TB> b,
Func<TA, TK> selectKeyA,
Func<TB, TK> selectKeyB,
Func<TA, TB, TK, TR> projection,
TA defaultA = default(TA),
TB defaultB = default(TB),
IEqualityComparer<TK> cmp = null)
{
cmp = cmp?? EqualityComparer<TK>.Default;
var alookup = a.ToLookup(selectKeyA, cmp);
var blookup = b.ToLookup(selectKeyB, cmp);
var keys = new HashSet<TK>(alookup.Select(p => p.Key), cmp);
keys.UnionWith(blookup.Select(p => p.Key));
var join = from key in keys
from xa in alookup[key].DefaultIfEmpty(defaultA)
from xb in blookup[key].DefaultIfEmpty(defaultB)
select projection(xa, xb, key);
return join.ToList();
}
}
dXNpbmcgU3lzdGVtOwp1c2luZyBTeXN0ZW0uQ29sbGVjdGlvbnMuR2VuZXJpYzsKdXNpbmcgU3lzdGVtLkxpbnE7CgpzdGF0aWMgY2xhc3MgUHJvZ3JhbQp7CiAgICBzdGF0aWMgdm9pZCBNYWluKHN0cmluZ1tdIGFyZ3MpCiAgICB7CiAgICAgICAgdmFyIGF4ID0gbmV3W10geyAKICAgICAgICAgICAgbmV3IHsgaWQgPSAxLCBuYW1lID0gIkpvaG4iIH0sCiAgICAgICAgICAgIG5ldyB7IGlkID0gMiwgbmFtZSA9ICJTdWUiIH0gfTsKICAgICAgICB2YXIgYnggPSBuZXdbXSB7IAogICAgICAgICAgICBuZXcgeyBpZCA9IDEsIHN1cm5hbWUgPSAiRG9lIiB9LAogICAgICAgICAgICBuZXcgeyBpZCA9IDMsIHN1cm5hbWUgPSAiU21pdGgiIH0gfTsKCiAgICAgICAgYXguRnVsbE91dGVySm9pbigKICAgICAgICAgICAgICAgIGJ4LCBhID0+IGEuaWQsIGIgPT4gYi5pZCwKICAgICAgICAgICAgICAgIChhLCBiLCBpZCkgPT4gbmV3IHsgYS5uYW1lLCBiLnN1cm5hbWUgfSwKICAgICAgICAgICAgICAgIG5ldyB7IGlkID0gLTEsIG5hbWUgPSAiKG5vIGZpcnN0bmFtZSkiIH0sCiAgICAgICAgICAgICAgICBuZXcgeyBpZCA9IC0yLCBzdXJuYW1lID0gIihubyBzdXJuYW1lKSIgfQogICAgICAgICAgICApCiAgICAgICAgICAgIC5Ub0xpc3QoKS5Gb3JFYWNoKENvbnNvbGUuV3JpdGVMaW5lKTsKICAgIH0KfQoKaW50ZXJuYWwgc3RhdGljIGNsYXNzIE15RXh0ZW5zaW9ucwp7CiAgICBpbnRlcm5hbCBzdGF0aWMgSUxpc3Q8VFI+IEZ1bGxPdXRlckdyb3VwSm9pbjxUQSwgVEIsIFRLLCBUUj4oCiAgICAgICAgdGhpcyBJRW51bWVyYWJsZTxUQT4gYSwKICAgICAgICBJRW51bWVyYWJsZTxUQj4gYiwKICAgICAgICBGdW5jPFRBLCBUSz4gc2VsZWN0S2V5QSwgCiAgICAgICAgRnVuYzxUQiwgVEs+IHNlbGVjdEtleUIsCiAgICAgICAgRnVuYzxJRW51bWVyYWJsZTxUQT4sIElFbnVtZXJhYmxlPFRCPiwgVEssIFRSPiBwcm9qZWN0aW9uLAogICAgICAgIElFcXVhbGl0eUNvbXBhcmVyPFRLPiBjbXAgPSBudWxsKQogICAgewogICAgICAgIGNtcCA9IGNtcD8/IEVxdWFsaXR5Q29tcGFyZXI8VEs+LkRlZmF1bHQ7CiAgICAgICAgdmFyIGFsb29rdXAgPSBhLlRvTG9va3VwKHNlbGVjdEtleUEsIGNtcCk7CiAgICAgICAgdmFyIGJsb29rdXAgPSBiLlRvTG9va3VwKHNlbGVjdEtleUIsIGNtcCk7CgogICAgICAgIHZhciBrZXlzID0gbmV3IEhhc2hTZXQ8VEs+KGFsb29rdXAuU2VsZWN0KHAgPT4gcC5LZXkpLCBjbXApOwogICAgICAgIGtleXMuVW5pb25XaXRoKGJsb29rdXAuU2VsZWN0KHAgPT4gcC5LZXkpKTsKCiAgICAgICAgdmFyIGpvaW4gPSBmcm9tIGtleSBpbiBrZXlzCiAgICAgICAgICAgICAgICAgICBsZXQgeGEgPSBhbG9va3VwW2tleV0KICAgICAgICAgICAgICAgICAgIGxldCB4YiA9IGJsb29rdXBba2V5XQogICAgICAgICAgICAgICAgICAgc2VsZWN0IHByb2plY3Rpb24oeGEsIHhiLCBrZXkpOwoKICAgICAgICByZXR1cm4gam9pbi5Ub0xpc3QoKTsKICAgIH0KCiAgICBpbnRlcm5hbCBzdGF0aWMgSUxpc3Q8VFI+IEZ1bGxPdXRlckpvaW48VEEsIFRCLCBUSywgVFI+KAogICAgICAgIHRoaXMgSUVudW1lcmFibGU8VEE+IGEsCiAgICAgICAgSUVudW1lcmFibGU8VEI+IGIsCiAgICAgICAgRnVuYzxUQSwgVEs+IHNlbGVjdEtleUEsIAogICAgICAgIEZ1bmM8VEIsIFRLPiBzZWxlY3RLZXlCLAogICAgICAgIEZ1bmM8VEEsIFRCLCBUSywgVFI+IHByb2plY3Rpb24sCiAgICAgICAgVEEgZGVmYXVsdEEgPSBkZWZhdWx0KFRBKSwgCiAgICAgICAgVEIgZGVmYXVsdEIgPSBkZWZhdWx0KFRCKSwKICAgICAgICBJRXF1YWxpdHlDb21wYXJlcjxUSz4gY21wID0gbnVsbCkKICAgIHsKICAgICAgICBjbXAgPSBjbXA/PyBFcXVhbGl0eUNvbXBhcmVyPFRLPi5EZWZhdWx0OwogICAgICAgIHZhciBhbG9va3VwID0gYS5Ub0xvb2t1cChzZWxlY3RLZXlBLCBjbXApOwogICAgICAgIHZhciBibG9va3VwID0gYi5Ub0xvb2t1cChzZWxlY3RLZXlCLCBjbXApOwoKICAgICAgICB2YXIga2V5cyA9IG5ldyBIYXNoU2V0PFRLPihhbG9va3VwLlNlbGVjdChwID0+IHAuS2V5KSwgY21wKTsKICAgICAgICBrZXlzLlVuaW9uV2l0aChibG9va3VwLlNlbGVjdChwID0+IHAuS2V5KSk7CgogICAgICAgIHZhciBqb2luID0gZnJvbSBrZXkgaW4ga2V5cwogICAgICAgICAgICAgICAgICAgZnJvbSB4YSBpbiBhbG9va3VwW2tleV0uRGVmYXVsdElmRW1wdHkoZGVmYXVsdEEpCiAgICAgICAgICAgICAgICAgICBmcm9tIHhiIGluIGJsb29rdXBba2V5XS5EZWZhdWx0SWZFbXB0eShkZWZhdWx0QikKICAgICAgICAgICAgICAgICAgIHNlbGVjdCBwcm9qZWN0aW9uKHhhLCB4Yiwga2V5KTsKCiAgICAgICAgcmV0dXJuIGpvaW4uVG9MaXN0KCk7CiAgICB9Cn0K