Files
LehrerApp/LehrerApp.WebUntis/WebUntisClassAbsenceParser.cs
adminandClaude Sonnet 5 7245196689 WIP (unstable): Klassenlehrer-Tab (fremde Klassenbucheinträge + Fehlzeiten der Klasse)
Neuer Tab in der Gruppendetailansicht (LearningGroup.IsClassTeacher-Flag), zeigt
Klassenbucheinträge anderer Lehrkräfte sowie Fehlzeiten der ganzen Klasse über alle
Fächer, pro Schüler*in und Tag zusammengefasst. Beides rein lesend, kein Übernehmen
in lokale Daten. Fehlzeiten-Bericht (AbsencePerStudent) adressiert die Klasse über
"KL"+getKlassen-ID, automatisch aufgelöst - noch nicht mit echtem WebUntis-Zugang
gegengeprüft (siehe TODO.md).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 21:19:39 +02:00

80 lines
3.5 KiB
C#

using System.Globalization;
namespace LehrerApp.WebUntis;
public static class WebUntisClassAbsenceParser
{
public static IReadOnlyList<UntisClassAbsenceEntry> Parse(string content)
{
var rows = TabSeparatedTextReader.ParseRows(content, '\t');
if (rows.Count == 0) return [];
var headers = rows[0]
.Select((header, index) => (index == 0 ? header.TrimStart('\uFEFF') : header).Trim())
.ToArray();
var result = new List<UntisClassAbsenceEntry>();
foreach (var row in rows.Skip(1))
{
if (row.All(string.IsNullOrWhiteSpace)) continue;
// Der Header "Text" kommt zweimal vor (Spalte 3, in echten Daten immer leer; die
// eigentliche freitextliche Tagesnotiz steht in der späteren Spalte). Der spätere Index
// überschreibt beim Befüllen den früheren, was hier zufällig den gewünschten Wert übrig
// lässt - bewusst kein Verlass darauf, dass Spalte 3 auch künftig immer leer bleibt.
var values = new Dictionary<string, string?>(StringComparer.Ordinal);
for (var index = 0; index < headers.Length; index++)
values[headers[index]] = index < row.Count ? row[index].Trim() : null;
result.Add(new UntisClassAbsenceEntry(
Get(values, "Schüler*innen")?.Trim() ?? "",
OptionalInt(Get(values, "Externe Id")),
Get(values, "Klasse")?.Trim() ?? "",
GermanShortDate(Get(values, "Datum")) ?? throw new InvalidDataException(
"Ungültiges Datum in Spalte \"Datum\"."),
RequiredInt(Get(values, "Fehlstd."), "Fehlstd."),
RequiredInt(Get(values, "Fehlmin."), "Fehlmin."),
Optional(Get(values, "Lehrkraft")),
Optional(Get(values, "Fach")),
Optional(Get(values, "Abwesenheitsgrund")),
Optional(Get(values, "Text")),
OptionalInt(Get(values, "ENr")),
Optional(Get(values, "Erledigt")),
ParseBoolean(Get(values, "Abwesenheit zählt")),
Optional(Get(values, "Entschuldigungstext")),
OptionalInt(Get(values, "Stundennr.")),
Optional(Get(values, "Status")),
Get(values, "Fehltage")?.Trim() == "1"));
}
return result;
}
private static bool ParseBoolean(string? value) =>
string.Equals(value?.Trim(), "true", StringComparison.OrdinalIgnoreCase);
private static string? Get(IReadOnlyDictionary<string, string?> values, string key) =>
values.TryGetValue(key, out var value) ? value : null;
private static string? Optional(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
private static int? OptionalInt(string? value) =>
int.TryParse(value?.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)
? parsed
: null;
private static int RequiredInt(string? value, string field) =>
int.TryParse(value?.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)
? parsed
: throw new InvalidDataException($"Ungültige Zahl in Spalte \"{field}\".");
private static int? GermanShortDate(string? value)
{
if (!DateOnly.TryParseExact(value?.Trim(), "dd.MM.yy", CultureInfo.InvariantCulture,
DateTimeStyles.None, out var date))
return null;
return date.Year * 10_000 + date.Month * 100 + date.Day;
}
}