From cf9949876dea7e367f08a71fb4c2ca7e4e1b5a4b Mon Sep 17 00:00:00 2001 From: James Kolpack Date: Thu, 30 Oct 2025 09:35:41 -0400 Subject: [PATCH] Add a TimeSlot object, refactor --- Core/Calculation/TeamScheduleTimeSlot.cs | 16 ++ Core/Calculation/TeamScheduler.cs | 4 +- Core/Calculation/TeamSchedulerSolution.cs | 15 +- .../Calculation/TeamScheduler_DecisionTree.cs | 2 +- .../Calculation/UnassignedStudentScheduler.cs | 22 ++- .../Pages/MeetingSchedulePages/Index.razor | 183 +++++++++--------- 6 files changed, 133 insertions(+), 109 deletions(-) create mode 100644 Core/Calculation/TeamScheduleTimeSlot.cs diff --git a/Core/Calculation/TeamScheduleTimeSlot.cs b/Core/Calculation/TeamScheduleTimeSlot.cs new file mode 100644 index 0000000..e6f8834 --- /dev/null +++ b/Core/Calculation/TeamScheduleTimeSlot.cs @@ -0,0 +1,16 @@ +using Core.Entities; + +namespace Core.Calculation; + +public class TeamScheduleTimeSlot +{ + public string Name { get; set; } + public Team[] Teams; + public Student[] UnscheduledStudents; + public IEnumerable>> StudentOverlaps; + + public bool StudentHasOverlaps(Student student) + { + return StudentOverlaps.FirstOrDefault(o => o.Item1.Equals(student)) != null; + } +} \ No newline at end of file diff --git a/Core/Calculation/TeamScheduler.cs b/Core/Calculation/TeamScheduler.cs index 36fb6a9..7026d70 100644 --- a/Core/Calculation/TeamScheduler.cs +++ b/Core/Calculation/TeamScheduler.cs @@ -92,7 +92,7 @@ public class TeamScheduler var timeSlotTeams = new Team[_timeSlots.Length][]; if (cpSolverStatus is not (CpSolverStatus.Optimal or CpSolverStatus.Feasible)) - return new TeamSchedulerSolution(timeSlotTeams, cpSolverStatus.ToString()); + return new TeamSchedulerSolution(timeSlotTeams, _studentObjects.ToArray(), cpSolverStatus.ToString()); Debug.WriteLine($"Total cost: {solver.ObjectiveValue}\n"); @@ -102,6 +102,6 @@ public class TeamScheduler timeSlotTeams[s] = teams; } //Debug.WriteLine("No solution found."); - return new TeamSchedulerSolution(timeSlotTeams, cpSolverStatus.ToString()); + return new TeamSchedulerSolution(timeSlotTeams, _studentObjects.ToArray(), cpSolverStatus.ToString()); } } \ No newline at end of file diff --git a/Core/Calculation/TeamSchedulerSolution.cs b/Core/Calculation/TeamSchedulerSolution.cs index 785797f..f7a6d0b 100644 --- a/Core/Calculation/TeamSchedulerSolution.cs +++ b/Core/Calculation/TeamSchedulerSolution.cs @@ -4,11 +4,18 @@ namespace Core.Calculation; public class TeamSchedulerSolution( Team[][] timeSlots, + Student[] students, string status) { - public Team[][] TimeSlots { get; set; } = timeSlots; - public string Status { get; set; } = status; + public string Status { get; } = status; + public TeamScheduleTimeSlot[] TimeSlots { get; set; } + = timeSlots.Select( (teams,i) => + new TeamScheduleTimeSlot{Name = i.ToString(), Teams = teams, + StudentOverlaps = GetStudentTeamOverlaps(teams), + UnscheduledStudents = GetStudentsNotInTimSlot(teams, students) + } + ).ToArray(); public static int GetStudentTeamOverlapCount(Team[][] timeSlots) { @@ -30,7 +37,6 @@ public class TeamSchedulerSolution( select Tuple.Create(gs.First(), gs.Key); } - public static Student[] GetStudentsNotInTimSlot(Team[] timeSlot, Student[] students) { var studentsInTimeSlot = timeSlot.SelectMany(ts => ts.Students).Distinct(); @@ -39,12 +45,11 @@ public class TeamSchedulerSolution( where studentsInTimeSlot.FirstOrDefault(e => e.Equals(allStudent)) == null select allStudent ).ToArray(); - } public Team[] StudentUnassignedTeams(Student student) { - var meetingTeams = TimeSlots.SelectMany(t => t); + var meetingTeams = TimeSlots.SelectMany(t => t.Teams); return student.Teams.Where(e => !meetingTeams.Contains(e)).ToArray(); } diff --git a/Core/Calculation/TeamScheduler_DecisionTree.cs b/Core/Calculation/TeamScheduler_DecisionTree.cs index e45735d..d3d0bf5 100644 --- a/Core/Calculation/TeamScheduler_DecisionTree.cs +++ b/Core/Calculation/TeamScheduler_DecisionTree.cs @@ -33,7 +33,7 @@ public class TeamScheduler_DecisionTree timeSlots[overlaps.First().Item1].Add(team); } - return new TeamSchedulerSolution(timeSlots.Select(e => e.ToArray()).ToArray(), "Success?"); + return new TeamSchedulerSolution(timeSlots.Select(e => e.ToArray()).ToArray(), [], "Success?"); } //public Team[][] SolveRecursive() diff --git a/Core/Calculation/UnassignedStudentScheduler.cs b/Core/Calculation/UnassignedStudentScheduler.cs index ff1f2e0..7ffedf3 100644 --- a/Core/Calculation/UnassignedStudentScheduler.cs +++ b/Core/Calculation/UnassignedStudentScheduler.cs @@ -5,13 +5,17 @@ namespace Core.Calculation; public class UnassignedStudentScheduler { private readonly Student[] _students; - private readonly Team[] _teams; + private readonly Team[] _allTeams; private readonly IList[] _timeSlots; - public UnassignedStudentScheduler(Team[] teams, Team[][] timeslots) + public UnassignedStudentScheduler(Team[] allTeams, TeamScheduleTimeSlot[] timeslots) + : this(allTeams, timeslots.Select(e => e.Teams).ToArray()) + { } + + public UnassignedStudentScheduler(Team[] allTeams, Team[][] timeslots) { - _teams = teams; - _students = teams.SelectMany(t => t.Students).Distinct().ToArray(); + _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) @@ -68,7 +72,7 @@ public class UnassignedStudentScheduler // find teams where several unassigned students can work together private IEnumerable GetAvailableTeams_BiggestGroup( IEnumerable scheduledTeams, IEnumerable assignedStudents) => - _teams + _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 @@ -79,7 +83,7 @@ public class UnassignedStudentScheduler // find individual events unassigned students can work on private IEnumerable GetAvailableTeams_Individual( IEnumerable scheduledTeams, IEnumerable assignedStudents) => - _teams + _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)) @@ -88,14 +92,14 @@ public class UnassignedStudentScheduler // find any unassigned eventDefinition students can work on private IEnumerable GetAvailableTeams_AnyNotMeetingAlready( IEnumerable scheduledTeams, IEnumerable assignedStudents) => - _teams + _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) => - _teams + _allTeams .Select(t => t.CloneWithOmittedStudents(assignedStudents)) .Where(t => t.Students.Count > 0); @@ -103,7 +107,7 @@ public class UnassignedStudentScheduler // find teams where several unassigned students can work together private IEnumerable GetAvailableTeams_LevelOfEffort( IEnumerable scheduledTeams, IEnumerable assignedStudents) => - _teams + _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 diff --git a/WebApp/Components/Pages/MeetingSchedulePages/Index.razor b/WebApp/Components/Pages/MeetingSchedulePages/Index.razor index 82e67c8..13a6351 100644 --- a/WebApp/Components/Pages/MeetingSchedulePages/Index.razor +++ b/WebApp/Components/Pages/MeetingSchedulePages/Index.razor @@ -11,66 +11,67 @@ @Configuration["ChapterSettings:Shortname"] TSA Schedule @Configuration["ChapterSettings:CompetitionYear"] - @* Include: @string.Join(", ", _scheduledTeams) *@ Time Slots - - - - - - Add High Effort - - - Add Regionals - - - Remove Individual - - - Remove Low Effort - - - Invert - - - Reset - - - Solve - - - - + + + + + + + + + Add High Effort + + + Add Regionals + + + Remove Individual + + + Remove Low Effort + + + Invert + + + Reset + + + Solve + + + + - - - - - + + + + + + + - @{ - var overlaps - = TeamSchedulerSolution.GetStudentTeamOverlaps(context).ToArray(); - } - @foreach (var team in context.OrderBy(e => e.ToString())) + @foreach (var team in context.Teams.OrderBy(e => e.ToString())) { var removed = !_scheduledTeams.Contains(team); - @team - + Class="@(removed ? "" : "d-none")"> + + @team - @foreach (var student in team.Students) { - var overlap = overlaps.Any(o => o.Item1.Equals(student)); + var overlap = context.StudentHasOverlaps(student); var color = overlap ? Color.Warning : Color.Default; } - - - @{ - var unscheduled = TeamSchedulerSolution.GetStudentsNotInTimSlot(context, _students); - } - @if (unscheduled.Any()) - { - - Unscheduled - - @foreach (var student in unscheduled) + + + @if (context.UnscheduledStudents.Any()) + { + Unscheduled + + @foreach (var student in context.UnscheduledStudents) { @student.FirstName   @@ -111,20 +108,22 @@ OnClick="@(() => ToggleRequiredTeam(unassignedTeam))"> + Class="@(added ? "" : "d-none")"> + @unassignedTeam } } - } - - - + } + + + + Scheduled Teams @string.Join(", ", (_possibleAdditions ?? []).Select(e => e.ToString())) _solutionData; + MudTable _solutionData; private TeamSchedulerSolution _solution; private TeamSchedulerOptions _parameters; bool _isSolving; @@ -161,13 +160,13 @@ private async Task AddRegionals() { - _scheduledTeams + _scheduledTeams = _teams.Where(e => e.Event.RegionalEvent).Concat(_scheduledTeams).Distinct(); } private async Task AddHighLevelOfEffort() { - _scheduledTeams + _scheduledTeams = _teams.Where(e => e.Event.LevelOfEffort >= 3).Concat(_scheduledTeams).Distinct(); } @@ -209,27 +208,27 @@ { _parameters = new TeamSchedulerOptions( - timeSlots: 2, + 2, mustIncludeEvents: [ - // "Medical Technology", "Electrical Applications" , "RegionalTeam", - // ,"Dragster", "Flight" + // "Medical Technology", "Electrical Applications" , "RegionalTeam", + // ,"Dragster", "Flight" ], extended: [ - // "Invention", "Construction Challenge", "Mechanical", "Mass", "Micro" - //"STEM" - //"Community", "Vlogging"// "Microcontroller" + // "Invention", "Construction Challenge", "Mechanical", "Mass", "Micro" + //"STEM" + //"Community", "Vlogging"// "Microcontroller" ], omittedEvents: [ - // "Vlogging", "Junior", "Community Service Video", "Digital Photography", - // "STEM" + // "Vlogging", "Junior", "Community Service Video", "Digital Photography", + // "STEM" - //"Leadership",// "Electrical", //"Construction" - // "Forensic", - //"CAD" - //"I&I Team 1", "I&I Team 2"//, "Website Design", + //"Leadership",// "Electrical", //"Construction" + // "Forensic", + //"CAD" + //"I&I Team 1", "I&I Team 2"//, "Website Design", ], absentStudents: [ @@ -237,14 +236,14 @@ ); _teams - = await Context.Teams - .Include(e => e.Event) - .Include(e => e.Students) - .OrderBy(e => e.Event.Name) - .ThenBy(e => e.Identifier) - .ToArrayAsync(); + = await Context.Teams + .Include(e => e.Event) + .Include(e => e.Students) + .OrderBy(e => e.Event.Name) + .ThenBy(e => e.Identifier) + .ToArrayAsync(); - _students = + _students = await Context.Students .Include(e => e.Teams) .ThenInclude(e => e.Captain) @@ -253,7 +252,7 @@ .OrderBy(e => e.FirstName).ToArrayAsync(); } - private async Task> SolveSchedule(TableState arg1, CancellationToken arg2) + private async Task> SolveSchedule(TableState arg1, CancellationToken arg2) { _isSolving = true; var teamScheduler = new TeamScheduler(_scheduledTeams, _parameters.TimeSlots); @@ -282,7 +281,7 @@ await InvokeAsync(StateHasChanged); // let the UI know that the solution has been found _isSolving = false; - return new TableData { Items = _solution.TimeSlots}; + return new TableData { Items = _solution.TimeSlots }; } private void Solve() @@ -295,9 +294,9 @@ var sb = new StringBuilder(); foreach (var timeslot in _solution.TimeSlots) { - var overlaps - = TeamSchedulerSolution.GetStudentTeamOverlaps(timeslot).Select(e => e.Item1).ToArray(); - foreach (var scheduledTeam in timeslot.OrderBy(e => e.ToString())) + //var overlaps + // = TeamSchedulerSolution.GetStudentTeamOverlaps(timeslot).Select(e => e.Item1).ToArray(); + foreach (var scheduledTeam in timeslot.Teams.OrderBy(e => e.ToString())) { var t = scheduledTeam.ToString(); var s = @@ -305,7 +304,7 @@ scheduledTeam.Students .OrderBy(e => e == scheduledTeam.Captain) .ThenBy(e => e.FirstName) - .Select(e => e.FirstName + (overlaps.Contains(e) ? "*": ""))); + .Select(e => e.FirstName + (timeslot.StudentHasOverlaps(e) ? "*" : ""))); if (scheduledTeam.Event.EventFormat is EventFormat.Individual) sb.Append(t); @@ -313,13 +312,12 @@ sb.Append($"{t} - {s}"); sb.Append(Environment.NewLine); } - var unscheduled = TeamSchedulerSolution.GetStudentsNotInTimSlot(timeslot, _students); - if (unscheduled.Any()) + if (timeslot.UnscheduledStudents.Any()) { sb.Append("--Unscheduled"); sb.Append(Environment.NewLine); - foreach (var student in unscheduled) + foreach (var student in timeslot.UnscheduledStudents) { var s = student.FirstName; var unassignedTeams = _solution.StudentUnassignedTeams(student); @@ -342,4 +340,5 @@ Console.WriteLine("Cannot write text to clipboard"); } } -} + +} \ No newline at end of file