How to get a Lookup as Union of 2 old Lookups?

If you have the original lists that the lookups came from, it might be easier. It might also be easier if you used Dictionarys of Lists instead of lookups. However, it's still possible to merge two lookup objects into a new object. The basic idea is to retrieve the original values from the lookups, and then create a new lookup from the concatenated set of both.

var a = new[] {"apple","aardvark","barn"};
var b = new[] {"baboon", "candy", "cork"};

var al = a.ToLookup (x => x[0]);
var bl = b.ToLookup (x => x[0]);

var cl = al.Concat(bl).SelectMany(x => x).ToLookup(x => x[0]);

If you also don't know the original key selector function, you can use this variant.

var cl = al.Concat(bl)
    .SelectMany(lookup => lookup.Select(value => new { lookup.Key, value}))
    .ToLookup(x => x.Key, x => x.value);

I wrote this extension method, which takes advantage of the fact that an ILookup<TK,TV> is an IEnumerable<IGrouping<TK,TV>>

    public static ILookup<TK, TV> Union<TK, TV>(this ILookup<TK, TV> self, IEnumerable<IGrouping<TK,TV>> moreGroupings)
    {
        return self.Concat(moreGroupings)
            .SelectMany(grouping => grouping.Select(val => new KeyValuePair<TK, TV>(grouping.Key, val)))
            .ToLookup(kvp => kvp.Key, kvp => kvp.Value);
    }

Here are a few tests that demonstrate. The lookups here contain strings, keyed by their lengths.

    [TestMethod]
    public void EmptyLookups_UnionReturnsEmpty()
    {
        var a = new string[] { }.ToLookup(x => x.Length, x => x);
        var b = new string[] { }.ToLookup(x => x.Length, x => x);
        var c = a.Union(b);
        Assert.AreEqual(0, c.Count);
        c = b.Union(a);
        Assert.AreEqual(0, c.Count);
    }

    [TestMethod]
    public void OneEmptyLookup_UnionReturnsContentsOfTheOther()
    {
        var a = new string[] { }.ToLookup(x => x.Length, x => x);
        var b = new string[] { "hello", "world" }.ToLookup(x => x.Length, x => x);
        var c = a.Union(b);
        Assert.AreEqual(1, c.Count);
        Assert.AreEqual("helloworld", string.Join("", c[5].OrderBy(x=>x)));
        c = b.Union(a);
        Assert.AreEqual(1, c.Count);
        Assert.AreEqual("helloworld", string.Join("", c[5].OrderBy(x=>x)));
    }

    [TestMethod]
    public void UniqueKeys_UnionAdds()
    {
        var a = new string[] { "cat", "frog", "elephant"}.ToLookup(x => x.Length, x => x);
        var b = new string[] { "hello", "world" }.ToLookup(x => x.Length, x => x);
        var c = a.Union(b);
        Assert.AreEqual(4, c.Count);
        Assert.AreEqual("cat", string.Join("", c[3]));
        Assert.AreEqual("frog", string.Join("", c[4]));
        Assert.AreEqual("elephant", string.Join("", c[8]));
        Assert.AreEqual("helloworld", string.Join("", c[5].OrderBy(x=>x)));
    }

    [TestMethod]
    public void OverlappingKeys_UnionMerges()
    {
        var a = new string[] { "cat", "frog", "horse", "elephant"}.ToLookup(x => x.Length, x => x);
        var b = new string[] { "hello", "world" }.ToLookup(x => x.Length, x => x);
        var c = a.Union(b);
        Assert.AreEqual(4, c.Count);
        Assert.AreEqual("cat", string.Join("", c[3]));
        Assert.AreEqual("frog", string.Join("", c[4]));
        Assert.AreEqual("elephant", string.Join("", c[8]));
        Assert.AreEqual("hellohorseworld", string.Join("", c[5].OrderBy(x=>x)));
    }

I also happen to need to handle case-insensitive strings, so I have this overload that takes a custom comparer.

        public static ILookup<TK, TV> Union<TK, TV>(this ILookup<TK, TV> self, IEnumerable<IGrouping<TK,TV>> moreGroupings, IEqualityComparer<TK> comparer)
    {
        return self.Concat(moreGroupings)
            .SelectMany(grouping => grouping.Select(val => new KeyValuePair<TK, TV>(grouping.Key, val)))
            .ToLookup(kvp => kvp.Key, kvp => kvp.Value, comparer);
    }

The lookups in these examples use the first letter as a key:

    [TestMethod]
    public void OverlappingKeys_CaseInsensitiveUnionAdds()
    {
        var a = new string[] { "cat", "frog", "HORSE", "elephant"}.ToLookup(x => x.Substring(0,1), x => x);
        var b = new string[] { "hello", "world" }.ToLookup(x => x.Substring(0,1), x => x);
        var c = a.Union(b, StringComparer.InvariantCulture);
        Assert.AreEqual(6, c.Count);
        Assert.AreEqual("cat", string.Join("", c["c"]));
        Assert.AreEqual("frog", string.Join("", c["f"]));
        Assert.AreEqual("elephant", string.Join("", c["e"]));
        Assert.AreEqual("hello", string.Join("", c["h"].OrderBy(x=>x)));
        Assert.AreEqual("HORSE", string.Join("", c["H"].OrderBy(x=>x)));
        Assert.AreEqual("world", string.Join("", c["w"]));
    }
    
    [TestMethod]
    public void OverlappingKeys_CaseSensitiveUnionMerges()
    {
        var a = new string[] { "cat", "frog", "HORSE", "elephant"}.ToLookup(x => x.Substring(0,1), x => x);
        var b = new string[] { "hello", "world" }.ToLookup(x => x.Substring(0,1), x => x);
        var c = a.Union(b, StringComparer.InvariantCultureIgnoreCase);
        Assert.AreEqual(5, c.Count);
        Assert.AreEqual("cat", string.Join("", c["c"]));
        Assert.AreEqual("frog", string.Join("", c["f"]));
        Assert.AreEqual("elephant", string.Join("", c["e"]));
        Assert.AreEqual("helloHORSE", string.Join("", c["h"].OrderBy(x=>x)));
        Assert.AreEqual("helloHORSE", string.Join("", c["H"].OrderBy(x=>x)));
        Assert.AreEqual("world", string.Join("", c["w"]));
    }

Tags:

C#

.Net

Linq