Equality comparison with multiple instances / IEqualityComparer problems in LINQ

This is similar to my last question; but from a different angle. See if element exists once in Enumerable (Linq)

Given the following set of items and lists containing them ...

Item 1
Item 2
Item 3
Item 4
Item 5

class Item
{
 string Name { get; set; }
}

List<Item> available = new List<Item>()
{
 Item 1
 Item 1
 Item 2
 Item 3
 Item 5
}

List<Item> selected = new List<Item>()
{
 Item 1
 Item 2
 Item 3
}

      

I need to make a third list that has everything from "available" except what's in "selected". However, "Item 1" is in "available" twice, but only in "selected" once. Since they are instances of the same element, I am having trouble defining the appropriate logic to accommodate this.

The final array should look like ...

List<Item> selectable = new List<Item>()
{
 Item 1
 Item5
}

      

+2


a source to share


3 answers


There can be a LINQ method to accomplish this task. You could get 5 items as they are unique, but the second item 1 can be tricky. However, you can always do it the old fashioned way and create a new list yourself. Consider this example:

class Item
{
    public Item(string name) { Name = name; }
    public string Name { get; set; }
}

      

...



List<Item> available = new List<Item>()
{
    new Item("1"), new Item("1"), new Item("2"), new Item("3"), new Item("5")
};

List<Item> selected = new List<Item>()
{
    new Item("1"),new Item("2"), new Item("3")
};

List<Item> stillAvailable = new List<Item>();
List<Item> stillSelected = new List<Item>(selected);

foreach (Item item in available)
{
    Item temp = stillSelected.Find(i => i.Name == item.Name);
    if (temp == null)
        stillAvailable.Add(item);
    else 
        stillSelected.Remove(temp);
}

      

You create a list of available items, which is initially empty. You create a list for selected items that contains all of the selected items. Then you just loop through the available items and search the stillSelected list. If the item is found, you remove it from the stillSelected list. If not, add it to the stillAvailable list. At the end of the loop, stillAvailable will contain one item 1 and item 5.

+1


a source


It's kind of a tricky approach, but it gets the job done. I borrowed from the Decorate-Sort-Undecorate idiom in Python, which sorts by associating a temporary sort key with an array, combined with the fun and useful fact that anonymous types in .NET have a standard EqualityComparer that compares based on the value of their fields.

Step 1. Group the items in each list by name, and then associate an index with each item in each group and align the groups back to a regular list:

var indexedAvailable = available.GroupBy(i => i.Name)
                                .SelectMany(g => g.Select((itm, idx) => new 
                                              { Name = itm.Name, Index = idx }));
var indexedSelected = selected.GroupBy(i => i.Name)
                              .SelectMany(g => g.Select((itm, idx) => new
                                              { Name = itm.Name, Index = idx }));

      

This will turn the lists into these:

indexedAvailable            indexedSelected
Name = Item 1, Index = 0    Name = Item 1, Index = 0
Name = Item 1, Index = 1    Name = Item 2, Index = 0
Name = Item 2, Index = 0    Name = Item 3, Index = 0
Name = Item 3, Index = 0
Name = Item 5, Index = 0

      



Now you can see that in indexed lists, each numbered occurrence of any name in available

will only match the same occurrence of the same name in selected

. So you can use simple Except

to delete anything in indexedAvailable

that is not in indexedSelected

, and then "undecorate" by rotating the anonymous printed objects back to Item

s.

var selectable = indexedAvailable.Except(indexedSelected)
                                 .Select(i => new Item() { Name = i.Name });

      

And proof:

foreach (var item in selectable)
    Console.WriteLine(item.Name);
//prints out:
//Item 1
//Item 5

      

Note that this will work even if it selected

contains names that are not in available

eg. if the second list has Item 4

like in your last question.

+2


a source


var comp = new MyEqualityComparer();
selectable = available.Distinct(comp).Except(selected, comp);

      

+1


a source







All Articles