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
}
a source to share
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.
a source to share
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.
a source to share