CI / build-and-test (push) Canceled after 0s
Nachschreibarbeiten nutzen die Wiederholungs-Verknüpfung, ersetzen aber nur fehlende Noten; mehrere Folgearbeiten je erster Arbeit sind möglich. Exam.Kind trennt Klausuren (schriftlich) von Tests u. Ä. (Sonstige). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
117 lines
5.5 KiB
C#
117 lines
5.5 KiB
C#
using LehrerApp.Core.Models;
|
|
|
|
namespace LehrerApp.Core.Services;
|
|
|
|
public static class ExamScoringService
|
|
{
|
|
public static bool AppliesTo(ExamTask task, ExamVariant variant) =>
|
|
task.Variant is null || task.Variant == variant;
|
|
|
|
public static double Maximum(IEnumerable<ExamTask> tasks, ExamVariant variant) =>
|
|
tasks.Where(t => AppliesTo(t, variant) && !t.IsBonus).Sum(t => t.MaxPoints);
|
|
|
|
/// <summary>
|
|
/// Anzeigenummern der Aufgaben in der gespeicherten Reihenfolge. Bonusaufgaben werden auf
|
|
/// dem Aufgabenblatt üblicherweise nicht nummeriert — würden sie mitgezählt, passten alle
|
|
/// Nummern hinter einer Bonusaufgabe nicht mehr zur gedruckten Arbeit. Sie heißen daher
|
|
/// "Bonus" (bei mehreren "Bonus 1", "Bonus 2", …). <see cref="ExamTask.Nr"/> bleibt davon
|
|
/// unberührt die reine Positions-/Sortiernummer.
|
|
/// </summary>
|
|
public static List<string> TaskNumbers(IEnumerable<bool> isBonus)
|
|
{
|
|
var flags = isBonus.ToList();
|
|
var multipleBonus = flags.Count(b => b) > 1;
|
|
int nr = 0, bonus = 0;
|
|
return flags.Select(b => b
|
|
? multipleBonus ? $"Bonus {++bonus}" : "Bonus"
|
|
: (++nr).ToString())
|
|
.ToList();
|
|
}
|
|
|
|
/// <summary>Beschriftung je Aufgabe ("Aufgabe 3", "3. Titel", "Bonus", "Bonus: Titel").</summary>
|
|
public static List<string> TaskLabels(IReadOnlyList<ExamTask> orderedTasks) =>
|
|
TaskNumbers(orderedTasks.Select(t => t.IsBonus))
|
|
.Select((number, i) =>
|
|
{
|
|
var title = orderedTasks[i].Title;
|
|
if (orderedTasks[i].IsBonus)
|
|
return string.IsNullOrWhiteSpace(title) ? number : $"{number}: {title}";
|
|
return string.IsNullOrWhiteSpace(title) ? $"Aufgabe {number}" : $"{number}. {title}";
|
|
})
|
|
.ToList();
|
|
|
|
public static double Achieved(IReadOnlyList<ExamTask> tasks, IReadOnlyList<double> points,
|
|
ExamVariant variant) => tasks.Select((task, index) =>
|
|
AppliesTo(task, variant) && index < points.Count ? points[index] : 0).Sum();
|
|
|
|
/// <summary>
|
|
/// Wählt pro Arbeit samt allen verknüpften Folgearbeiten (Wiederholung, Nachschreibarbeit)
|
|
/// genau eine Note. Andere Arbeiten bleiben erhalten. Fehlt die erste Arbeit (Schüler war
|
|
/// abwesend), zählt die Nachschreibarbeit allein; hat der Schüler beide geschrieben, zählt bei
|
|
/// einer Nachschreibarbeit immer die erste — sie ersetzt nur eine fehlende Note.
|
|
/// </summary>
|
|
public static List<string> EffectiveGrades(IEnumerable<(Exam Exam, string Grade)> entries,
|
|
GradingSystem system)
|
|
{
|
|
var chains = new List<List<(Exam Exam, string Grade)>>();
|
|
var chainByRoot = new Dictionary<Guid, List<(Exam Exam, string Grade)>>();
|
|
foreach (var entry in entries)
|
|
{
|
|
var root = entry.Exam.RepeatOfExamId ?? entry.Exam.Id;
|
|
if (!chainByRoot.TryGetValue(root, out var chain))
|
|
chains.Add(chainByRoot[root] = chain = []);
|
|
chain.Add(entry);
|
|
}
|
|
|
|
var result = new List<string>();
|
|
foreach (var chain in chains)
|
|
{
|
|
// Die erste Arbeit steht vorn, Folgearbeiten chronologisch dahinter.
|
|
var ordered = chain.OrderBy(x => x.Exam.RepeatOfExamId is null ? 0 : 1)
|
|
.ThenBy(x => x.Exam.Date).ToList();
|
|
var current = ordered[0].Grade;
|
|
foreach (var next in ordered.Skip(1))
|
|
current = Choose(current, next.Grade,
|
|
next.Exam.IsMakeUp ? RepeatGradeRule.Original : next.Exam.RepeatGradeRule, system);
|
|
result.Add(current);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wie <see cref="EffectiveGrades"/>, trennt aber nach Art: Klausuren zählen als schriftliche
|
|
/// Note, Tests u. Ä. fließen unter "Sonstige" ein. Verknüpfte Arbeiten erben die Art der
|
|
/// ersten Arbeit (der Dialog erzwingt das), landen also immer im selben Topf.
|
|
/// </summary>
|
|
public static (List<string> Written, List<string> Other) EffectiveGradesByKind(
|
|
IEnumerable<(Exam Exam, string Grade)> entries, GradingSystem system)
|
|
{
|
|
var list = entries.ToList();
|
|
return (
|
|
EffectiveGrades(list.Where(x => x.Exam.Kind.CountsAsWritten()), system),
|
|
EffectiveGrades(list.Where(x => !x.Exam.Kind.CountsAsWritten()), system));
|
|
}
|
|
|
|
private static string Choose(string original, string repeat, RepeatGradeRule rule, GradingSystem system)
|
|
{
|
|
if (rule == RepeatGradeRule.Original) return original;
|
|
if (rule == RepeatGradeRule.Repeat) return repeat;
|
|
var first = GradeValue(original, system);
|
|
var second = GradeValue(repeat, system);
|
|
if (first is null || second is null) return original;
|
|
return system == GradingSystem.Grades1To6
|
|
? (second < first ? repeat : original)
|
|
: (second > first ? repeat : original);
|
|
}
|
|
|
|
private static double? GradeValue(string grade, GradingSystem system)
|
|
{
|
|
var core = grade.Trim();
|
|
var tendency = 0.0;
|
|
if (core.EndsWith('+')) { tendency = system == GradingSystem.Grades1To6 ? -1.0 / 3 : 1.0 / 3; core = core[..^1]; }
|
|
else if (core.EndsWith('-')) { tendency = system == GradingSystem.Grades1To6 ? 1.0 / 3 : -1.0 / 3; core = core[..^1]; }
|
|
return double.TryParse(core.Trim(), System.Globalization.NumberStyles.Any,
|
|
System.Globalization.CultureInfo.InvariantCulture, out var value) ? value + tendency : null;
|
|
}
|
|
}
|