Comparing equality by list property - c#

I am trying to find the number of items in a list that differ in a property which itself is a list. I found this example using Linq here:
List<Person> distinctPeople = allPeople
.GroupBy(p => p.PersonId)
.Select(g => g.First())
.ToList();
This works nicely if the property PersonId is scalar. But in my case this does not work and in the following the items in SelectedTargets are always returned as distinct even though the ListOfActions is equal in all items:
List<Target> distinctTargets = SelectedTargets.GroupBy(p => p.ListOfActions).Select(g => g.First()).ToList();
If instead I pick the first item in ListOfActions it works:
List<Target> distinctTargets = SelectedTargets.GroupBy(p => p.ListOfActions[0]).Select(g => g.First()).ToList();
So how can I check for equality of the whole list ListOfActions? (it doesn't necessarily have to user Linq)
The definition of SelectedTargets is:
List<Target> SelectedTargets = new List<Target>();
and is DispensingActionList:
private DispensingActionList ListOfActions = new DispensingActionList();
public class DispensingActionList : List<DispensingAction>
{ ...

You could use a custom IEqualityComparer<T> for the GroupBy overload which compares sequences. For example this which uses Enumerable.SequenceEqual:
public class SequenceComparer<T> : IEqualityComparer<IEnumerable<T>>
{
public bool Equals(IEnumerable<T> x, IEnumerable<T> y)
{
if (x == null && y == null) return true;
if (x == null || y == null) return false;
var comparer = EqualityComparer<T>.Default;
return x.SequenceEqual(y, comparer);
}
public int GetHashCode(IEnumerable<T> items)
{
unchecked
{
int hash = 17;
foreach (T item in items)
{
hash = hash * 23 + (item == null ? 0 : item.GetHashCode());
}
return hash;
}
}
}
Now this should work:
List<Target> distinctTargets = SelectedTargets
.GroupBy(p => p.ListOfActions, new SequenceComparer<DispensingAction>())
.Select(g => g.First())
.ToList();
Of course DispensingAction also needs to override Equals to compare the objects meaningfully and not only checks whether they're the same reference or not.

You could use Enumerable.SequenceEqual<TSource>
You will have to override the GetHashCode and Equals methods for your types if you didn't do so yet.

Related

Order a list of Sorted Set of Strings by Count and then by strings

I have a custom class called PairString
public class PairString: IComparer<PairString>
{
public string first;
public string second;
public PairString(string f, string s)
{
first = f;
second = s;
}
public int Compare([AllowNull] PairString x, [AllowNull] PairString y)
{
if (x == null || y == null) return -1;
var f = string.Compare(x.first, y.first);
var s = string.Compare(x.second, y.second);
return f == s ? s : f;
}
}
I want to create groups by count and then by lexical order of strings in that groups, from a list of input PairString List. Below method does the grouping right. But when I try to sort the groups in lexical order for equal count groups, it throws "Atleast one object must implement IComparer error"
public static List<string> MaxItemAssociatoinGroup(List<PairString> input)
{
if (input == null || input.Count == 0) return null;
List<SortedSet<string>> output = new List<SortedSet<string>>();
foreach (var item in input)
{
if (output.Any(x => x.Contains(item.first) || x.Contains(item.second)))
{
//Take the set containing one or two or both items
var set1 = output.FirstOrDefault(x => x.Contains(item.first));
var set2 = output.FirstOrDefault(x => x.Contains(item.second));
if (set1 == null)
set2.UnionWith(new SortedSet<string> { item.first, item.second });
else if (set2 == null)
set1.UnionWith(new SortedSet<string> { item.first, item.second });
else if (set1 != set2)
{
set1.UnionWith(set2);
output.Remove(set2);
}
}
else
output.Add(new SortedSet<string>(new List<string>() { item.first, item.second }));
}
var maxlistAssociation = output.OrderByDescending(x => x.Count).First();
return new List<string>(maxlistAssociation);
}
I am not sure how to achieve lexical order for same count groups,
Sample input is
new PairString("item3","item4"),
new PairString("item3","item6"),
new PairString("item5","item6"),
new PairString("item2","item8"),
new PairString("item8","item9"),
new PairString("item1","item2")
it groups into 2 groups of equal count {item3,item4,item5,item6} & {item1,item2,item8,item9} but returns {item3,item4,item5,item6} as its first in the list. but I want the second group as it contains the item that lexicographically first than first group. what am I missing here?
It appears that you're missing a method that will compare two SortedSet<string> objects and return the one which comes first lexically. One way to do this is to compare each item from one set with the corresponding one in the other set, and return the first non-equal comparison:
public class SortedSetComparer<T> : IComparer<SortedSet<T>> where T : IComparable<T>
{
public int Compare(SortedSet<T> x, SortedSet<T> y)
{
// Null checks
if (x == null) return y == null ? 0 : 1;
if (y == null) return -1;
var minCount = Math.Min(x.Count, y.Count);
// Compare each item from one set with the corresponding one in the other set
for (var i = 0; i < minCount; i++)
{
var result = x.ElementAt(i).CompareTo(y.ElementAt(i));
// Return the first non-equal result
if (result != 0) return result;
}
// If all the items were equal, return the comparison of the Count
return x.Count.CompareTo(y.Count);
}
}
Then we can order our results (after sorting by size) by passing an instance of this class to the ThenBy method:
var maxlistAssociation = output
.OrderByDescending(x => x.Count)
.ThenBy(x => x, new SortedSetComparer<string>())
.First();
Depending on the behavior you want from this method, we could also incorporate the ordering by Count into our comparison method, so that it puts the sets with the most items first, then sorts them alphabetically:
public class SortedSetComparer<T> : IComparer<SortedSet<T>> where T : IComparable<T>
{
public int Compare(SortedSet<T> x, SortedSet<T> y)
{
// Null checks
if (x == null) return y == null ? 0 : 1;
if (y == null) return -1;
// Compare the counts first, in descending order
var countComparison = x.Count.CompareTo(y.Count);
if (countComparison != 0) return countComparison * -1;
// Then compare each item from one set lecially
// with the corresponding one in the other set
return x.Select((item, index) =>
x.ElementAt(index).CompareTo(y.ElementAt(index)))
.FirstOrDefault(result => result != 0);
}
}
And now we only need one OrderBy clause:
var maxlistAssociation = output
.OrderBy(x => x, new SortedSetComparer<string>())
.First();

How to merge two sorting functions?

I'm using the following class to sort the list by type member:
public class CameraSortByType : IComparer<Camera>
{
private bool asc;
public CameraSortByType(bool a)
{
this.asc = a;
}
public int Compare(Camera x, Camera y)
{
if (x.type > y.type)
return asc? -1 : 1;
if (x.type < y.type)
return asc? 1 : -1;
else
return 0;
}
}
I'm also sorting the same list by name:
myList.Sort((s1, s2) => s2.name.CompareTo(s1.name));
How can I incorporate sorting by name into sorting class by type? So when I sort by type it also sorts by name?
Update: Linq version
var primarySortResult = primarySort ? CameraStorage.CameraList.OrderBy(x => x.type) : CameraStorage.CameraList.OrderByDescending(x => x.type);
var secondarySortResult = secondarySort ? primarySortResult.ThenBy(x => x.name) : primarySortResult.ThenByDescending(x => x.name);
CameraStorage.CameraList = secondarySortResult.ToList();
If LINQ is an option you can use following operators to create ordered sequence:
OrderBy - sorts the elements of a sequence in ascending order
OrderByDescending - sorts in descending order
ThenBy - performs subsequent ordering of the elements in a sequence in ascending order
ThenByDescending- subsequent descending ordering
Sample:
var orderedList = myList.OrderBy(x => x.type)
.ThenByDescending(x => x.name)
.ToList();
If you need to order based on some conditions:
var result = ascendingByType ? myList.OrderBy(x => x.type) :
myList.OrderByDescending(x => x.type);
if (orderByNameRequired)
{
result = ascendingByName ? result.ThenBy(x => x.name) :
result.ThenByDescending(x => x.name);
}
orderedList = result.ToList();
Consider also using Dynamic Linq.
Also here is comparer implementation for sorting on two properties (consider also nulls handling here):
public class CameraComparer : IComparer<Camera>
{
private SortDirection typeSortDirection;
private SortDirection nameSortDirection;
public CameraComparer(SortDirection typeSortDirection,
SortDirection nameSortDirection)
{
this.typeSortDirection = typeSortDirection;
this.nameSortDirection = nameSortDirection;
}
public int Compare(Camera x, Camera y)
{
if (x.Type == y.Type)
return x.Name.CompareTo(y.Name) *
(nameSortDirection == SortDirection.Ascending ? 1 : -1);
return x.Type.CompareTo(y.Type) *
(typeSortDirection == SortDirection.Ascending ? 1 : -1);
}
}
public enum SortDirection
{
Ascending,
Descending
}
Usage:
myList.Sort(new CameraComparer(SortDirection.Ascending, SortDirection.Descending));
I use Tuples for cases like this:
public int Compare(Camera x, Camera y) {
var xx = Tuple.Create(x.name, x.type);
var yy = Tuple.Create(y.name, y.type);
return xx.CompareTo(yy);
}
That is, assuming that you want to compare all properties in the same order. If not, I think you could reverse the order in which a property (e.g. name) is considered by doing something like:
var xx = Tuple.Create(y.name, x.type);
var yy = Tuple.Create(x.name, y.type);
That is, putting it in the "wrong" tuple. But I haven't tested this part at all.
public int Compare(Camera x, Camera y)
{
if (x.type > y.type)
return asc ? -1 : 1;
if (x.type < y.type)
return asc ? 1 : -1;
if (x.name.CompareTo(y.name) > 0)
return asc ? -1 : 1;
if (x.name.CompareTo(y.name) < 0)
return asc ? 1 : -1;
return 0;
}

Remove from List<> duplicated items and record duplicated quantity

I have a List<Item>. Item has properties Id,Name and Amount. There are duplicated items in this list. I need to get a new List<Item> which contains only non-duplicated Items and in Item's Amount should be the quantity of how many times it duplicated in first List<Item>. I tried something like
for (int i = 0; i < list.Count; i++)
{
for (int j = 0; j < list.Count; j++)
{
if (list[i].Name == list[j].Name)
{
list.Remove(prod.Components[j]);
list[i].Amount++;
}
}
}
but there are some problems in this loop. My brains are overheated. Please, help.
A simple LINQ query can get you the unique items along with the number of times they appear:
var distinct = list.GroupBy(o => o.Name)
.Select(g => new { Count = g.Count(), Item = g.First() })
.ToList();
Then you can modify each item's Amount to the count of duplicates:
foreach (var row in distinct)
{
row.Item.Amount = row.Count;
}
And finally get back a List<Item> that contains no duplicates and has the correct amounts:
var uniqueItems = distinct.Select(r => r.Item).ToList();
Important: The code above assumes that "duplicate" items are indistinguishable from each other, but nothing else (e.g. it doesn't need Item to have a default constructor). Depending on the particulars it may be possible to write it in an even shorter form.
Additionally, the Amount property looks strange here. Since duplicates do not warrant summation of their amounts, what's the purpose of Item.Amount? I would assume that duplicate items with amount of 2 should result in one item with an amount of 4, but your code does not do that (and mine follows that lead).
Off the top of my head (haven't tested it):
list.GroupBy(x => x.Name)
.Select(x => new Item {
Name = x.Key,
Amount = x.Count()
})
.ToList();
You haven't specified what happens to the Ids, so I've left ignored them.
(Note this creates a new list, rather than modifying the original).
Try something like that:
var groups = from item in items
group item by item.Property
into grouped
select grouped;
var distinct = from g in groups
let item = g.First()
let amount = g.Count()
select new Item {Property = item.Property, Amount = amount};
After that distinct contains IEnumerable<Item> with their amount from original items list.
foreach(var item in list)
{
if(list.Count(e=>e.Id == item.Id && e.Name == item.Name)!=1)
{
list.Remove(item);
}
}
Assuming that you determine duplicates by the first two properties ID and Name.
You can implement an IEqualityComparer<Item> and use that for Enumerable.GroupBy:
var itemAmounts = items.GroupBy(i => i, new Item())
.Select(g => new Item {
ID = g.First().ID,
Name = g.First().Name,
Amount = g.Count()
});
Here's your Item class with a meaningful implementation of IEqualityComparer<Item>:
class Item : IEqualityComparer<Item>
{
public int ID;
public string Name;
public int Amount;
public bool Equals(Item x, Item y)
{
if (x == null || y == null) return false;
bool equals = x.ID == y.ID && x.Name == y.Name;
return equals;
}
public int GetHashCode(Item obj)
{
if (obj == null) return int.MinValue;
int hash = 19;
hash = hash + obj.ID.GetHashCode();
hash = hash + obj.Name.GetHashCode();
return hash;
}
}
You could also override Equals and GetHasdhCode from object, then you don't need a custom comparer at all in GroupBy:
var itemAmounts = items.GroupBy(i => i)
.Select(g => new Item {
ID = g.First().ID,
Name = g.First().Name,
Amount = g.Count()
});
You can use above already available methods:
public override bool Equals(object obj)
{
Item item2 = obj as Item;
if (item2 == null)
return false;
else
return Equals(this, item2);
}
public override int GetHashCode()
{
return GetHashCode(this);
}

Selecting Items with the same subcollection using LINQ

Here is the pseudo case:
class parent{
string name; //Some Property
List<int> myValues;
.......
}
........
//Initialize some parent classes
List<parent> parentList = new List<parent>();
parentList.add(parent123); //parent123.myValues == {1,2,3}
parentList.add(parent456); //parent456.myValues == {4,5,6}
parentList.add(parentMatch); //parentMatch.myValues == {1,2,3}
What I am aiming for is a query which retrieves a List of parent objects where their
myValues Lists are equivalent. In this case it would return parent123 and parentMatch.
So you can wrap the logic up and just use GroupBy if you implement an IEqualityComparer:
class IntegerListComparer : IEqualityComparer<List<int>>
{
#region IEqualityComparer<List<int>> Members
public bool Equals(List<int> x, List<int> y)
{
//bool xContainsY = y.All(i => x.Contains(i));
//bool yContainsX = x.All(i => y.Contains(i));
//return xContainsY && yContainsX;
return x.SequenceEqual(y);
}
public int GetHashCode(List<int> obj)
{
return 0;
}
#endregion
}
Call it like so:
var results = list
.GroupBy(p => p.MyValues, new IntegerListComparer())
.Where(g => g.Count() > 1)
.SelectMany(g => g);
Very silly solution:
var groups = list.GroupBy(p => string.Join(",", p.list.Select(i => i.ToString()).ToArray()))
.Where(x => x.Count() > 1).ToList();
Result:
an IEnumerable of groups containing parent objects having list with same int (in the same order).
If you need to match list of elements in any order (i.e. 1,2,3 == 3,1,2), just change p.list to p.list.OrderBy(x => x).
Plus, if you're targeting framework 4.0, you can avoid ToArray and ToString
EDIT:
added a where to filter single-occurrence groups.
Now if you have these parents:
parent A 1,2,3
parent B 1,2,3
parent C 1,2,3
parent D 4,5,6
parent E 4,5,6
parent F 7,8,9
it returns:
(A,B,C) - (D,E)
Try this:
var matches = (from p1 in parentList
from p2 in parentList
let c1 = p1.myValues
let c2 = p2.myValues
where p1 != p2 &&
c1.All(child => c2.Contains(child)) &&
c2.All(child => c1.Contains(child))
select p1).Distinct();

Use LINQ to move item to top of list

Is there a way to move an item of say id=10 as the first item in a list using LINQ?
Item A - id =5
Item B - id = 10
Item C - id =12
Item D - id =1
In this case how can I elegantly move Item C to the top of my List<T> collection?
This is the best I have right now:
var allCountries = repository.GetCountries();
var topitem = allCountries.Single(x => x.id == 592);
var finalList = new List<Country>();
finalList.Add(topitem);
finalList = finalList.Concat(allCountries.Where(x=> x.id != 592)).ToList();
What do you want to order by, other than the known top item? If you don't care, you can do this:
var query = allCountries.OrderBy(x => x.id != 592).ToList();
Basically, "false" comes before "true"...
Admittedly I don't know what this does in LINQ to SQL etc. You may need to stop it from doing the ordering in the database:
var query = allCountries.AsEnumerable()
.OrderBy(x => x.id != 592)
.ToList();
LINQ is strong in querying collections, creating projections over existing queries or generating new queries based on existing collections. It is not meant as a tool to re-order existing collections inline. For that type of operation it's best to use the type at hande.
Assuming you have a type with a similar definition as below
class Item {
public int Id { get; set; }
..
}
Then try the following
List<Item> list = GetTheList();
var index = list.FindIndex(x => x.Id == 12);
var item = list[index];
list[index] = list[0];
list[0] = item;
Linq generallyworks on Enumerables, so it doesn't now that the underlying type is a collection. So for moving the item on top of the list I would suggest using something like (if you need to preserve the order)
var idx = myList.FindIndex(x => x.id == 592);
var item = myList[idx];
myList.RemoveAt(idx);
myList.Insert(0, item);
If your function returns only an IEnumerable, you can use the ToList() method to convert it to a List first
If you don't preserve the order you can simply swap the values at position 0 and position idx
var allCountries = repository.GetCountries();
allCountries.OrderByDescending(o => o.id == 12).ThenBy(o => o.id)
This will insert the object with id=12 at the top of the list and rotate the rest down, preserving the order.
Here is an extension method you might want to use. It moves the element(s) that match the given predicate to the top, preserving order.
public static IEnumerable<T> MoveToTop(IEnumerable<T> list, Func<T, bool> func) {
return list.Where(func)
.Concat(list.Where(item => !func(item)));
}
In terms of complexity, I think it would make two passes on the collection, making it O(n), like the Insert/Remove version, but better than Jon Skeet's OrderBy suggestion.
You can "group by" in two groups with Boolean key, and then sort them
var finalList= allCountries
.GroupBy(x => x.id != 592)
.OrderBy(g => g.Key)
.SelectMany(g => g.OrderBy(x=> x.id ));
I know this a old question but I did it like this
class Program
{
static void Main(string[] args)
{
var numbers = new int[] { 5, 10, 12, 1 };
var ordered = numbers.OrderBy(num => num != 10 ? num : -1);
foreach (var num in ordered)
{
Console.WriteLine("number is {0}", num);
}
Console.ReadLine();
}
}
this prints:
number is 10
number is 1
number is 5
number is 12
public static IEnumerable<T> ServeFirst<T>(this IEnumerable<T> source,
Predicate<T> p)
{
var list = new List<T>();
foreach (var s in source)
{
if (p(s))
yield return s;
else
list.Add(s);
}
foreach (var s in list)
yield return s;
}
Its interesting the number of approaches you find when trying to solve a problem.
var service = AutogateProcessorService.GetInstance();
var allConfigs = service.GetAll();
allConfigs = allConfigs.OrderBy(c => c.ThreadDescription).ToList();
var systemQueue = allConfigs.First(c => c.AcquirerId == 0);
allConfigs.Remove(systemQueue);
allConfigs.Insert(0, systemQueue);
To also check if the item was found without Exception, something like:
var allCountries = repository.GetCountries();
var lookup = allCountries.ToLookup(x => x.id == 592);
var finalList = lookup[true].Concat(lookup[false]).ToList();
if ( lookup[true].Count() != 1 ) YouAreInTrouble();
Even easier if you have the object:
listOfObjects.Remove(object);
listOfObjects.Insert(0, object);
I wrote a static extension method to do this. Note this doesn't preserve the order, it simply swaps the item out. If you needed to preserve the order you should do a rotate not a simple swap.
/// <summary>
/// Moves the item to the front of the list if it exists, if it does not it returns false
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="collection"></param>
/// <param name="predicate"></param>
/// <returns></returns>
public static bool MoveToFrontOfListWhere<T>(this List<T> collection, Func<T, bool> predicate)
{
if (collection == null || collection.Count <= 0) return false;
int index = -1;
for (int i = 0; i < collection.Count; i++)
{
T element = collection.ElementAt(i);
if (!predicate(element)) continue;
index = i;
break;
}
if (index == -1) return false;
T item = collection[index];
collection[index] = collection[0];
collection[0] = item;
return true;
}

Categories

Resources