using LehrerApp.Core.Models; namespace LehrerApp.Core.Services; public class GradingService { public string CalculateGrade(double achieved, double maximum, List key) { if (maximum <= 0) return "-"; var percent = achieved / maximum * 100.0; return key.OrderByDescending(k => k.MinPercent) .FirstOrDefault(k => percent >= k.MinPercent) ?.Grade ?? key.OrderBy(k => k.MinPercent).First().Grade; } public static List DefaultKey1To6() => [ new() { Grade = "1", MinPercent = 87.5 }, new() { Grade = "2", MinPercent = 75.0 }, new() { Grade = "3", MinPercent = 62.5 }, new() { Grade = "4", MinPercent = 50.0 }, new() { Grade = "5", MinPercent = 25.0 }, new() { Grade = "6", MinPercent = 0.0 }, ]; public static List DefaultKey0To15() => [ new() { Grade = "15", MinPercent = 95.0 }, new() { Grade = "14", MinPercent = 90.0 }, new() { Grade = "13", MinPercent = 85.0 }, new() { Grade = "12", MinPercent = 80.0 }, new() { Grade = "11", MinPercent = 75.0 }, new() { Grade = "10", MinPercent = 70.0 }, new() { Grade = "9", MinPercent = 65.0 }, new() { Grade = "8", MinPercent = 60.0 }, new() { Grade = "7", MinPercent = 55.0 }, new() { Grade = "6", MinPercent = 50.0 }, new() { Grade = "5", MinPercent = 45.0 }, new() { Grade = "4", MinPercent = 40.0 }, new() { Grade = "3", MinPercent = 33.0 }, new() { Grade = "2", MinPercent = 27.0 }, new() { Grade = "1", MinPercent = 20.0 }, new() { Grade = "0", MinPercent = 0.0 }, ]; public string? ValidateGradingKey(List entries) { if (entries.Count < 2) return "Der Notenschlüssel braucht mindestens zwei Stufen."; if (entries.Any(e => string.IsNullOrWhiteSpace(e.Grade))) return "Jede Stufe braucht eine Bezeichnung."; if (entries.Any(e => e.MinPercent < 0 || e.MinPercent > 100)) return "Prozentgrenzen müssen zwischen 0 und 100 liegen."; var sorted = entries.OrderByDescending(e => e.MinPercent).ToList(); if (sorted.Select(e => e.MinPercent).Distinct().Count() != sorted.Count) return "Prozentgrenzen dürfen sich nicht doppeln."; if (sorted.Last().MinPercent != 0) return "Die unterste Stufe muss bei 0 % liegen (lückenlose Abdeckung)."; return null; } /// Wandelt eine Mitarbeit-Bewertung im Bereich -2..+2 in eine Note/Punktzahl /// nach dem jeweiligen Notensystem der Gruppe um (0 % = -2, 100 % = +2). public string ParticipationGrade(double averageRating, GradingSystem system) { var percent = Math.Clamp((averageRating + 2.0) / 4.0 * 100.0, 0, 100); var key = system == GradingSystem.Points0To15 ? DefaultKey0To15() : DefaultKey1To6(); return CalculateGrade(percent, 100, key); } public string? ValidateGradingScheme(GradingScheme scheme) { if (scheme.ExamsPercent < 0 || scheme.ParticipationPercent < 0 || scheme.OtherPercent < 0) return "Anteile dürfen nicht negativ sein."; var sum = scheme.ExamsPercent + scheme.ParticipationPercent + scheme.OtherPercent; return Math.Abs(sum - 100.0) > 0.01 ? $"Die Anteile müssen zusammen 100 % ergeben (aktuell {sum.ToString("0.#")} %)." : null; } public double WeightedAverage(List<(string Grade, double Weight)> grades) { var numeric = grades .Select(g => (Value: int.TryParse(g.Grade, out var n) ? (int?)n : null, g.Weight)) .Where(g => g.Value.HasValue).ToList(); if (numeric.Count == 0) return 0; var total = numeric.Sum(g => g.Weight); return total == 0 ? 0 : numeric.Sum(g => g.Value!.Value * g.Weight) / total; } /// Rundet einen rechnerischen Notenwert auf eine ganze Note/Punktzahl. /// "Kaufmännisch" rundet bei genau 0,5 immer vom Nullpunkt weg (Standard). /// "Pädagogisch" rundet bei genau 0,5 in Richtung der besseren Note /// (Grades1To6: kleinere Zahl ist besser → abrunden; Points0To15: größere Zahl ist besser → aufrunden). public string RoundToGrade(double value, GradingSystem system, RoundingRule rule) { int rounded; var floor = Math.Floor(value); var isExactHalf = Math.Abs(value - floor - 0.5) < 0.0001; if (rule == RoundingRule.Pedagogical && isExactHalf) { var betterIsLower = system == GradingSystem.Grades1To6; rounded = betterIsLower ? (int)floor : (int)floor + 1; } else { rounded = (int)Math.Round(value, MidpointRounding.AwayFromZero); } var (min, max) = system == GradingSystem.Grades1To6 ? (1, 6) : (0, 15); return Math.Clamp(rounded, min, max).ToString(); } /// Berechnet die Zeugnisnote (2.4.1) aus den drei Leistungsbereichen gemäß Gewichtungsschema. /// Bereiche ohne Werte werden ausgelassen; die verbleibenden Prozentanteile werden neu normiert. /// Liefert null, wenn in keinem Bereich Werte vorliegen. public string? CalculateReportGrade( List<(string Grade, double Weight)> examGrades, List<(string Grade, double Weight)> participationGrades, List<(string Grade, double Weight)> otherGrades, GradingScheme scheme, GradingSystem system, RoundingRule rounding) { var buckets = new List<(double Avg, double Percent)>(); if (examGrades.Count > 0) buckets.Add((WeightedAverage(examGrades), scheme.ExamsPercent)); if (participationGrades.Count > 0) buckets.Add((WeightedAverage(participationGrades), scheme.ParticipationPercent)); if (otherGrades.Count > 0) buckets.Add((WeightedAverage(otherGrades), scheme.OtherPercent)); var totalPercent = buckets.Sum(b => b.Percent); if (buckets.Count == 0 || totalPercent <= 0) return null; var weighted = buckets.Sum(b => b.Avg * b.Percent) / totalPercent; return RoundToGrade(weighted, system, rounding); } } public enum RoundingRule { Commercial, Pedagogical }