using Core.Entities; namespace Core.Calculation; public class UnassignedStudentScheduler { private readonly Student[] _students; private readonly Team[] _allTeams; private readonly IList[] _timeSlots; public UnassignedStudentScheduler(Team[] allTeams, TeamScheduleTimeSlot[] timeslots) : this(allTeams, timeslots.Select(e => e.Teams).ToArray()) { } public UnassignedStudentScheduler(Team[] allTeams, Team[][] timeslots) { _allTeams = allTeams; _students = allTeams.SelectMany(t => t.Students).Distinct().ToArray(); _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 Team[] ScheduleStrategy(UnassignedScheduleStrategy scheduleStrategy) { var assignments = scheduleStrategy switch { UnassignedScheduleStrategy.BiggestGroup => ScheduleStrategy(GetAvailableTeams_BiggestGroup), UnassignedScheduleStrategy.IndividualEvents => ScheduleStrategy(GetAvailableTeams_Individual), UnassignedScheduleStrategy.AnyNotMeetingAlready => ScheduleStrategy(GetAvailableTeams_AnyNotMeetingAlready), UnassignedScheduleStrategy.Any => ScheduleStrategy(GetAvailableTeams_Any), UnassignedScheduleStrategy.LevelOfEffort => ScheduleStrategy(GetAvailableTeams_LevelOfEffort), _ => throw new ArgumentOutOfRangeException(nameof(scheduleStrategy), scheduleStrategy, null) }; return assignments; } public Team[] ScheduleStrategy(Func, IEnumerable, IEnumerable> availableTeamSelector) { // Find stuff for unassigned students in each timeslot var scheduledTeams = _timeSlots.SelectMany(list => list).Distinct().ToList(); var additions = new List(); 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); additions.Add(teamToAdd); foreach (var student in teamToAdd.Students) unassigned.Remove(student); } } return additions.ToArray(); } // find teams where several unassigned students can work together private IEnumerable GetAvailableTeams_BiggestGroup( IEnumerable scheduledTeams, IEnumerable assignedStudents) => _allTeams .Where(t => scheduledTeams.All(st => st.Id != t.Id)) .Select(t => t.CloneWithOmittedStudents(assignedStudents)) .Where(t => t.Students.Count > 1) //|| t.Event.EventFormat 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 eventDefinition assignment // find individual events unassigned students can work on private IEnumerable GetAvailableTeams_Individual( IEnumerable scheduledTeams, IEnumerable assignedStudents) => _allTeams .Where(t => scheduledTeams.All(st => st.Id != t.Id)) .Where(t => t.Event.EventFormat == EventFormat.Individual || t.Students.Count == 1) .Select(t => t.CloneWithOmittedStudents(assignedStudents)) .Where(t => t.Students.Count > 0); // find any unassigned eventDefinition students can work on private IEnumerable GetAvailableTeams_AnyNotMeetingAlready( IEnumerable scheduledTeams, IEnumerable assignedStudents) => _allTeams .Where(t => scheduledTeams.All(st => st.Id != t.Id)) .Select(t => t.CloneWithOmittedStudents(assignedStudents)) .Where(t => t.Students.Count > 0); private IEnumerable GetAvailableTeams_Any( IEnumerable scheduledTeams, IEnumerable assignedStudents) => _allTeams .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) => _allTeams .Where(t => scheduledTeams.All(st => st.Id != t.Id)) .Select(t => t.CloneWithOmittedStudents(assignedStudents)) .Where(t => t.Students.Count > 1) //|| t.Event.EventFormat 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.Identifier == team.Identifier)) 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; } }