Neben Wiederholung und Nachschreibarbeit gibt es im Arbeitsdialog die Art Förderarbeit (Exam.IsSupportWork). Sie darf am Tag der ersten Arbeit liegen, übernimmt dessen Datum als Vorschlag und ersetzt wie eine Nachschreibarbeit nur fehlende Noten. 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, Nachschreib-/Förderarbeit)
|
|
/// genau eine Note. Andere Arbeiten bleiben erhalten. Fehlt die erste Arbeit (Schüler war
|
|
/// abwesend), zählt die Nachschreib-/Förderarbeit allein; hat der Schüler beide geschrieben,
|
|
/// zählt immer die erste — sie ersetzen 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.ReplacesMissingGrade() ? 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;
|
|
}
|
|
}
|