CI / build-and-test (push) Canceled after 0s
Bisher liess int.TryParse jede Note mit Tendenz durchfallen und WeightedAverage rechnete den betroffenen Bereich mit 0 statt sie auszulassen - faelschte Notenuebersicht, Zielnoten- und Was-waere-wenn-Rechner. Ausserdem schlaegt der Mitarbeits-Assistent beim Abschnitt-Abschliessen jetzt das Ende der zuletzt abgeschlossenen Unterrichtseinheit statt immer "heute" vor. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
150 lines
6.8 KiB
C#
150 lines
6.8 KiB
C#
using LehrerApp.Core.Models;
|
|
|
|
namespace LehrerApp.Core.Services;
|
|
|
|
public class GradingService
|
|
{
|
|
public string CalculateGrade(double achieved, double maximum, List<GradingKeyEntry> 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<GradingKeyEntry> 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<GradingKeyEntry> 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<GradingKeyEntry> 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: ParseGradeValue(g.Grade), 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;
|
|
}
|
|
|
|
/// Wandelt eine Notenangabe mit optionaler Tendenz ("3+", "2-") in einen rechnerischen
|
|
/// Notenwert um. Tendenzen verschieben um ein Drittel in Richtung der besseren Note
|
|
/// (z.B. "3+" → 2,67, "2-" → 2,33) — der in deutschen Notensystemen übliche Drittelabstand.
|
|
private static double? ParseGradeValue(string grade)
|
|
{
|
|
var trimmed = grade.Trim();
|
|
if (trimmed.Length == 0) return null;
|
|
|
|
var tendency = 0.0;
|
|
var core = trimmed;
|
|
if (core.EndsWith('+')) { tendency = -1.0 / 3.0; core = core[..^1].TrimEnd(); }
|
|
else if (core.EndsWith('-')) { tendency = 1.0 / 3.0; core = core[..^1].TrimEnd(); }
|
|
|
|
return int.TryParse(core, out var n) ? n + tendency : null;
|
|
}
|
|
|
|
/// 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 }
|