Untis-API: Fehlzeiten abgleich

This commit is contained in:
2026-08-25 00:21:53 +02:00
parent 17475a781f
commit 87a7badb44
21 changed files with 593 additions and 298 deletions
@@ -0,0 +1,94 @@
using System.Globalization;
namespace LehrerApp.WebUntis;
public static class WebUntisLessonAbsenceParser
{
public static IReadOnlyList<UntisLessonAbsence> 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<UntisLessonAbsence>();
foreach (var row in rows.Skip(1))
{
if (row.All(string.IsNullOrWhiteSpace)) continue;
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;
var (start, end) = ParseTimeRange(Get(values, "Zeit"));
var (externKey, inParentheses) = ParseExternKey(Get(values, "ENr"));
result.Add(new UntisLessonAbsence(
Get(values, "Schüler*innen")?.Trim() ?? "",
GermanShortDate(Get(values, "Datum")) ?? throw new InvalidDataException(
"Ungültiges Datum in Spalte \"Datum\"."),
RequiredInt(Get(values, "Fehlstd."), "Fehlstd."),
RequiredInt(Get(values, "Unentsch. Fehlstd."), "Unentsch. Fehlstd."),
RequiredInt(Get(values, "Fehlmin."), "Fehlmin."),
RequiredInt(Get(values, "Unentsch. Fehlmin."), "Unentsch. Fehlmin."),
start,
end,
Optional(Get(values, "Abwesenheitsgrund")),
externKey,
inParentheses,
Optional(Get(values, "Erledigt")),
ParseBoolean(Get(values, "Abwesenheit zählt"))));
}
return result;
}
private static (int? Start, int? End) ParseTimeRange(string? value)
{
var parts = value?.Split('-', 2);
return parts is { Length: 2 } ? (ParseClock(parts[0]), ParseClock(parts[1])) : (null, null);
}
private static int? ParseClock(string value) =>
TimeOnly.TryParseExact(value.Trim(), "HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.None,
out var time)
? time.Hour * 100 + time.Minute
: null;
// WebUntis klammert die externe Schülerkennung in dieser Spalte teils ein, teils nicht - die
// Klammer selbst ist das Signal (siehe Statuszuordnung im Desktop-Client), nicht nur Formatierung,
// und wird deshalb hier separat zurückgegeben statt beim Parsen verworfen zu werden.
private static (int? Key, bool InParentheses) ParseExternKey(string? value)
{
var trimmed = value?.Trim();
if (string.IsNullOrEmpty(trimmed)) return (null, false);
var inParentheses = trimmed.StartsWith('(') && trimmed.EndsWith(')');
var digits = trimmed.Trim('(', ')');
return int.TryParse(digits, NumberStyles.Integer, CultureInfo.InvariantCulture, out var key)
? (key, inParentheses)
: (null, false);
}
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 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;
}
}