1

I am trying to conditioanlly show a "week list"(each day of a week) either showing all 5 days(Mon-Fri), or a single "list item"(single list item represnting all days of week) based on if all items are the same for each day (with exception of key(s)). The following works until I add the "DayFunctions"(a list) as an item of the "week list". Is there something different that needs to be done when Grouping with a List property?

I have not been able to find anything in SO or even googling (for days) that addresses this specific Grouping issue.

UPDATED with Custom Comparer

//POCO
public class ScheduleWeek : IScheduleWeek
{
    public int Id { get; set; }
    public int Yr { get; set; }
    public int WeekNo { get; set; }
    public List<ScheduleDay> ScheduleDays { get; set; }
}


// POCO
public class ScheduleDay : IScheduleDay
{
    public int Id { get; set; }
    public int ScheduleWeekId { get; set; }
    public string EmpId { get; set; }
    public DateTime ScheduleDayDt { get; set; }
    public Shift Shift { get; set; }
    public Station Station { get; set; }
    public List<DayFunction> DayFunctions { get; set; }
}

// POCO
public class DayFunction
{
    public int ScheduleDayId { get; set; }
    public int FunctionId { get; set; }
}


// HELPER METHOD
public async Task<List<ScheduleDay>> GetScheduleDaysWithDayFunctions(int stationId, 
                                                                        string empId, 
                                                                        int scheduleWeekId)
{
    var sdlist = await _ctx.ScheduleDays
        .Where(sd => sd.ScheduleDayDt.DayOfWeek != DayOfWeek.Sunday &&
                        sd.ScheduleDayDt.DayOfWeek != DayOfWeek.Saturday &&
                        sd.Station.Id == stationId &&
                        sd.EmpId == empId &&
                        sd.ScheduleWeekId == scheduleWeekId)
        .Include(d => d.DayFunctions)
        .ToListAsync();

    return sdlist;
}

// HELPER METHOD
//  ** ADDED CUSTOM COMPARER
public class ScheduleDayComparer : IEqualityComparer<ScheduleDay>
{
    public bool Equals(ScheduleDay x, ScheduleDay y)
    {
        return x.Id == y.Id && x.DayFunctions
                    .SequenceEqual(y.DayFunctions);
    }

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

// MODEL FOR VIEW
public class ScheduleWeekViewModel
{
    public ScheduleWeek ScheduleWeek { get; set; }
    public ScheduleHelpers ScheduleHelpers { get; set; }
}


// MVC VIEW
@{
    var scheduleDayComparer = new ScheduleHelpers.ScheduleDayComparer(); // <-- ADDED        
    var scheduledays = Model.ScheduleHelpers
        .GetScheduleDaysWithDayFunctions(station.Id,
            employee.Id.ToString(),
            Model.ScheduleWeek.Id).Result
        .GroupBy(x => x, scheduleDayComparer) // <-- ADDED
            .Select(g => g.First())           // <-- ADDED
            .ToList();                        // <-- ADDED
        //.GroupBy(sd => new
        //    {
        //        sd.Shift.Id,
        //        sd.DayFunctions // <-- this causes the grouping to break
        //    })
        //.Select(g => g.First());
}
<table>
    @foreach (var scheduleday in scheduledays)
    {
        <tr>
            <td>
                <text>SHIFT: @scheduleday.Shift.Name </text>
            </td>
            <td>
                @foreach (var df in scheduleday.DayFunctions)
                    {
                        <text>DAY FUNCTIONS: @df.FunctionId </text>
                    }   
            </td>
        </tr>
    }
</table>

//  CONTROLLER ACTION
public async Task<IActionResult> LocationScheduler()
{
    var vm = new ScheduleWeekViewModel{};
    var user = await _userManager.GetUserAsync(User);
    vm.Employees = await _tcomsHelpers.GetEmployeesOfSupervisor(user);
    vm.ScheduleDays = await _scheduleHelpers.GetScheduleWeekDaysForScheduler(vm.Employees);
    vm.ScheduleWeek = await _scheduleHelpers.GetCurrentScheduleWeek();
    vm.ScheduleHelpers = _scheduleHelpers;
    return View(vm);
}
Emo333
  • 116
  • 1
  • 12
  • C# does not know, how to compare two lists for goupping. You must to write custom comparasion function for two lists – Backs Jun 13 '18 at 16:40
  • I am not comparing two lists. This is a list inside a list. – Emo333 Jun 13 '18 at 16:42
  • 1
    `GroupBy` need some function to compare objects. For default types (ints, strings) it use default comparison. But complex types like `List` need custom. Read more about `GroupBy` method – Backs Jun 13 '18 at 16:46
  • @Backs That was the ticket. Thank you for pointing me to looking at the IEqualityComparer<>! I found this [link](https://stackoverflow.com/a/35129285/6448599) after you helped me narrow down my search. Please answer so I can give you credit and thank you! – Emo333 Jun 13 '18 at 17:35
  • @Emo333 Your implementation of `IEqualityComparer` with `SequenceEquals` call requires identical sequence in addition to having identical content. This may be problematic, depending on your requirements. Also your hash code method is sub-optimal, because `^` may lead to "false positives." A better implementation uses a small prime and multiplication, like this: `Aggregate(0, (a, y) => 31*a + y.GetHashCode());` Finally, note that you need to implement `HashCode` and `Equals` in your `DayFunction` class. – Sergey Kalinichenko Jun 13 '18 at 20:02

1 Answers1

0

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}")
    )
})
Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523