2

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?

surprised_ferret
  • 119
  • 3
  • 12
  • @Enigmativity - Are you misunderstanding the issue? – Travis J Aug 18 '20 at 22:54
  • 1
    @Enigmativity I added my question at the bottom just now, thank you – surprised_ferret Aug 18 '20 at 22:54
  • @TravisJ - No, but the OP originally just gave a series of statements. They didn't explain where they were stuck, what attempts they made, or clearly asked us what they needed from us. A refresher on [ask] might be useful. – Enigmativity Aug 18 '20 at 22:55
  • @Enigmativity - The statement "My LINQ query does not do what I think it will do" in coordination with the groupby statement seems pretty clear cut to me. A refresher on GetHashCode or Equals may be useful. – Travis J Aug 18 '20 at 22:58
  • 1
    This problem would be a lot easier to solve if you were to override [`Equals`](https://learn.microsoft.com/en-us/dotnet/api/system.object.equals?view=netcore-3.1) in `Diff`. – John Wu Aug 18 '20 at 23:05
  • 2
    One of your `GroupBy` properties (`diff.properties`) is a `List`. The default comparer for a `List` is a reference comparison, so no two lists will ever match in this case. You'll need to write your own comparer for the `Diff` class, where you might use `.SequenceEqual` to determine equality between the `strings` in `properties`. Probably the same thing applies to the `Action` property. – Rufus L Aug 18 '20 at 23:17

3 Answers3

4

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
Travis J
  • 81,153
  • 41
  • 202
  • 273
  • If I want to keep the anonymous grouping by using `new {...}`, would I then want to override Equals() and GetHashCode() that includes only those fields (because I only want to group by those fields)? – surprised_ferret Aug 18 '20 at 23:18
2

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 
Rufus L
  • 36,127
  • 5
  • 30
  • 43
1

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();


surprised_ferret
  • 119
  • 3
  • 12