How to incorporate List<string> field in GroupBy()

Viewed 307

I have a list of Diff objects Diff looks like this

public class Diff
{
    ChangeAction Action // ChangeAction is an Enum
    string Type
    DateTime StartDateTime
    DateTime EndDateTime
    int rateId
    string rateDescription
    List<string> properties
    DayOfWeek day
    List<DaysOfWeek> DaysOfWeek
    DayOfWeek DayOfWeek

}

My LINQ query does not do what I think it will do. I am passing in diff.properties in the GroupBy(), which is a List and I want it to group when all string values in a list match

var results = diffs
    .GroupBy(diff => new { diff.properties, diff.Action, diff.Type, diff.StartDateTime, 
                           diff.EndDateTime, diff.rateId, diff.rateDescription}) 
    .Select(group => new Diff(
        group.Key.Action,
        group.Key.ScheduleType,
        group.Key.StartDateTime,
        group.Key.EndDateTime,
        group.Key.rateId,
        group.Key.rateDescription,
        group.Key.properties,
        group
            .Select(ts => ts.DayOfWeek)
            .Distinct()
            .OrderBy(dow => dow)
            .ToList()))
    .ToList();

The only difference between results and diffs is that the singular DayOfWeek that was previously stored in diffs is now put into the plural DaysOfWeek field (but just 1 item in the list). Currently, same # of items in both results and diffs.

What I'd like to see in the results list are:

  1. A shorter list that consolidates based on matching on all the grouping (including diff.properties list values)
  2. This would also mean more than 1 item in the DaysOfWeek list.

My question is:

How can I change my LINQ query above to see what I want to see in results?

3 Answers

The grouping you are using with the anonymous type contains a List<string> which is causing you to get a 1-1 ungrouped set.

You need to either

- OR -

  • compose a string from the values, or a subset of them, being selected (diff.properties, diff.Action, diff.Type, diff.startdatetime, diff.enddatetime, diff.rateId, diff.rateDescription) that will serve as a unique key to group with

A few of your GroupBy properties are reference types, and the default comparer for these types is a refernence comparison, so none of these will ever match. To overcome this, we can write our own EqualityComparer for the Diff class so we can compare them in our own way:

public class DiffEqualityComparer : IEqualityComparer<Diff>
{
    public bool Equals(Diff first, Diff second)
    {
        if (first == null || second == null) return ReferenceEquals(first, second);

        if (first.Properties == null && second.Properties != null) return false;
        if (first.Properties != null && second.Properties == null) return false;
        if (first.Properties != null && second.Properties != null &&
            !first.Properties.OrderBy(p => p)
                .SequenceEqual(second.Properties.OrderBy(p => p)))
            return false;
        if (!first.Action.Equals(second.Action)) return false;
        if (!string.Equals(first.Type, second.Type)) return false;
        if (!first.Start.Equals(second.Start)) return false;
        if (!first.End.Equals(second.End)) return false;
        if (!first.RateId.Equals(second.RateId)) return false;
        if (!string.Equals(first.RateDescription, second.RateDescription)) return false;

        return true;
    }

    public int GetHashCode(Diff obj)
    {
        var hash = obj.Properties?.Aggregate(0,
            (accumulator, current) => accumulator * 17 + current.GetHashCode()) ?? 0;
        hash = hash * 17 + obj.Action.GetHashCode();
        hash = hash * 17 + obj.Type?.GetHashCode() ?? 0;
        hash = hash * 17 + obj.Start.GetHashCode();
        hash = hash * 17 + obj.End.GetHashCode();
        hash = hash * 17 + obj.RateId.GetHashCode();
        hash = hash * 17 + obj.RateDescription?.GetHashCode() ?? 0;

        return hash;
    }
}

And finally we can use this custom comparer in our GroupBy method:

var results = diffs
    .GroupBy(diff => new DiffEqualityComparer())
    .Select( // rest of code omitted 

I solved it!

Reading another question and the comments + answers in this question helped me figure it out!

public class DiffComparer : IEqualityComparer<Diff>
    {
        public bool Equals(Diff x, Diff y)
        {
            return x.Action == y.Action &&
                x.Type == y.Type &&
                x.StartDateTime == y.StartDateTime &&
                x.EndDateTime == y.EndDateTime &&
                x.rateId== y.rateId &&
                x.rateDescription == y.rateDescription &&
                x.properties.SequenceEqual(y.properties);
        }

        public int GetHashCode(Diff x)
        {
            int hash = 17;

            hash = hash * 23 + x.Action.GetHashCode();
            hash = hash * 23 + x.Type.GetHashCode();
            hash = hash * 23 + x.StartDateTime .GetHashCode();
            hash = hash * 23 + x.EndDateTime.GetHashCode();
            hash = hash * 23 + x.rateId.GetHashCode();
            hash = hash * 23 + x.rateDescription.GetHashCode();

            foreach (string prop in x.properties)
            {
                hash = hash * 31 + prop.GetHashCode();
            }

            return hash;
        }
    }

And I made this edit to the LINQ:


var results = diffs
    .GroupBy(diff => diff, new DiffComparer()) 
    .Select(group => new Diff(
        group.Key.Action,
        group.Key.ScheduleType,
        group.Key.StartDateTime,
        group.Key.EndDateTime,
        group.Key.rateId,
        group.Key.rateDescription,
        group.Key.properties,
        group
            .Select(ts => ts.DayOfWeek)
            .Distinct()
            .OrderBy(dow => dow)
            .ToList()))
    .ToList();


Related