diff --git a/crafter.cs b/crafter.cs index 1cce44d..1313f19 100644 --- a/crafter.cs +++ b/crafter.cs @@ -48,7 +48,7 @@ public class CourseCrafter { if (item.Item2.Count >= item.Item1.MinStudents) { - int semester = getSemesterForSport(item.Item1); + int semester = getSemesterForSport(item.Item1, item.Item2); if (semester <= 0) goto semeq0; var inst = new CourseInstance(); inst.Sport = item.Item1; @@ -90,42 +90,7 @@ public class CourseCrafter } - // Kurse auffüllen (mit restl. Leuten) - foreach (var item in initial_sportlist) - { - if (item.Item2.Count > 0) - { - foreach (var ci in GeneratedCourses) - { - - if (item.Item1.ID == ci.Instance.Sport.ID) - { - 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)) - { - ci.Instance.Students.Add(stud); - students_in_semester[semester-1].Add(stud); - //ci.Instance.Students.Add(stud); - added.Add(stud); - } - } - - // Hinzugefügte aus Initialkurs entfernen - foreach (string s in added) - { - item.Item2.Remove(s); - } - } - //MainWindow.Instance.TbResultLog.Text += ($"{ci.Semester} -> {ci.Instance.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"); - } - } - - } + FillExistingCourses(); // Kurs umdisponieren (besser verteilen) // Kurs umdisponieren (besser verteilen) @@ -140,7 +105,7 @@ public class CourseCrafter // nach Sport gruppieren var sports = GeneratedCourses - .GroupBy(c => c.Instance.Sport.ID); + .GroupBy(c => c.Instance.Sport.Name); foreach (var sportGroup in sports) { @@ -193,22 +158,6 @@ public class CourseCrafter } while (changed && iteration < maxIterations); - // // --- Kurse nachträglich aufteilen, um NumCoursesPerSemester exakt zu erreichen --- - // foreach (var course in GeneratedCourses) - // { - // int sem_count_total = GeneratedCourses.Count(tuple => tuple.Semester == course.Semester); - // if (sem_count_total >= Settings.Instance.NumCoursesPerSemester) break; - // - // int sem_count = GeneratedCourses.Count(tuple => tuple.Semester == course.Semester && tuple.Instance.Sport.Name == course.Instance.Sport.Name); - // - // - // if (sem_count < course.Instance.Sport.Semester[course.Semester - 1] && course.Instance.Students.Count >= course.Instance.Sport.MinStudents *2) - // { - // // hier aufteilen - // Console.WriteLine("Könnte aufgeteilt werden."); - // } - // } - // // --- Kurse nachträglich aufteilen, um NumCoursesPerSemester exakt zu erreichen --- for (int semester = 1; semester <= 4; semester++) { @@ -216,7 +165,7 @@ public class CourseCrafter while (GeneratedCourses.Count(c => c.Semester == semester) < Settings.Instance.NumCoursesPerSemester) { cancel++; - if (cancel >= 5) break; + if (cancel >= 20) break; // Kandidaten suchen: splittbare Kurse, deren Sport noch Kapazität hat var candidate = GeneratedCourses .Where(c => c.Semester == semester) @@ -233,54 +182,58 @@ public class CourseCrafter .OrderByDescending(c => c.Instance.Students.Count) .FirstOrDefault(); - try + 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) { - - var students = candidate.Instance.Students; - - var newCourse = new CourseInstance - { - Sport = candidate.Instance.Sport, - Students = new List() - }; - - List moved = new(); - - for (int i = students.Count - 1; i >= 0; i--) - { - string stud = students[i]; - - if (newCourse.Students.Count >= candidate.Instance.Sport.MaxStudents) - break; - - // Sicherstellen, dass beide Kurse >= MinStudents bleiben - if (students.Count - moved.Count <= candidate.Instance.Sport.MinStudents) - break; - - newCourse.Students.Add(stud); - moved.Add(stud); - } - - // Validierung - if (newCourse.Students.Count < candidate.Instance.Sport.MinStudents) - break; - - // Move durchführen - foreach (var s in moved) - { - candidate.Instance.Students.Remove(s); - } - - GeneratedCourses.Add((semester, newCourse)); + break; } - catch + + 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(); + OptimizeStudentWishCoverage(); + OptimizeStudentWishCoverage(); bool isStudentFree(int semester, string studentID) { @@ -317,13 +270,305 @@ public class CourseCrafter return false; } + int total_missing = 0; foreach (var tuple in initial_sportlist) { - MainWindow.Instance.TbResultTextout.Text += $"{tuple.Item1}: {tuple.Item2.Count} remaining\n"; + 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}"; - // ...existing code... - int getSemesterForSport2(Sport sp) + 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; + } + + return false; + } + + void OptimizeStudentWishCoverage() + { + bool changed; + int iterations = 0; + + do + { + changed = false; + iterations++; + + foreach (var student in Settings.Instance.Students) + { + 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 + .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) + { + 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 (relocationTarget.Instance == null) + continue; + + if (!CanRelocateStudent(student.ID, currentCourseValue, relocationTarget)) + continue; + + currentCourseValue.Instance.Students.Remove(student.ID); + students_in_semester[sourceSemester - 1].Remove(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 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 < sourceCourse.Instance.Sport.MinStudents) + return false; + + if (targetCourse.Instance.Students.Count >= targetCourse.Instance.Sport.MaxStudents) + return false; + + if (!isStudentFree(targetCourse.Semester, studentId)) + return false; + + 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]; @@ -339,11 +584,18 @@ public class CourseCrafter // 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.ID == sp.ID); + .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) { @@ -355,7 +607,7 @@ public class CourseCrafter return bestSem; } - int getSemesterForSport(Sport sp) + int getSemesterForSport(Sport sp, List interestedStudents) { // 1. Zähle alle Kurse pro Semester (egal welche Sportart) int[] totalCoursesPerSemester = new int[4]; @@ -364,6 +616,7 @@ public class CourseCrafter int bestSem = 0; int minCourses = int.MaxValue; + int maxFreeInterestedStudents = -1; // 2. Kandidaten-Semester durchgehen for (int i = 0; i < 4; i++) @@ -387,9 +640,21 @@ public class CourseCrafter if (totalCoursesPerSemester[i] >= Settings.Instance.NumCoursesPerSemester) continue; - // e) Wähle das Semester mit bisher insgesamt den wenigsten Kursen - if (totalCoursesPerSemester[i] < minCourses) + 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; } @@ -411,6 +676,11 @@ public class CourseCrafter 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)