Groupby on Complex Object (E.G. List<T>)

GroupBy on complex object (e.g. ListT)

To get objects to work with many of LINQ's operators, such as GroupBy or Distinct, you must either implement GetHashCode & Equals, or you must provide a custom comparer.

In your case, with a property as a list you probably need a comparer, unless you made the list read only.

Try this comparer:

public class SampleObjectComparer : IEqualityComparer<SampleObject>
{
public bool Equals(SampleObject x, SampleObject y)
{
return x.Id == y.Id && x.Events.SequenceEqual(y.Events);
}

public int GetHashCode(SampleObject x)
{
return x.Id.GetHashCode() ^ x.Events.Aggregate(0, (a, y) => a ^ y.GetHashCode());
}
}

Now this code works:

    var items = new List<SampleObject>()
{
new SampleObject() { Id = "Id", Events = new List<string>() { "ExampleEvent"} },
new SampleObject() { Id = "Id", Events = new List<string>() { "ExampleEvent" } }
};

var comparer = new SampleObjectComparer();

var duplicates = items.GroupBy(x => x, comparer)
.Where(g => g.Count() > 1)
.Select(g => g.Key)
.ToList();

Linq Group by Complex Object (E.G list element of object)

1 ) I've created my classes to meet json,

public class MyTable {
public string TableNumber { get; set; }
public string TableId { get; set; }
public List<TableMenu> tableMenuItems { get; set; }
public int TotalPrice { get; set; }
}

public class TableMenu {
public string Id { get; set; }
public string Name { get; set; }
public List<TableMenuItemCondiment> tableMenuItemCondiment { get; set; }
public int Price { get; set; }
public int Count { get; set; }
}

public class TableMenuItemCondiment {
public string Id { get; set; }
public string Name { get; set; }
public int Price { get; set; }
}

2 ) Then i've deserialized object by NewtonSoft,

MyTable myTable = JsonConvert.DeserializeObject<MyTable>(json);

3 ) Converted object as,

MyTable result = new MyTable() {
TableNumber = myTable.TableNumber,
TableId = myTable.TableId,
tableMenuItems = myTable.tableMenuItems.GroupBy(x => x.Id).Select(x => new TableMenu() {
Id = x.Key,
Name = x.First()?.Name,
tableMenuItemCondiment = x.SelectMany(a=>a.tableMenuItemCondiment).GroupBy(z => z.Name).Select(z => new TableMenuItemCondiment() {
Id = z.First()?.Id,
Name = z.Key,
Price = z.Sum(t => t.Price)
}).ToList(),
Price = x.Sum(t => t.Price),
Count = x.Sum(t => t.Count)
}).ToList(),
TotalPrice = myTable.TotalPrice
};

4 ) Finally serialized object to see whether it gives the response as you want,

string output = JsonConvert.SerializeObject(result);

5 ) Print the output,

Console.WriteLine(output);
Console.ReadKey();

Output:

{  "TableNumber":"3", 
"TableId":"81872d39-9480-4d2d-abfc-b8e4f33a43b6",
"tableMenuItems":[
{
"Id":"4664a2d3-c0af-443d-8af5-2bd21e71838b",
"Name":"Bonfile",
"tableMenuItemCondiment":[
{
"Id":"9b1f01a0-0313-46b6-b7f1-003c0e846136",
"Name":"Kekikkli",
"Price":0
},
{
"Id":"38a9cce6-f20c-4f78-b6c9-c15e79ecc8f1",
"Name":"mayonez",
"Price":0
},
{
"Id":"cb3a7811-668b-4e45-bce3-e6b2b13af9e1",
"Name":"Rare",
"Price":0
}
],
"Price":90,
"Count":2
}
],
"TotalPrice":90 }

How to .GroupBy() by Id and by list property?

A typical way to compare two lists is to use the System.Linq exension method, SequenceEquals. This method returns true if both lists contain the same items, in the same order.

In order to make this work with an IEnumerable<EvaluatedTag>, we need to have a way to compare instances of the EvaluatedTag class for equality (determining if two items are the same) and for sorting (since the lists need to have their items in the same order).

To do this, we can override Equals and GetHashCode and implement IComparable<EvaluatedTag> (and might as well do IEquatable<EvaluatedTag> for completeness):

public class EvaluatedTag : IEquatable<EvaluatedTag>, IComparable<EvaluatedTag>
{
public string Id { get; set; }
public string Name { get; set; }

public int CompareTo(EvaluatedTag other)
{
if (other == null) return -1;
var result = string.CompareOrdinal(Id, other.Id);
return result == 0 ? string.CompareOrdinal(Name, other.Name) : result;
}

public bool Equals(EvaluatedTag other)
{
return other != null &&
string.Equals(other.Id, Id) &&
string.Equals(other.Name, Name);
}

public override bool Equals(object obj)
{
return Equals(obj as EvaluatedTag);
}

public override int GetHashCode()
{
return Id.GetHashCode() * 17 +
Name.GetHashCode() * 17;
}
}

Now we can use this in the custom comparer you have in your question, for sorting and comparing the EvaluatedTags:

public class AlertEvaluationComparer : IEqualityComparer<AlertEvaluation>
{
// Return true if the AlertIds are equal, and the EvaluatedTags
// contain the same items (call OrderBy to ensure they're in
// the same order before calling SequenceEqual).
public bool Equals(AlertEvaluation x, AlertEvaluation y)
{
if (x == null) return y == null;
if (y == null) return false;
if (!string.Equals(x.AlertId, y.AlertId)) return false;
if (x.EvaluatedTags == null) return y.EvaluatedTags == null;
if (y.EvaluatedTags == null) return false;
return x.EvaluatedTags.OrderBy(et => et)
.SequenceEqual(y.EvaluatedTags.OrderBy(et => et));
}

// Use the same properties in GetHashCode that were used in Equals
public int GetHashCode(AlertEvaluation obj)
{
return obj.AlertId?.GetHashCode() ?? 0 * 17 +
obj.EvaluatedTags?.Sum(et => et.GetHashCode() * 17) ?? 0;
}
}

And finally we can pass your AlertEvaluationComparer to the GroupBy method to group our items:

var evaluationsGroupedAndOrdered = evaluations
.GroupBy(ae => ae, new AlertEvaluationComparer())
.OrderBy(group => group.Key.EvaluationDate)
.ToList();

Linq Group by Complex Type of List

You could create two IEqualityComparer<T> classes here. One for comparing if two CondimentDto objects are equal and one for comparing if two List<CondimentDto> are equal. We pass the CondimentComparer to SequenceEqual() to determine if two lists are equal, and we pass ItemDtoComparer to GroupBy to group ItemDto objects from the main items list. We have to also create our own Equals() and GetHashCode() to determine the equality.

public class CondimentComparer : IEqualityComparer<CondimentDto>
{
public bool Equals(CondimentDto x, CondimentDto y)
{
// If all the properties match, then its equal
return x.PosItemId == y.PosItemId &&
x.ProductName == y.ProductName &&
x.OriginalPrice == y.OriginalPrice;
}

// Get all hashcodes from object properties using XOR
// Might be better ways to do this
public int GetHashCode(CondimentDto obj)
{
return obj.PosItemId.GetHashCode() ^
obj.ProductName.GetHashCode() ^
obj.OriginalPrice.GetHashCode();
}
}

public class ItemDtoComparer : IEqualityComparer<ItemDto>
{
public bool Equals(ItemDto x, ItemDto y)
{

// If two condiments are null, then just compare ItemId
if (x.Condiments == null && y.Condiments == null)
{
return x.ItemId == y.ItemId;
}

// If one is null and the other is not, can't possiblly be equal
else if (x.Condiments == null && y.Condiments != null ||
x.Condiments != null && y.Condiments == null)
{
return false;
}

// If we get here we have two normal objects
// Compare two objects ItemId and lists of condiments
return x.ItemId == y.ItemId &&
x.Condiments.SequenceEqual(y.Condiments, new CondimentComparer());
}

public int GetHashCode(ItemDto obj)
{

// If condiments list is null, just get the ItemId hashcode
if (obj.Condiments == null)
{
return obj.ItemId.GetHashCode();
}

// Otherwise do the XOR of ItemId and the aggregated XOR of the condiment list properties
return obj.ItemId.GetHashCode() ^ obj.Condiments.Aggregate(0, (a, c) => a ^ c.PosItemId.GetHashCode() ^ c.ProductName.GetHashCode() ^ c.OriginalPrice.GetHashCode());
}
}

Then you can pass ItemDtoComparer to Groupby:

var result = items
.GroupBy(item => item, new ItemDtoComparer()) // Pass ItemDtoComparer here
.Select(grp => new ItemDto
{
ItemId = grp.Key.ItemId,
ProductName = grp.Key.ProductName,
Condiments = grp.Key.Condiments,
Count = grp.Count()
})
.ToList();

Demo on dotnetfiddle.net

Note: The above GetHashCode() implementations are XORing hashcodes. This is considered to not be best practice, and following the suggestions in this answer offer better alternatives to avoiding hash collisions. I just chose this method as its easy to remember and demo.

GroupBy() not grouping correctly on ListT property

When you include a list property in GroupBy you end up with list comparisons deep inside GroupBy implementation, which is usually not what you want.

You could solve this by implementing a custom IEqualityComparer, but then you would have to define a named type for the group key, because IEqualityComparer cannot be defined for an anonymous type. Another approach is to group by a composite string key, like this:

.GroupBy(sd => new {
sd.Shift.Id
, string.Join(
"|"
, sd.DayFunctions
.OrderBy(f => f.ScheduleDayId).ThenBy(f => f.FunctionId)
.Select(f => $"{f.ScheduleDayId}:{f.FunctionId}")
)
})

Using LINQ GroupBy to group by reference objects instead of value objects

To accomplish this task, you can override Equals() and GetHashCode() methods for your classes:

public class Room {
public string Name;
public string Foo;

public override bool Equals(object obj)
{
Room other = obj as Room;
if (other == null) return false;
return this.Name == other.Name && this.Foo == other.Foo;
}

public override int GetHashCode()
{
return (Name.GetHashCode() ^ Foo.GetHashCode()).GetHashCode();
}
}

Take a look here for more complicated example

LINQ Group By into a Dictionary Object

Dictionary<string, List<CustomObject>> myDictionary = ListOfCustomObjects
.GroupBy(o => o.PropertyName)
.ToDictionary(g => g.Key, g => g.ToList());

GroupBy on multiple properties with complex type

Enumerable.GroupBy allows you to pass custom equality comparer for keys. But in your case key is not an Address object - it's an anonymous object containing three properties - SiteRefNum, SiteRefName and Address. Of course passing AddressComparer to compare such keys will cause an error.

And your first problem was using complex object as key property. If you don't override Equals and GetHashCode methods for Address objects, then all addresses will be compared by reference. Which is of course different for each address instance. You can provide Equals and GetHashCode implementations to compare addresses.

Or you can modify your query to use address string for grouping:

var aggregated = 
from s in sitesWithLive
group s by new {
s.SiteRefNum,
s.SiteRefName,
Address = s.Address.ToString() // here we group by string
} into g
select new Site
{
SiteRefNum = g.Key.SiteRefNum,
SiteRefName = g.Key.SiteRefName,
Address = g.First().Address, // here we just get first address object
ContractLive = g.Max(x => x.ContractLive)
};

You can use method syntax for query, but I find declarative query syntax more readable :)

How do I use itertools.groupby()?

IMPORTANT NOTE: You have to sort your data first.


The part I didn't get is that in the example construction

groups = []
uniquekeys = []
for k, g in groupby(data, keyfunc):
groups.append(list(g)) # Store group iterator as a list
uniquekeys.append(k)

k is the current grouping key, and g is an iterator that you can use to iterate over the group defined by that grouping key. In other words, the groupby iterator itself returns iterators.

Here's an example of that, using clearer variable names:

from itertools import groupby

things = [("animal", "bear"), ("animal", "duck"), ("plant", "cactus"), ("vehicle", "speed boat"), ("vehicle", "school bus")]

for key, group in groupby(things, lambda x: x[0]):
for thing in group:
print("A %s is a %s." % (thing[1], key))
print("")

This will give you the output:

A bear is a animal.

A duck is a animal.

A cactus is a plant.

A speed boat is a vehicle.

A school bus is a vehicle.

In this example, things is a list of tuples where the first item in each tuple is the group the second item belongs to.

The groupby() function takes two arguments: (1) the data to group and (2) the function to group it with.

Here, lambda x: x[0] tells groupby() to use the first item in each tuple as the grouping key.

In the above for statement, groupby returns three (key, group iterator) pairs - once for each unique key. You can use the returned iterator to iterate over each individual item in that group.

Here's a slightly different example with the same data, using a list comprehension:

for key, group in groupby(things, lambda x: x[0]):
listOfThings = " and ".join([thing[1] for thing in group])
print(key + "s: " + listOfThings + ".")

This will give you the output:

animals: bear and duck.

plants: cactus.

vehicles: speed boat and school bus.



Related Topics



Leave a reply



Submit