Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Difference between two lists preserving duplicates

Tags:

c#

linq

I have two lists:

var list1 = new List<string> { "A", "A", "B", C" };
var list2 = new List<string> { "A", "B" };

and I would like to produce a list like

var result = new[] { "A", "C" };

Where the list is all the elements from list1 removed from list2 I don't think there is a Linq extension method for this since Except removes duplicates.

The non-linq way to do this would be:

var tempList = list1.ToList();
foreach(var item in list2)
{
    tempList.Remove(item);
}

but I am wondering if there is a Linq extension method that I might have missed.

Edit:

Since there probably aren't any here's an extension method I made.

public static class LinqExtensions
{
    public static IEnumerable<T> RemoveRange<T>(this IEnumerable<T> source, IEnumerable<T> second)
    {
        var tempList = source.ToList();

        foreach(var item in second)
        {
            tempList.Remove(item);
        }

        return tempList;
    }

    public static IEnumerable<TFirst> RemoveMany<TFirst, TSecond>(this IEnumerable<TFirst> source, IEnumerable<TSecond> second, Func<TSecond, IEnumerable<TFirst>> selector)
    {
        var tempList = source.ToList();

        foreach(var item in second.SelectMany(selector))
        {
            tempList.Remove(item);
        }

        return tempList;
    }
}

Usage:

list1.RemoveRange(list2)
like image 781
Dustin Kingen Avatar asked Apr 04 '13 02:04

Dustin Kingen


3 Answers

Looking at your example, I think you mean "all the elements from list2 removed from list1":

var lookup2 = list2.ToLookup(str => str);

var result = from str in list1
             group str by str into strGroup
             let missingCount 
                  = Math.Max(0, strGroup.Count() - lookup2[strGroup.Key].Count())
             from missingStr in strGroup.Take(missingCount)
             select missingStr;
like image 149
Ani Avatar answered Oct 17 '22 16:10

Ani


Not LINQ, but a one-line anyway:

list2.ForEach(l => list1.Remove(l));

btw... it would be nice if List<int> had something like AddRange but to remove a bunch of items at the same time.

like image 31
Julián Urbano Avatar answered Oct 17 '22 15:10

Julián Urbano


If you do not care about the order in which the elements of the result come, you can do it with LINQ's GroupBy:

var a = new List<string>{"A","A", "B", "C"};
var b = new List<string>{"A", "B"};
var res  =  a.Select(e => new {Key=e, Val=1})
    .Concat(b.Select(e => new {Key=e, Val=-1}))
    .GroupBy(e => e.Key, e => e.Val)
    .SelectMany(g => Enumerable.Repeat(g.Key, Math.Max(0, g.Sum())))
    .ToList();

Here is a demo on ideone.

I must admit that your solution is far simpler than mine, so it should be regarded as a mere curiosity, a way to prove that this could be done with LINQ too.

Here is how it works: for each element from the first list we add a key-value pair with a 1; for each element from the second list we add a key-value pair with -1. Then we group all elements by their key, total up their ones and negative ones, and produce as many keys as the total, making sure that we do not select anything when the result is negative.

like image 20
Sergey Kalinichenko Avatar answered Oct 17 '22 17:10

Sergey Kalinichenko