diff --git a/MainWindow.axaml.cs b/MainWindow.axaml.cs index 189ed1e..5f3f20c 100644 --- a/MainWindow.axaml.cs +++ b/MainWindow.axaml.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; @@ -59,14 +60,14 @@ public partial class MainWindow : Window } } - private void ChangeStudentCourse(Sport targetSport) + private async void ChangeStudentCourse(Sport targetSport) { if (LbResult.SelectedItem is not ResultEntry selectedEntry) return; try { - if (ApplyStudentCourseChange(selectedEntry.Student, selectedEntry.Semester, targetSport)) + if (await ApplyStudentCourseChange(selectedEntry.Student, selectedEntry.Semester, targetSport)) { CourseCrafter.ReloadResult(); RefreshResultView(); @@ -79,7 +80,7 @@ public partial class MainWindow : Window } } - private bool ApplyStudentCourseChange(Student student, int semester, Sport targetSport) + private async Task ApplyStudentCourseChange(Student student, int semester, Sport targetSport) { if (semester < 1 || semester > 4) return false; @@ -92,14 +93,24 @@ public partial class MainWindow : Window .Where(course => course.Instance.Students.Contains(student.ID)) .ToList(); - CourseCrafter.CourseInstance? targetCourse = currentCourses - .FirstOrDefault(course => course.Instance.Sport.Name == targetSport.Name) - .Instance; + string? oldSportName = currentCourses + .Select(course => course.Instance.Sport.Name) + .FirstOrDefault(); - if (targetCourse == null) + var existingTargetCourses = semesterCourses + .Where(course => course.Instance.Sport.Name == targetSport.Name) + .ToList(); + + CourseCrafter.CourseInstance? targetCourse = existingTargetCourses + .Where(course => !course.Instance.Students.Contains(student.ID) && + course.Instance.Students.Count < course.Instance.Sport.MaxStudents) + .OrderBy(course => course.Instance.Students.Count) + .Select(course => course.Instance) + .FirstOrDefault(); + + if (targetCourse == null && existingTargetCourses.Any()) { - targetCourse = semesterCourses - .Where(course => course.Instance.Sport.Name == targetSport.Name) + targetCourse = existingTargetCourses .OrderBy(course => course.Instance.Students.Count) .Select(course => course.Instance) .FirstOrDefault(); @@ -140,9 +151,87 @@ public partial class MainWindow : Window changed = true; } + if (changed && !string.IsNullOrEmpty(oldSportName) && + IsRebalanceNeededForSportSemester(oldSportName, semester)) + { + var result = await MessageBox.Show(this, + $"Der Kursplan für {oldSportName} im {semester}. Semester ist nach der Änderung unausgeglichen. Soll die alte Sportart umverteilt werden?", + "Umverteilung der alten Sportart", MessageBoxButton.YesNo); + + if (result == MessageBoxResult.Yes) + BalanceSportSemester(oldSportName, semester); + } + return changed; } + private bool IsRebalanceNeededForSportSemester(string sportName, int semester) + { + var courses = CourseCrafter.GeneratedCourses + .Where(course => course.Semester == semester && course.Instance.Sport.Name == sportName) + .ToList(); + + if (courses.Count <= 1) + return false; + + var ordered = courses + .OrderBy(course => course.Instance.Students.Count) + .ToList(); + + int minCount = ordered.First().Instance.Students.Count; + int maxCount = ordered.Last().Instance.Students.Count; + if (maxCount - minCount <= 1) + return false; + + if (ordered.Last().Instance.Students.Count - 1 < GetEffectiveMinStudents(ordered.Last().Instance.Sport, semester)) + return false; + + if (ordered.First().Instance.Students.Count >= ordered.First().Instance.Sport.MaxStudents) + return false; + + return true; + } + + private void BalanceSportSemester(string sportName, int semester) + { + bool changed; + do + { + changed = false; + + var courses = CourseCrafter.GeneratedCourses + .Where(course => course.Semester == semester && course.Instance.Sport.Name == sportName) + .OrderBy(course => course.Instance.Students.Count) + .ToList(); + + if (courses.Count <= 1) + break; + + var target = courses.First(); + var source = courses.Last(); + + if (source.Instance.Students.Count <= target.Instance.Students.Count + 1) + break; + + if (source.Instance.Students.Count - 1 < GetEffectiveMinStudents(source.Instance.Sport, semester)) + break; + + if (target.Instance.Students.Count >= target.Instance.Sport.MaxStudents) + break; + + var studentId = source.Instance.Students[^1]; + source.Instance.Students.RemoveAt(source.Instance.Students.Count - 1); + target.Instance.Students.Add(studentId); + changed = true; + } while (changed); + } + + private int GetEffectiveMinStudents(Sport sport, int semester) + { + int reduction = (semester >= 3) ? 2 : 0; + return Math.Max(1, sport.MinStudents - reduction); + } + private async void MnuExpSettings_OnClick(object? sender, RoutedEventArgs e) { await ExportConfigurationAsync(); diff --git a/crafter.cs b/crafter.cs index d6f7ff2..7d6c76d 100644 --- a/crafter.cs +++ b/crafter.cs @@ -383,6 +383,8 @@ public class CourseCrafter } } + BalanceCoursesBetweenSameSportAndSemester(); + // --------------------------------------------------------------------------- // Lokale Hilfsfunktionen // --------------------------------------------------------------------------- @@ -406,6 +408,54 @@ public class CourseCrafter return true; } + void BalanceCoursesBetweenSameSportAndSemester() + { + bool changed; + do + { + changed = false; + var groups = GeneratedCourses + .GroupBy(c => (c.Semester, c.Instance.Sport.Name)) + .Where(g => g.Count() > 1); + + foreach (var group in groups) + { + var courses = group + .OrderBy(c => c.Instance.Students.Count) + .ToList(); + + for (int sourceIndex = courses.Count - 1; sourceIndex > 0; sourceIndex--) + { + var sourceCourse = courses[sourceIndex]; + for (int targetIndex = 0; targetIndex < sourceIndex; targetIndex++) + { + var targetCourse = courses[targetIndex]; + if (sourceCourse.Instance.Students.Count <= targetCourse.Instance.Students.Count + 1) + break; + + if (targetCourse.Instance.Students.Count >= targetCourse.Instance.Sport.MaxStudents) + continue; + + if (sourceCourse.Instance.Students.Count - 1 < getEffectiveMinStudents(sourceCourse.Instance.Sport, sourceCourse.Semester)) + continue; + + var studentId = sourceCourse.Instance.Students[^1]; + sourceCourse.Instance.Students.RemoveAt(sourceCourse.Instance.Students.Count - 1); + targetCourse.Instance.Students.Add(studentId); + changed = true; + break; + } + + if (changed) + break; + } + + if (changed) + break; + } + } while (changed); + } + bool requestExit() { globalCount++;