using System; using System.Collections.Generic; using System.Linq; namespace spplus; public class CourseCrafter { public class CourseInstance { public Sport Sport = null!; public int Remaining; public List Students = new(); } public static List<(int Semester, CourseInstance Instance)> GeneratedCourses = 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) { initial_sportlist.Add((sp, new())); } foreach (Student s in Settings.Instance.Students) { foreach (var sp in s.SelectedCourseNames) { foreach (var item in initial_sportlist) { 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); 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; string stud = item.Item2[i]; 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"); } semeq0: ; } } // 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"); } } } // Kurs umdisponieren (besser verteilen) // Kurs umdisponieren (besser verteilen) bool changed; int maxIterations = 20; int iteration = 0; do { changed = false; iteration++; // nach Sport gruppieren var sports = GeneratedCourses .GroupBy(c => c.Instance.Sport.ID); foreach (var sportGroup in sports) { var courses = sportGroup.ToList(); // paarweise vergleichen 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]; // 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 } } } } } while (changed && iteration < maxIterations); 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; return false; } foreach (var tuple in initial_sportlist) { MainWindow.Instance.TbResultTextout.Text += $"{tuple.Item1}: {tuple.Item2.Count} remaining\n"; } // ...existing code... int getSemesterForSport2(Sport sp) { 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.ID == sp.ID); if (sportCoursesInSemester >= sp.Semester[i]) continue; if (semcount[i] < Settings.Instance.NumCoursesPerSemester && semcount[i] < minCourses) { minCourses = semcount[i]; bestSem = i + 1; } } return bestSem; } int getSemesterForSport(Sport sp) { // 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; // 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; // e) Wähle das Semester mit bisher insgesamt den wenigsten Kursen if (totalCoursesPerSemester[i] < minCourses) { 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 = "Alles 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 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) ); string sb = $"Generierte Kurse: {GeneratedCourses.Count}\n\n"; foreach (var genc in GeneratedCourses) { sb += $"Sem. {genc.Semester}: {genc.Instance.Sport.Name} ({genc.Instance.Students.Count} SuS)\n"; } return sb; } public static string GenerateStatisticsOld() { var settings = Settings.Instance; var students = settings.Students; if (GeneratedCourses == null || GeneratedCourses.Count == 0) return "Keine Kurse generiert."; int semesterCount = students .Where(s => s.Result != null) .Select(s => s.Result!.Count) .DefaultIfEmpty(0) .Max(); var sb = new System.Text.StringBuilder(); sb.AppendLine($"Anzahl generierter Kurse: {GeneratedCourses.Count}"); sb.AppendLine("Übersicht:"); // ===== Kursübersicht ===== var grouped = GeneratedCourses .GroupBy(g => new { g.Semester, g.Instance.Sport.Name }) .OrderBy(g => g.Key.Semester) .ThenBy(g => g.Key.Name); foreach (var group in grouped) { int counter = 1; foreach (var entry in group) { int semester = group.Key.Semester + 1; string sportName = group.Key.Name; string number = counter.ToString("D2"); int count = entry.Instance.Students.Count; sb.AppendLine( $"Semester {semester}: {sportName} {number}: {count} Schüler*innen" ); counter++; } } sb.AppendLine(); sb.AppendLine("Fehlerübersicht:"); // ===== Fehler pro Semester ===== for (int sem = 0; sem < semesterCount; sem++) { int errors = students.Count(st => st.Result != null && st.Result.Count > sem && st.Result[sem] == "Fehler"); sb.AppendLine($"Semester {sem + 1}: {errors} Fehler"); } return sb.ToString(); } public static List ValidateCourses(List<(int Semester, CourseInstance Instance)> courses) { List errors = new(); // --- 1. Min/Max + Semester erlaubt --- foreach (var tuple in courses) { int semester = tuple.Semester; var inst = tuple.Instance; var sport = inst.Sport; if (inst.Students.Count < sport.MinStudents) { errors.Add($"[Min] {sport.Name} (Sem {semester}): {inst.Students.Count} < {sport.MinStudents}"); } if (inst.Students.Count > sport.MaxStudents) { errors.Add($"[Max] {sport.Name} (Sem {semester}): {inst.Students.Count} > {sport.MaxStudents}"); } if (sport.Semester[semester - 1] == 0) { errors.Add($"[Semester] {sport.Name} darf nicht in Semester {semester} stattfinden"); } // --- doppelte Schüler im selben Kurs --- for (int i = 0; i < inst.Students.Count; i++) { for (int j = i + 1; j < inst.Students.Count; j++) { if (inst.Students[i] == inst.Students[j]) { errors.Add($"[Kurs-Duplikat] {inst.Students[i]} mehrfach in {sport.Name} (Sem {semester})"); } } } } // --- 2. Schüler doppelt im Semester --- for (int sem = 1; sem <= 4; sem++) { List students = new(); foreach (var tuple in courses) { if (tuple.Semester != sem) continue; foreach (var stud in tuple.Instance.Students) { // prüfen ob schon drin bool exists = false; foreach (var s in students) { if (s == stud) { exists = true; break; } } if (exists) { errors.Add($"[Doppelt] Schüler {stud} doppelt in Semester {sem}"); } else { students.Add(stud); } } } } // --- 3. Sport-Angebote pro Semester zählen --- // (ohne Dictionary: wir iterieren über alle Kurse und zählen jeweils erneut) foreach (var tuple in courses) { int semester = tuple.Semester; var sport = tuple.Instance.Sport; int count = 0; foreach (var other in courses) { if (other.Semester == semester && other.Instance.Sport.Name == sport.Name) { count++; } } int allowed = sport.Semester[semester - 1]; if (count > allowed) { errors.Add($"[Sport-Semester] {sport.Name} in Sem {semester}: {count} Kurse > erlaubt {allowed}"); } } return errors; } }