From 22e93770623646227bcddcb9d2a57bcb9a413033 Mon Sep 17 00:00:00 2001 From: Elias Fierke Date: Fri, 29 May 2026 12:52:02 +0200 Subject: [PATCH] [chore:] better crafting --- crafter.cs | 1303 ++++++++++++++++++++++++++++------------------------ 1 file changed, 692 insertions(+), 611 deletions(-) diff --git a/crafter.cs b/crafter.cs index 633ef10..92d420b 100644 --- a/crafter.cs +++ b/crafter.cs @@ -16,682 +16,763 @@ public class CourseCrafter = new(); public static void Craft() +{ + GeneratedCourses = new(); + int globalCount = 0; + List<(Sport, List)> initial_sportlist = new(); + List[] students_in_semester = new List[4] { new(), new(), new(), new() }; + foreach (var sp in Settings.Instance.Sports) { - GeneratedCourses = new(); - int globalCount = 0; - List<(Sport, List)> initial_sportlist = new(); - List[] students_in_semester = new List[4] { new(), new(), new(), new() }; - foreach (var sp in Settings.Instance.Sports) - { - initial_sportlist.Add((sp, new())); - } + initial_sportlist.Add((sp, new())); + } - foreach (Student s in Settings.Instance.Students) + foreach (Student s in Settings.Instance.Students) + { + foreach (var sp in s.SelectedCourseNames) { - foreach (var sp in s.SelectedCourseNames) + foreach (var item in initial_sportlist) { - foreach (var item in initial_sportlist) + if (item.Item1.AlternativeNames.Contains(sp)) { - if (item.Item1.AlternativeNames.Contains(sp)) + item.Item2.Add(s.ID); + break; + } + } + } + } + + while (!requestExit()) + { + Console.WriteLine($"Calculating... ({globalCount})"); + foreach (var item in initial_sportlist) + { + if (item.Item2.Count >= item.Item1.MinStudents) + { + int semester = getSemesterForSport(item.Item1, item.Item2); + if (semester <= 0) goto semeq0; + var inst = new CourseInstance(); + inst.Sport = item.Item1; + inst.Students = new List(); + for (int i = item.Item2.Count - 1; i >= 0; i--) + { + if (inst.Students.Count >= inst.Sport.MaxStudents) + break; + + string stud = item.Item2[i]; + + if (!students_in_semester[semester - 1].Contains(stud)) { - item.Item2.Add(s.ID); + inst.Students.Add(stud); + students_in_semester[semester - 1].Add(stud); + item.Item2.RemoveAt(i); + } + } + if (inst.Students.Count < getEffectiveMinStudents(inst.Sport, semester)) + { + // Rückgängig machen + foreach (var s in inst.Students) + { + students_in_semester[semester - 1].Remove(s); + item.Item2.Add(s); + } + continue; // Kurs nicht erstellen + } + GeneratedCourses.Add((semester, inst)); + } + + semeq0: ; + } + } + + FillExistingCourses(); + + // Kurs umdisponieren (besser verteilen) + bool changed; + int maxIterations = 20; + int iteration = 0; + + do + { + changed = false; + iteration++; + + var sports = GeneratedCourses + .GroupBy(c => c.Instance.Sport.Name); + + foreach (var sportGroup in sports) + { + var courses = sportGroup.ToList(); + + for (int i = 0; i < courses.Count; i++) + { + for (int j = 0; j < courses.Count; j++) + { + if (i == j) continue; + + var cA = courses[i]; + var cB = courses[j]; + + if (cA.Instance.Students.Count <= cB.Instance.Students.Count + 1) + continue; + + for (int k = cA.Instance.Students.Count - 1; k >= 0; k--) + { + string stud = cA.Instance.Students[k]; + + if (!isStudentFree(cB.Semester, stud)) + continue; + + if (cB.Instance.Students.Count >= cB.Instance.Sport.MaxStudents) + continue; + + // Quellkurs darf nicht unter effektives Min fallen + if (cA.Instance.Students.Count - 1 < getEffectiveMinStudents(cA.Instance.Sport, cA.Semester)) + continue; + + cA.Instance.Students.RemoveAt(k); + students_in_semester[cA.Semester - 1].Remove(stud); + + cB.Instance.Students.Add(stud); + students_in_semester[cB.Semester - 1].Add(stud); + + changed = true; break; } } } } - while (!requestExit()) + } while (changed && iteration < maxIterations); + + // --- Kurse nachträglich aufteilen, um NumCoursesPerSemester exakt zu erreichen --- + for (int semester = 1; semester <= 4; semester++) + { + int cancel = 0; + while (GeneratedCourses.Count(c => c.Semester == semester) < Settings.Instance.NumCoursesPerSemester) { - Console.WriteLine($"Calculating... ({globalCount})"); - foreach (var item in initial_sportlist) - { - if (item.Item2.Count >= item.Item1.MinStudents) + cancel++; + if (cancel >= 20) break; + + var candidate = GeneratedCourses + .Where(c => c.Semester == semester) + .Where(c => c.Instance.Students.Count >= getEffectiveMinStudents(c.Instance.Sport, c.Semester) * 2) + .Where(c => { - int semester = getSemesterForSport(item.Item1, item.Item2); - if (semester <= 0) goto semeq0; - var inst = new CourseInstance(); - inst.Sport = item.Item1; - inst.Students = new List(); - // int dist = 1; - for (int i = item.Item2.Count - 1; i >= 0; i--) - { - if (inst.Students.Count >= inst.Sport.MaxStudents) - break; + int sportCount = GeneratedCourses.Count(g => + g.Semester == semester && + g.Instance.Sport.Name == c.Instance.Sport.Name); - string stud = item.Item2[i]; + int allowed = c.Instance.Sport.Semester[semester - 1]; + return sportCount < allowed; + }) + .OrderByDescending(c => c.Instance.Students.Count) + .FirstOrDefault(); - if (!students_in_semester[semester - 1].Contains(stud)) - { - inst.Students.Add(stud); - students_in_semester[semester - 1].Add(stud); - item.Item2.RemoveAt(i); - } - } - if (inst.Students.Count < inst.Sport.MinStudents) - { - // Rückgängig machen - foreach (var s in inst.Students) - { - students_in_semester[semester-1].Remove(s); - item.Item2.Add(s); - } - continue; // Kurs nicht erstellen - } - GeneratedCourses.Add((semester, inst)); - - //MainWindow.Instance.TbResultLog.Text += ($"{semester} -> {inst.Students.Count}\n"); - //MainWindow.Instance.TbResultLog.Text += ($"{students_in_semester[0].Count} - {students_in_semester[1].Count} - {students_in_semester[2].Count} - {students_in_semester[3].Count}\n\n"); + if (candidate.Instance == null) + break; + + var students = candidate.Instance.Students; + int totalStudents = students.Count; + int movedCount = totalStudents / 2; + int remainingCount = totalStudents - movedCount; + + if (movedCount < getEffectiveMinStudents(candidate.Instance.Sport, candidate.Semester) || + remainingCount < getEffectiveMinStudents(candidate.Instance.Sport, candidate.Semester)) + { + break; + } + + var newCourse = new CourseInstance + { + Sport = candidate.Instance.Sport, + Students = new List() + }; + + var moved = students + .Skip(remainingCount) + .Take(movedCount) + .ToList(); + + foreach (var s in moved) + { + candidate.Instance.Students.Remove(s); + newCourse.Students.Add(s); + } + + GeneratedCourses.Add((semester, newCourse)); + } + } + + bool rescueChanged; + do + { + rescueChanged = FillExistingCourses(); + + foreach (var item in initial_sportlist) + { + if (TryCreateRescueCourse(item.Item1, item.Item2)) + { + rescueChanged = true; + } + } + } while (rescueChanged); + + OptimizeStudentWishCoverage(); + + // --- Ringtausch: Schüler mit 4 Wünschen aber < 4 Zuteilungen neu zuordnen --- + foreach (var student in Settings.Instance.Students) + { + if (student.SelectedCourseNames.Count < 4) + continue; + + var currentAssignments = GetAssignmentsBySemester(student.ID); + int currentCount = currentAssignments.Count(a => a != null); + + if (currentCount >= 4) + continue; + + // Alle aktuellen Zuteilungen entfernen (temporär) + foreach (var assignment in currentAssignments) + { + if (assignment == null) continue; + assignment.Value.Instance.Students.Remove(student.ID); + students_in_semester[assignment.Value.Semester - 1].Remove(student.ID); + } + + // Alle gewünschten Sports ermitteln + var desiredSports = student.SelectedCourseNames + .Select(ResolveSportFromSelection) + .Where(s => s != null) + .DistinctBy(s => s!.Name) + .Select(s => s!) + .ToList(); + + // Kandidatenkurse pro Semester: Kurse mit freiem Platz, die einem Wunschsport entsprechen + var candidatesPerSemester = new List<(int Semester, CourseInstance Instance)>[4]; + for (int si = 0; si < 4; si++) + { + candidatesPerSemester[si] = GeneratedCourses + .Where(c => c.Semester == si + 1) + .Where(c => c.Instance.Students.Count < c.Instance.Sport.MaxStudents) + .Where(c => desiredSports.Any(ds => ds.Name == c.Instance.Sport.Name)) + .ToList(); + candidatesPerSemester[si].Insert(0, default); // default = kein Kurs + } + + (int Semester, CourseInstance Instance)?[] bestAssignment = currentAssignments; + int bestScore = currentCount; + + int s0 = candidatesPerSemester[0].Count; + int s1 = candidatesPerSemester[1].Count; + int s2 = candidatesPerSemester[2].Count; + int s3 = candidatesPerSemester[3].Count; + + var sportSelectedCounts = student.SelectedCourseNames + .Select(ResolveSportFromSelection) + .Where(sp => sp != null) + .GroupBy(sp => sp!.Name) + .ToDictionary(g => g.Key, g => g.Count()); + + for (int i0 = 0; i0 < s0; i0++) + for (int i1 = 0; i1 < s1; i1++) + for (int i2 = 0; i2 < s2; i2++) + for (int i3 = 0; i3 < s3; i3++) + { + var c0 = candidatesPerSemester[0][i0]; + var c1 = candidatesPerSemester[1][i1]; + var c2 = candidatesPerSemester[2][i2]; + var c3 = candidatesPerSemester[3][i3]; + + var chosen = new (int Semester, CourseInstance Instance)?[] + { + c0.Instance != null ? (c0.Semester, c0.Instance) : null, + c1.Instance != null ? (c1.Semester, c1.Instance) : null, + c2.Instance != null ? (c2.Semester, c2.Instance) : null, + c3.Instance != null ? (c3.Semester, c3.Instance) : null + }; + + bool valid = true; + var sportUsageCounts = new Dictionary(); + + for (int si = 0; si < 4; si++) + { + if (chosen[si] == null) continue; + string sportName = chosen[si]!.Value.Instance.Sport.Name; + sportUsageCounts.TryGetValue(sportName, out int used); + int allowed = sportSelectedCounts.GetValueOrDefault(sportName, 0); + if (used >= allowed) { valid = false; break; } + sportUsageCounts[sportName] = used + 1; + } + + if (!valid) continue; + + int score = chosen.Count(c => c != null); + if (score > bestScore) + { + bestScore = score; + bestAssignment = chosen; + } + } + + // Beste Zuteilung anwenden + foreach (var assignment in bestAssignment) + { + if (assignment == null) continue; + assignment.Value.Instance.Students.Add(student.ID); + students_in_semester[assignment.Value.Semester - 1].Add(student.ID); + } + } + + // --------------------------------------------------------------------------- + // Lokale Hilfsfunktionen + // --------------------------------------------------------------------------- + + int getEffectiveMinStudents(Sport sport, int semester) + { + int reduction = (semester >= 3) ? 2 : 0; + return Math.Max(1, sport.MinStudents - reduction); + } + + bool isStudentFree(int semester, string studentID) + { + foreach (var inst in GeneratedCourses) + { + if (semester != inst.Semester) continue; + foreach (string stud in inst.Instance.Students) + { + if (stud == studentID) return false; + } + } + return true; + } + + bool requestExit() + { + globalCount++; + if (GeneratedCourses.Count >= Settings.Instance.NumCoursesPerSemester * 4) return true; + + int low = 0; + foreach (var item in initial_sportlist) + { + if (item.Item2.Count < item.Item1.MinStudents) low++; + } + + if (low >= initial_sportlist.Count) return true; + if (globalCount >= 20) return true; + return false; + } + + int total_missing = 0; + foreach (var tuple in initial_sportlist) + { + int[] missingPerSemester = new int[4]; + foreach (var studentId in tuple.Item2.Distinct()) + { + for (int semesterIndex = 0; semesterIndex < 4; semesterIndex++) + { + if (!students_in_semester[semesterIndex].Contains(studentId)) + { + missingPerSemester[semesterIndex]++; + total_missing++; + } + } + } + + MainWindow.Instance.TbResultTextout.Text += + $"{tuple.Item1}: {tuple.Item2.Count} remaining ({missingPerSemester[0]},{missingPerSemester[1]},{missingPerSemester[2]},{missingPerSemester[3]})\n"; + } + MainWindow.Instance.TbResultTextout.Text += $"\n total remaining: {total_missing}"; + + bool FillExistingCourses() + { + bool changed = false; + + foreach (var item in initial_sportlist) + { + if (item.Item2.Count == 0) + continue; + + foreach (var ci in GeneratedCourses + .Where(ci => ci.Instance.Sport.Name == item.Item1.Name) + .OrderBy(ci => ci.Instance.Students.Count)) + { + int semester = ci.Semester; + List added = new(); + + foreach (string stud in item.Item2) + { + if (ci.Instance.Students.Count >= ci.Instance.Sport.MaxStudents) + break; + + if (students_in_semester[semester - 1].Contains(stud)) + continue; + + ci.Instance.Students.Add(stud); + students_in_semester[semester - 1].Add(stud); + added.Add(stud); } - semeq0: ; + foreach (string s in added) + { + item.Item2.Remove(s); + changed = true; + } } - - } - - FillExistingCourses(); - - // Kurs umdisponieren (besser verteilen) - // Kurs umdisponieren (besser verteilen) + + return changed; + } + + bool TryCreateRescueCourse(Sport sport, List remainingStudents) + { + if (remainingStudents.Count == 0) + return false; + + for (int semester = 1; semester <= 4; semester++) + { + int semesterIndex = semester - 1; + + if (sport.Semester[semesterIndex] == 0) + continue; + + int sportCoursesInSemester = GeneratedCourses.Count(c => + c.Semester == semester && c.Instance.Sport.Name == sport.Name); + if (sportCoursesInSemester >= sport.Semester[semesterIndex]) + continue; + + if (GeneratedCourses.Count(c => c.Semester == semester) >= Settings.Instance.NumCoursesPerSemester) + continue; + + var directlyPlaceable = remainingStudents + .Distinct() + .Where(studentId => !students_in_semester[semesterIndex].Contains(studentId)) + .ToList(); + + var donorMoves = new List<((int Semester, CourseInstance Instance) Course, string StudentId)>(); + foreach (var course in GeneratedCourses + .Where(c => c.Instance.Sport.Name == sport.Name && c.Semester != semester) + .OrderByDescending(c => c.Instance.Students.Count)) + { + int movableCount = course.Instance.Students.Count - getEffectiveMinStudents(sport, course.Semester); + if (movableCount <= 0) + continue; + + foreach (var studentId in course.Instance.Students.ToList()) + { + if (movableCount <= 0) + break; + + if (students_in_semester[semesterIndex].Contains(studentId)) + continue; + + donorMoves.Add((course, studentId)); + movableCount--; + } + } + + if (directlyPlaceable.Count + donorMoves.Count < getEffectiveMinStudents(sport, semester)) + continue; + + var newCourse = new CourseInstance + { + Sport = sport, + Students = new List() + }; + + foreach (var studentId in directlyPlaceable) + { + if (newCourse.Students.Count >= sport.MaxStudents) + break; + + newCourse.Students.Add(studentId); + } + + foreach (var move in donorMoves) + { + if (newCourse.Students.Count >= sport.MaxStudents) + break; + + if (newCourse.Students.Contains(move.StudentId)) + continue; + + move.Course.Instance.Students.Remove(move.StudentId); + students_in_semester[move.Course.Semester - 1].Remove(move.StudentId); + newCourse.Students.Add(move.StudentId); + } + + if (newCourse.Students.Count < getEffectiveMinStudents(sport, semester)) + continue; + + foreach (var studentId in newCourse.Students) + { + remainingStudents.Remove(studentId); + students_in_semester[semesterIndex].Add(studentId); + } + + GeneratedCourses.Add((semester, newCourse)); + return true; + } + + return false; + } + + void OptimizeStudentWishCoverage() + { bool changed; - int maxIterations = 20; - int iteration = 0; + int iterations = 0; do { changed = false; - iteration++; + iterations++; - // nach Sport gruppieren - var sports = GeneratedCourses - .GroupBy(c => c.Instance.Sport.Name); - - foreach (var sportGroup in sports) + foreach (var student in Settings.Instance.Students) { - var courses = sportGroup.ToList(); - - // paarweise vergleichen - for (int i = 0; i < courses.Count; i++) + if (TryImproveStudentCoverage(student)) { - for (int j = 0; j < courses.Count; j++) - { - if (i == j) continue; - - var cA = courses[i]; - var cB = courses[j]; - - // nur sinnvoll, wenn Unterschied - if (cA.Instance.Students.Count <= cB.Instance.Students.Count + 1) - continue; - - // Kandidaten aus A nach B verschieben - for (int k = cA.Instance.Students.Count - 1; k >= 0; k--) - { - string stud = cA.Instance.Students[k]; - - // 1. Zielsemester frei? - if (!isStudentFree(cB.Semester, stud)) - continue; - - // 2. Zielkurs hat noch Platz? - if (cB.Instance.Students.Count >= cB.Instance.Sport.MaxStudents) - continue; - - // 3. Quellkurs darf nicht unter Min fallen - if (cA.Instance.Students.Count - 1 < cA.Instance.Sport.MinStudents) - continue; - - // --- MOVE durchführen --- - cA.Instance.Students.RemoveAt(k); - students_in_semester[cA.Semester - 1].Remove(stud); - - cB.Instance.Students.Add(stud); - students_in_semester[cB.Semester - 1].Add(stud); - - changed = true; - break; // nach jedem Move neu bewerten - } - } + changed = true; } } + } while (changed && iterations < 20); + } - } while (changed && iteration < maxIterations); - - // --- Kurse nachträglich aufteilen, um NumCoursesPerSemester exakt zu erreichen --- - for (int semester = 1; semester <= 4; semester++) - { - int cancel = 0; - while (GeneratedCourses.Count(c => c.Semester == semester) < Settings.Instance.NumCoursesPerSemester) - { - cancel++; - if (cancel >= 20) break; - // Kandidaten suchen: splittbare Kurse, deren Sport noch Kapazität hat - var candidate = GeneratedCourses - .Where(c => c.Semester == semester) - .Where(c => c.Instance.Students.Count >= c.Instance.Sport.MinStudents * 2) - .Where(c => - { - int sportCount = GeneratedCourses.Count(g => - g.Semester == semester && - g.Instance.Sport.Name == c.Instance.Sport.Name); - - int allowed = c.Instance.Sport.Semester[semester - 1]; - return sportCount < allowed; - }) - .OrderByDescending(c => c.Instance.Students.Count) - .FirstOrDefault(); - - if (candidate.Instance == null) - break; - - var students = candidate.Instance.Students; - int totalStudents = students.Count; - int movedCount = totalStudents / 2; - int remainingCount = totalStudents - movedCount; - - if (movedCount < candidate.Instance.Sport.MinStudents || - remainingCount < candidate.Instance.Sport.MinStudents) - { - break; - } - - var newCourse = new CourseInstance - { - Sport = candidate.Instance.Sport, - Students = new List() - }; - - var moved = students - .Skip(remainingCount) - .Take(movedCount) - .ToList(); - - foreach (var s in moved) - { - candidate.Instance.Students.Remove(s); - newCourse.Students.Add(s); - } - - GeneratedCourses.Add((semester, newCourse)); - } - } - - bool rescueChanged; - do - { - rescueChanged = FillExistingCourses(); - - foreach (var item in initial_sportlist) - { - if (TryCreateRescueCourse(item.Item1, item.Item2)) - { - rescueChanged = true; - } - } - } while (rescueChanged); - - OptimizeStudentWishCoverage(); - - bool isStudentFree(int semester, string studentID) - { - foreach (var inst in GeneratedCourses) - { - if (semester != inst.Semester) continue; - foreach (string stud in inst.Instance.Students) - { - if (stud == studentID) return false; // Schüler in genanntem Semester bereits gefunden - } - } - - // Schüler nicht gefunden: - return true; - } - - - bool requestExit() - { - globalCount++; - // max Kursanzahl - if (GeneratedCourses.Count >= Settings.Instance.NumCoursesPerSemester * 4) return true; - - - int low = 0; - foreach (var item in initial_sportlist) - { - if (item.Item2.Count < item.Item1.MinStudents) low++; - } - - if (low >= initial_sportlist.Count) return true; - - if (globalCount >= 20) return true; + bool TryImproveStudentCoverage(Student student) + { + var assignments = GetAssignmentsBySemester(student.ID); + if (assignments.All(a => a != null)) return false; - } - - int total_missing = 0; - foreach (var tuple in initial_sportlist) - { - int[] missingPerSemester = new int[4]; - foreach (var studentId in tuple.Item2.Distinct()) - { - for (int semesterIndex = 0; semesterIndex < 4; semesterIndex++) - { - if (!students_in_semester[semesterIndex].Contains(studentId)) - { - missingPerSemester[semesterIndex]++; - total_missing++; - } - } - } - - MainWindow.Instance.TbResultTextout.Text += - $"{tuple.Item1}: {tuple.Item2.Count} remaining ({missingPerSemester[0]},{missingPerSemester[1]},{missingPerSemester[2]},{missingPerSemester[3]})\n"; - - } - MainWindow.Instance.TbResultTextout.Text += $"\n total remaining: {total_missing}"; - - bool FillExistingCourses() - { - bool changed = false; - - foreach (var item in initial_sportlist) - { - if (item.Item2.Count == 0) - continue; - - foreach (var ci in GeneratedCourses - .Where(ci => ci.Instance.Sport.Name == item.Item1.Name) - .OrderBy(ci => ci.Instance.Students.Count)) - { - int semester = ci.Semester; - List added = new(); - - foreach (string stud in item.Item2) - { - if (ci.Instance.Students.Count >= ci.Instance.Sport.MaxStudents) - break; - - if (students_in_semester[semester - 1].Contains(stud)) - continue; - - ci.Instance.Students.Add(stud); - students_in_semester[semester - 1].Add(stud); - added.Add(stud); - } - - foreach (string s in added) - { - item.Item2.Remove(s); - changed = true; - } - } - } - - return changed; - } - - bool TryCreateRescueCourse(Sport sport, List remainingStudents) - { - if (remainingStudents.Count == 0) - return false; - - for (int semester = 1; semester <= 4; semester++) - { - int semesterIndex = semester - 1; - - if (sport.Semester[semesterIndex] == 0) - continue; - - int sportCoursesInSemester = GeneratedCourses.Count(c => - c.Semester == semester && c.Instance.Sport.Name == sport.Name); - if (sportCoursesInSemester >= sport.Semester[semesterIndex]) - continue; - - if (GeneratedCourses.Count(c => c.Semester == semester) >= Settings.Instance.NumCoursesPerSemester) - continue; - - var directlyPlaceable = remainingStudents - .Distinct() - .Where(studentId => !students_in_semester[semesterIndex].Contains(studentId)) - .ToList(); - - var donorMoves = new List<((int Semester, CourseInstance Instance) Course, string StudentId)>(); - foreach (var course in GeneratedCourses - .Where(c => c.Instance.Sport.Name == sport.Name && c.Semester != semester) - .OrderByDescending(c => c.Instance.Students.Count)) - { - int movableCount = course.Instance.Students.Count - sport.MinStudents; - if (movableCount <= 0) - continue; - - foreach (var studentId in course.Instance.Students.ToList()) - { - if (movableCount <= 0) - break; - - if (students_in_semester[semesterIndex].Contains(studentId)) - continue; - - donorMoves.Add((course, studentId)); - movableCount--; - } - } - - if (directlyPlaceable.Count + donorMoves.Count < sport.MinStudents) - continue; - - var newCourse = new CourseInstance - { - Sport = sport, - Students = new List() - }; - - foreach (var studentId in directlyPlaceable) - { - if (newCourse.Students.Count >= sport.MaxStudents) - break; - - newCourse.Students.Add(studentId); - } - - foreach (var move in donorMoves) - { - if (newCourse.Students.Count >= sport.MaxStudents) - break; - - if (newCourse.Students.Contains(move.StudentId)) - continue; - - move.Course.Instance.Students.Remove(move.StudentId); - students_in_semester[move.Course.Semester - 1].Remove(move.StudentId); - newCourse.Students.Add(move.StudentId); - } - - if (newCourse.Students.Count < sport.MinStudents) - continue; - - foreach (var studentId in newCourse.Students) - { - remainingStudents.Remove(studentId); - students_in_semester[semesterIndex].Add(studentId); - } - - GeneratedCourses.Add((semester, newCourse)); - return true; - } + var openSemesters = Enumerable.Range(0, 4) + .Where(i => assignments[i] == null) + .Select(i => i + 1) + .ToList(); + if (openSemesters.Count == 0) return false; - } - void OptimizeStudentWishCoverage() + var assignedSports = assignments + .Where(a => a != null) + .Select(a => a!.Value.Instance.Sport.Name) + .ToHashSet(); + + var preferredSports = student.SelectedCourseNames + .Select(ResolveSportFromSelection) + .Where(sport => sport != null) + .DistinctBy(sport => sport!.Name) + .Select(sport => sport!) + .Where(sport => !assignedSports.Contains(sport.Name)) + .ToList(); + + foreach (var desiredSport in preferredSports) { - bool changed; - int iterations = 0; - - do + for (int sourceSemester = 1; sourceSemester <= 4; sourceSemester++) { - changed = false; - iterations++; + var currentCourse = assignments[sourceSemester - 1]; + if (currentCourse == null) + continue; - foreach (var student in Settings.Instance.Students) + var currentCourseValue = currentCourse.Value; + + var desiredCourse = GeneratedCourses + .FirstOrDefault(course => + course.Semester == sourceSemester && + course.Instance.Sport.Name == desiredSport.Name && + course.Instance.Students.Count < course.Instance.Sport.MaxStudents); + + if (desiredCourse.Instance == null) + continue; + + foreach (var targetSemester in openSemesters) { - if (TryImproveStudentCoverage(student)) - { - changed = true; - } - } - } while (changed && iterations < 20); - } - - bool TryImproveStudentCoverage(Student student) - { - var assignments = GetAssignmentsBySemester(student.ID); - if (assignments.All(a => a != null)) - return false; - - var openSemesters = Enumerable.Range(0, 4) - .Where(i => assignments[i] == null) - .Select(i => i + 1) - .ToList(); - if (openSemesters.Count == 0) - return false; - - var assignedSports = assignments - .Where(a => a != null) - .Select(a => a!.Value.Instance.Sport.Name) - .ToHashSet(); - - var preferredSports = student.SelectedCourseNames - .Select(ResolveSportFromSelection) - .Where(sport => sport != null) - .DistinctBy(sport => sport!.Name) - .Select(sport => sport!) - .Where(sport => !assignedSports.Contains(sport.Name)) - .ToList(); - - foreach (var desiredSport in preferredSports) - { - for (int sourceSemester = 1; sourceSemester <= 4; sourceSemester++) - { - var currentCourse = assignments[sourceSemester - 1]; - if (currentCourse == null) - continue; - - var currentCourseValue = currentCourse.Value; - - var desiredCourse = GeneratedCourses + var relocationTarget = GeneratedCourses .FirstOrDefault(course => - course.Semester == sourceSemester && - course.Instance.Sport.Name == desiredSport.Name && + course.Semester == targetSemester && + course.Instance.Sport.Name == currentCourseValue.Instance.Sport.Name && course.Instance.Students.Count < course.Instance.Sport.MaxStudents); - if (desiredCourse.Instance == null) + if (relocationTarget.Instance == null) continue; - foreach (var targetSemester in openSemesters) - { - var relocationTarget = GeneratedCourses - .FirstOrDefault(course => - course.Semester == targetSemester && - course.Instance.Sport.Name == currentCourseValue.Instance.Sport.Name && - course.Instance.Students.Count < course.Instance.Sport.MaxStudents); + if (!CanRelocateStudent(student.ID, currentCourseValue, relocationTarget)) + continue; - if (relocationTarget.Instance == null) - continue; + currentCourseValue.Instance.Students.Remove(student.ID); + students_in_semester[sourceSemester - 1].Remove(student.ID); - if (!CanRelocateStudent(student.ID, currentCourseValue, relocationTarget)) - continue; + relocationTarget.Instance.Students.Add(student.ID); + students_in_semester[targetSemester - 1].Add(student.ID); - currentCourseValue.Instance.Students.Remove(student.ID); - students_in_semester[sourceSemester - 1].Remove(student.ID); + desiredCourse.Instance.Students.Add(student.ID); + students_in_semester[sourceSemester - 1].Add(student.ID); - relocationTarget.Instance.Students.Add(student.ID); - students_in_semester[targetSemester - 1].Add(student.ID); - - desiredCourse.Instance.Students.Add(student.ID); - students_in_semester[sourceSemester - 1].Add(student.ID); - - return true; - } + return true; } } + } + return false; + } + + bool CanRelocateStudent( + string studentId, + (int Semester, CourseInstance Instance) sourceCourse, + (int Semester, CourseInstance Instance) targetCourse) + { + if (sourceCourse.Semester == targetCourse.Semester) return false; - } - bool CanRelocateStudent( - string studentId, - (int Semester, CourseInstance Instance) sourceCourse, - (int Semester, CourseInstance Instance) targetCourse) - { - if (sourceCourse.Semester == targetCourse.Semester) - return false; + if (sourceCourse.Instance.Students.Count - 1 < getEffectiveMinStudents(sourceCourse.Instance.Sport, sourceCourse.Semester)) + return false; - if (sourceCourse.Instance.Students.Count - 1 < sourceCourse.Instance.Sport.MinStudents) - return false; + if (targetCourse.Instance.Students.Count >= targetCourse.Instance.Sport.MaxStudents) + return false; - if (targetCourse.Instance.Students.Count >= targetCourse.Instance.Sport.MaxStudents) - return false; + if (!isStudentFree(targetCourse.Semester, studentId)) + return false; - if (!isStudentFree(targetCourse.Semester, studentId)) - return false; + return true; + } - return true; - } - - (int Semester, CourseInstance Instance)?[] GetAssignmentsBySemester(string studentId) - { - var assignments = new (int Semester, CourseInstance Instance)?[4]; - - foreach (var course in GeneratedCourses) - { - if (course.Instance.Students.Contains(studentId)) - { - assignments[course.Semester - 1] = course; - } - } - - return assignments; - } - - Sport? ResolveSportFromSelection(string selectedCourseName) - { - if (string.IsNullOrWhiteSpace(selectedCourseName) || - string.Equals(selectedCourseName, "null", StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - return Settings.Instance.Sports - .FirstOrDefault(sport => sport.AlternativeNames.Contains(selectedCourseName)); - } - - int getSemesterForSport2(Sport sp, List interestedStudents) - { - int[] semcount = new int[4]; - - foreach (var inst in GeneratedCourses) - semcount[inst.Semester - 1]++; - - int bestSem = 0; - int minCourses = int.MaxValue; - - for (int i = 0; i < 4; i++) - { - if (sp.Semester[i] == 0) continue; - - // prüfen, ob für diesen Sport im Semester i schon die maximale Anzahl erreicht ist - int sportCoursesInSemester = GeneratedCourses - .Count(g => g.Semester == i + 1 && g.Instance.Sport.Name == sp.Name); - - if (sportCoursesInSemester >= sp.Semester[i]) - continue; - - int freeInterestedStudents = interestedStudents - .Distinct() - .Count(studentId => !students_in_semester[i].Contains(studentId)); - - if (freeInterestedStudents < sp.MinStudents) - continue; - - if (semcount[i] < Settings.Instance.NumCoursesPerSemester && - semcount[i] < minCourses) - { - minCourses = semcount[i]; - bestSem = i + 1; - } - } - - return bestSem; - } - - int getSemesterForSport(Sport sp, List interestedStudents) - { - // 1. Zähle alle Kurse pro Semester (egal welche Sportart) - int[] totalCoursesPerSemester = new int[4]; - foreach (var inst in GeneratedCourses) - totalCoursesPerSemester[inst.Semester - 1]++; - - int bestSem = 0; - int minCourses = int.MaxValue; - int maxFreeInterestedStudents = -1; - - // 2. Kandidaten-Semester durchgehen - for (int i = 0; i < 4; i++) - { - // a) Sport darf in diesem Semester gar nicht stattfinden? - if (sp.Semester[i] == 0) - continue; - - int semesterNumber = i + 1; - - // b) Wie viele Kurse DIESES Sports gibt es schon in diesem Semester? - int sportCoursesInThisSemester = GeneratedCourses - .Count(g => g.Semester == semesterNumber && - g.Instance.Sport.Name == sp.Name); - - // c) Pro-Sport-Limit erreicht? - if (sportCoursesInThisSemester >= sp.Semester[i]) - continue; - - // d) Globales Limit pro Semester erreicht? - if (totalCoursesPerSemester[i] >= Settings.Instance.NumCoursesPerSemester) - continue; - - int freeInterestedStudents = interestedStudents - .Distinct() - .Count(studentId => !students_in_semester[i].Contains(studentId)); - - // e) Ohne genügend freie Interessenten kann in diesem Semester kein Kurs entstehen - if (freeInterestedStudents < sp.MinStudents) - continue; - - // f) Primär das Semester mit den meisten tatsächlich freien Interessenten wählen, - // sekundär das mit den wenigsten Kursen insgesamt. - if (freeInterestedStudents > maxFreeInterestedStudents || - (freeInterestedStudents == maxFreeInterestedStudents && - totalCoursesPerSemester[i] < minCourses)) - { - maxFreeInterestedStudents = freeInterestedStudents; - minCourses = totalCoursesPerSemester[i]; - bestSem = semesterNumber; - } - } - - return bestSem; // 0, falls kein zulässiges Semester gefunden - } - - var errors = ValidateCourses(GeneratedCourses); - - if (errors.Count == 0) - { - MainWindow.Instance.TbResultLog.Text = "--- Alle generierten Kursen erfüllen die gegebenen Voraussetzungen ---"; - } - else - { - MainWindow.Instance.TbResultLog.Text = "--- Bei der Generierung sind folgende Fehler aufgetreten: ---\n\n"; - foreach (var e in errors) - MainWindow.Instance.TbResultLog.Text += e + "\n"; - } - - foreach (var student in Settings.Instance.Students) - { - student.Result = new string[4]; - } + (int Semester, CourseInstance Instance)?[] GetAssignmentsBySemester(string studentId) + { + var assignments = new (int Semester, CourseInstance Instance)?[4]; foreach (var course in GeneratedCourses) { - foreach (var student in Settings.Instance.Students) + if (course.Instance.Students.Contains(studentId)) { - if (course.Instance.Students.Contains(student.ID)) - { - student.Result[course.Semester-1] = course.Instance.Sport.Name; - } + assignments[course.Semester - 1] = course; } } - + + return assignments; } + Sport? ResolveSportFromSelection(string selectedCourseName) + { + if (string.IsNullOrWhiteSpace(selectedCourseName) || + string.Equals(selectedCourseName, "null", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + return Settings.Instance.Sports + .FirstOrDefault(sport => sport.AlternativeNames.Contains(selectedCourseName)); + } + + int getSemesterForSport2(Sport sp, List interestedStudents) + { + int[] semcount = new int[4]; + + foreach (var inst in GeneratedCourses) + semcount[inst.Semester - 1]++; + + int bestSem = 0; + int minCourses = int.MaxValue; + + for (int i = 0; i < 4; i++) + { + if (sp.Semester[i] == 0) continue; + + int sportCoursesInSemester = GeneratedCourses + .Count(g => g.Semester == i + 1 && g.Instance.Sport.Name == sp.Name); + + if (sportCoursesInSemester >= sp.Semester[i]) + continue; + + int freeInterestedStudents = interestedStudents + .Distinct() + .Count(studentId => !students_in_semester[i].Contains(studentId)); + + if (freeInterestedStudents < getEffectiveMinStudents(sp, i + 1)) + continue; + + if (semcount[i] < Settings.Instance.NumCoursesPerSemester && + semcount[i] < minCourses) + { + minCourses = semcount[i]; + bestSem = i + 1; + } + } + + return bestSem; + } + + int getSemesterForSport(Sport sp, List interestedStudents) + { + int[] totalCoursesPerSemester = new int[4]; + foreach (var inst in GeneratedCourses) + totalCoursesPerSemester[inst.Semester - 1]++; + + int bestSem = 0; + int minCourses = int.MaxValue; + int maxFreeInterestedStudents = -1; + + for (int i = 0; i < 4; i++) + { + if (sp.Semester[i] == 0) + continue; + + int semesterNumber = i + 1; + + int sportCoursesInThisSemester = GeneratedCourses + .Count(g => g.Semester == semesterNumber && + g.Instance.Sport.Name == sp.Name); + + if (sportCoursesInThisSemester >= sp.Semester[i]) + continue; + + if (totalCoursesPerSemester[i] >= Settings.Instance.NumCoursesPerSemester) + continue; + + int freeInterestedStudents = interestedStudents + .Distinct() + .Count(studentId => !students_in_semester[i].Contains(studentId)); + + if (freeInterestedStudents < getEffectiveMinStudents(sp, semesterNumber)) + continue; + + if (freeInterestedStudents > maxFreeInterestedStudents || + (freeInterestedStudents == maxFreeInterestedStudents && + totalCoursesPerSemester[i] < minCourses)) + { + maxFreeInterestedStudents = freeInterestedStudents; + minCourses = totalCoursesPerSemester[i]; + bestSem = semesterNumber; + } + } + + return bestSem; + } + + var errors = ValidateCourses(GeneratedCourses); + + if (errors.Count == 0) + { + MainWindow.Instance.TbResultLog.Text = "--- Alle generierten Kursen erfüllen die gegebenen Voraussetzungen ---"; + } + else + { + MainWindow.Instance.TbResultLog.Text = "--- Bei der Generierung sind folgende Fehler aufgetreten: ---\n\n"; + foreach (var e in errors) + MainWindow.Instance.TbResultLog.Text += e + "\n"; + } + + foreach (var student in Settings.Instance.Students) + { + student.Result = new string[4]; + } + + foreach (var course in GeneratedCourses) + { + foreach (var student in Settings.Instance.Students) + { + if (course.Instance.Students.Contains(student.ID)) + { + student.Result[course.Semester - 1] = course.Instance.Sport.Name; + } + } + } +} public static string GenerateStatistics() { GeneratedCourses.Sort((x,y) => x.Semester.CompareTo(y.Semester) ); -- 2.52.0