Untis-API von Server zu Client

This commit is contained in:
2026-08-24 22:00:26 +02:00
parent 0f10f754d0
commit 34a9fdf73b
21 changed files with 243 additions and 445 deletions
@@ -0,0 +1,133 @@
using System.Globalization;
using System.Text;
namespace LehrerApp.WebUntis;
public static class WebUntisStudentReportParser
{
public static IReadOnlyList<UntisStudent> Parse(string content)
{
var rows = ParseSeparatedRows(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<UntisStudent>();
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 untisId = RequiredInt(Get(values, "id"), "id");
var externalKey = RequiredInt(Get(values, "externKey"), "externKey");
var name = Optional(Get(values, "name"));
var lastName = Optional(Get(values, "longName"));
var firstName = Optional(Get(values, "foreName"));
var displayName = string.Join(' ', new[] { firstName, lastName }.Where(value => value is not null));
if (string.IsNullOrWhiteSpace(displayName)) displayName = name ?? $"Schüler {untisId}";
result.Add(new UntisStudent(
untisId,
externalKey,
Get(values, "klasse.name")?.Trim() ?? "",
name,
lastName,
firstName,
displayName,
Optional(Get(values, "gender")),
GermanDate(Get(values, "birthDate")),
Optional(Get(values, "birthDate")),
GermanDate(Get(values, "entryDate")),
Optional(Get(values, "entryDate")),
GermanDate(Get(values, "exitDate")),
Optional(Get(values, "exitDate")),
Optional(Get(values, "text")),
Optional(Get(values, "medicalReportDuty")),
Optional(Get(values, "schulpflicht")),
Optional(Get(values, "majority")),
new UntisStudentAddress(
Optional(Get(values, "adress.email")),
Optional(Get(values, "adress.mobile")),
Optional(Get(values, "adress.phone")),
Optional(Get(values, "adress.city")),
Optional(Get(values, "adress.postCode")),
Optional(Get(values, "adress.street"))),
Optional(Get(values, "attribute.iL"))));
}
return result;
}
private static List<List<string>> ParseSeparatedRows(string content, char separator)
{
var rows = new List<List<string>>();
var row = new List<string>();
var field = new StringBuilder();
var inQuotes = false;
for (var index = 0; index < content.Length; index++)
{
var character = content[index];
if (character == '"')
{
if (inQuotes && index + 1 < content.Length && content[index + 1] == '"')
{
field.Append('"');
index++;
}
else
{
inQuotes = !inQuotes;
}
continue;
}
if (!inQuotes && character == separator)
{
row.Add(field.ToString());
field.Clear();
continue;
}
if (!inQuotes && character == '\n')
{
row.Add(field.ToString());
rows.Add(row);
row = [];
field.Clear();
continue;
}
if (!inQuotes && character == '\r') continue;
field.Append(character);
}
row.Add(field.ToString());
if (row.Count > 1 || !string.IsNullOrWhiteSpace(row[0])) rows.Add(row);
return rows;
}
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? GermanDate(string? value)
{
if (!DateOnly.TryParseExact(value?.Trim(), "dd.MM.yyyy", CultureInfo.InvariantCulture,
DateTimeStyles.None, out var date))
return null;
return date.Year * 10_000 + date.Month * 100 + date.Day;
}
}