using System.Globalization; namespace LehrerApp.WebUntis; public static class WebUntisClassAbsenceParser { public static IReadOnlyList 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(); 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(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 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; } }