using Core.Entities; namespace Core.Calculation; public enum UnassignedScheduleStrategy { BiggestGroup, IndividualEvents, AnyNotMeetingAlready, LevelOfEffort, Any } public class UnassignedStudentScheduler { private readonly IList _students; private readonly IList _teams; private readonly IList[] _timeslots; public UnassignedStudentScheduler(IList teams, IList[] timeslots) { _teams = teams; _students = teams.SelectMany(t => t.Students).Distinct().ToList(); _timeslots = timeslots.Select(ts => ts.Select(t => t.Clone()).ToList()).ToArray(); } public static IEnumerable UnassignedStudents(IList students, IList timeSlot) => students.Where(s => !timeSlot.SelectMany(t => t.Students).Contains(s)); public static IEnumerable[] UnassignedStudents(IList students, IList[] schedule) => schedule.Select(ts => UnassignedStudents(students, ts)).ToArray(); public IList[] ScheduleStrategy(UnassignedScheduleStrategy scheduleStrategy) { switch (scheduleStrategy) { case UnassignedScheduleStrategy.BiggestGroup: return ScheduleStrategy(GetAvailableTeams_BiggestGroup); case UnassignedScheduleStrategy.IndividualEvents: return ScheduleStrategy(GetAvailableTeams_Individual); case UnassignedScheduleStrategy.AnyNotMeetingAlready: return ScheduleStrategy(GetAvailableTeams_AnyNotMeetingAlready); case UnassignedScheduleStrategy.Any: return ScheduleStrategy(GetAvailableTeams_Any); case UnassignedScheduleStrategy.LevelOfEffort: return ScheduleStrategy(GetAvailableTeams_LevelOfEffort); default: throw new ArgumentOutOfRangeException(nameof(scheduleStrategy), scheduleStrategy, null); } } public IList[] ScheduleStrategy(Func, IEnumerable, IEnumerable> availableTeamSelector) { // Find stuff for unassigned students in each timeslot var scheduledTeams = _timeslots.SelectMany(list => list).Distinct().ToList(); foreach (var slot in _timeslots) { var unassigned = UnassignedStudents(_students, slot).ToList(); while (unassigned.Count > 0) { var assignedStudents = slot.SelectMany(t => t.Students).Distinct(); var availableTeams = availableTeamSelector(scheduledTeams, assignedStudents); var teamToAdd = availableTeams.FirstOrDefault(); if (teamToAdd == null) break; slot.Add(teamToAdd); scheduledTeams.Add(teamToAdd); foreach (var student in teamToAdd.Students) unassigned.Remove(student); } } return _timeslots; } // find teams where several unassigned students can work together private IEnumerable GetAvailableTeams_BiggestGroup( IEnumerable scheduledTeams, IEnumerable assignedStudents) => _teams .Where(t => scheduledTeams.All(st => st.Name != t.Name)) .Select(t => t.CloneWithOmittedStudents(assignedStudents)) .Where(t => t.Students.Count > 1) //|| t.Event.Format is EventFormat.Individual //.OrderBy(t => scheduledTeams.Count(st => st.Name == t.Name)) .OrderByDescending(t => t.Students.Count); // select descending greatest number of students assigned //.ThenBy(t => Random.Shared.Next()); // todo: sort by student historic record of event assignment // find individual events unassigned students can work on private IEnumerable GetAvailableTeams_Individual( IEnumerable scheduledTeams, IEnumerable assignedStudents) => _teams .Where(t => scheduledTeams.All(st => st.Name != t.Name)) .Where(t => t.Event.Format == EventFormat.Individual || t.Students.Count == 1) .Select(t => t.CloneWithOmittedStudents(assignedStudents)) .Where(t => t.Students.Count > 0); // find any unassigned event students can work on private IEnumerable GetAvailableTeams_AnyNotMeetingAlready( IEnumerable scheduledTeams, IEnumerable assignedStudents) => _teams .Where(t => scheduledTeams.All(st => st.Name != t.Name)) .Select(t => t.CloneWithOmittedStudents(assignedStudents)) .Where(t => t.Students.Count > 0); private IEnumerable GetAvailableTeams_Any( IEnumerable scheduledTeams, IEnumerable assignedStudents) => _teams .Select(t => t.CloneWithOmittedStudents(assignedStudents)) .Where(t => t.Students.Count > 0); // find teams where several unassigned students can work together private IEnumerable GetAvailableTeams_LevelOfEffort( IEnumerable scheduledTeams, IEnumerable assignedStudents) => _teams .Where(t => scheduledTeams.All(st => st.Name != t.Name)) .Select(t => t.CloneWithOmittedStudents(assignedStudents)) .Where(t => t.Students.Count > 1) //|| t.Event.Format is EventFormat.Individual //.OrderBy(t => scheduledTeams.Count(st => st.Name == t.Name)) .OrderByDescending(t => t.Event.LevelOfEffort); // select descending greatest number of students assigned //add team to another timeslot, if any available public IList[] AddAdditionalTimeSlot(Team team) { // sort by how many teammembers are already in that timeslot, descending foreach (var timeslot in _timeslots.OrderBy(ts => ts.SelectMany(t => t.Students).Count(team.Students.Contains))) { if (timeslot.Any(t => t.Name == team.Name)) continue; timeslot.Add(team); break; } return _timeslots; } // Keep the current events rolling for the other time slots public IList[] ExtendEvents() { var scheduledTeams = _timeslots.SelectMany(list => list).Distinct().ToList(); // get all the students in each time slot var timeslotStudents = _timeslots.Select(ts => ts.SelectMany(t => t.Students).Distinct()).ToArray(); // clone teams from timeslot for each other timeslot, removing the students //var copiedTeams = new Dictionary>(); for (var i = 0; i < _timeslots.Length; i++) { var sourceTimeslot = _timeslots[i]; for (var j = 0; j < _timeslots.Length; j++) { if (i == j) continue; var targetTimeslot = _timeslots[j]; var targetTimeslotStudents = targetTimeslot.SelectMany(t => t.Students).Distinct(); var clonedTeams = sourceTimeslot.Select(t => t.CloneWithOmittedStudents(targetTimeslotStudents)); foreach (var clonedTeam in clonedTeams.Where(t => t.Students.Any())) { targetTimeslot.Add(clonedTeam); } } } return _timeslots; } }