simplified code and added documentation

This commit is contained in:
2026-04-17 22:46:47 +02:00
parent 6889cf15bf
commit 4f6b33f924
2 changed files with 60 additions and 8 deletions
+60 -4
View File
@@ -1,3 +1,30 @@
/**
* Formats the raw WebUntis JSON-RPC timetable response into a compact list of lessons.
*
* Output schema (per entry):
* - room: string
* - teacher: string
* - subject: string
* - startTime: "HH:MM" | ""
* - endTime: "HH:MM" | ""
* - weekday: German weekday name | ""
* - date: "DD.MM.YYYY" | ""
* - cancelled: boolean
*
* Business rules implemented here:
* 1) SZ normalization:
* If multiple lessons share the same day + same start time and at least one subject starts
* with "SZ" (e.g. "SZ1", "SZ10"), the whole slot becomes exactly ONE entry with:
* subject="SZ", room="-", teacher="-".
* 2) Cancelled flag:
* If the raw item contains code="cancelled", cancelled=true, otherwise cancelled=false.
* 3) Double-lesson merging:
* Consecutive lessons with same (date, weekday, subject, teacher, room) are merged into one
* block by extending endTime. A small gap (break) up to MERGE_GAP_MINUTES is allowed.
*
* @param {any} rawData WebUntis JSON-RPC response (expects rawData.result to be an array)
* @returns {Array<{room:string,teacher:string,subject:string,startTime:string,endTime:string,weekday:string,date:string,cancelled:boolean}>}
*/
export const formatData = (rawData) => { export const formatData = (rawData) => {
const weekdays = [ const weekdays = [
"Sonntag", "Sonntag",
@@ -9,8 +36,16 @@ export const formatData = (rawData) => {
"Samstag", "Samstag",
]; ];
/**
* Allowed gap (in minutes) between two blocks to still merge them into a double lesson.
* Example: 15:30 -> 15:35 (5 min break) will still merge.
*/
const MERGE_GAP_MINUTES = 15; const MERGE_GAP_MINUTES = 15;
/**
* WebUntis times are typically numbers like 800, 855, 1435.
* Converts to "HH:MM". Returns "" for missing/invalid values.
*/
function formatTime(time) { function formatTime(time) {
if (time === null || time === undefined) return ""; if (time === null || time === undefined) return "";
const n = Number(time); const n = Number(time);
@@ -19,6 +54,10 @@ export const formatData = (rawData) => {
return str.slice(0, 2) + ":" + str.slice(2); return str.slice(0, 2) + ":" + str.slice(2);
} }
/**
* WebUntis dates are typically numbers like 20260417.
* Converts to "DD.MM.YYYY". Returns "" for missing/invalid values.
*/
function formatDate(date) { function formatDate(date) {
if (date === null || date === undefined) return ""; if (date === null || date === undefined) return "";
const str = String(date); const str = String(date);
@@ -26,6 +65,10 @@ export const formatData = (rawData) => {
return str.slice(6, 8) + "." + str.slice(4, 6) + "." + str.slice(0, 4); return str.slice(6, 8) + "." + str.slice(4, 6) + "." + str.slice(0, 4);
} }
/**
* Returns German weekday name for a WebUntis date (YYYYMMDD).
* Returns "" for missing/invalid values.
*/
function getWeekday(date) { function getWeekday(date) {
if (date === null || date === undefined) return ""; if (date === null || date === undefined) return "";
const str = String(date); const str = String(date);
@@ -35,6 +78,10 @@ export const formatData = (rawData) => {
return weekdays[d.getDay()]; return weekdays[d.getDay()];
} }
/**
* Parses "HH:MM" into minutes since midnight.
* Returns null for "" or invalid values.
*/
function parseTimeToMinutes(timeStr) { function parseTimeToMinutes(timeStr) {
if (!timeStr || timeStr === "") return null; if (!timeStr || timeStr === "") return null;
const [h, m] = String(timeStr).split(":"); const [h, m] = String(timeStr).split(":");
@@ -45,6 +92,10 @@ export const formatData = (rawData) => {
return hh * 60 + mm; return hh * 60 + mm;
} }
/**
* Converts a formatted date "DD.MM.YYYY" to a sortable numeric key YYYYMMDD.
* Returns null for "" or invalid values.
*/
function dateKeyFromFormatted(dateStr) { function dateKeyFromFormatted(dateStr) {
if (!dateStr || dateStr === "") return null; if (!dateStr || dateStr === "") return null;
const [dd, mm, yyyy] = String(dateStr).split("."); const [dd, mm, yyyy] = String(dateStr).split(".");
@@ -57,16 +108,19 @@ export const formatData = (rawData) => {
return y * 10000 + m * 100 + d; return y * 10000 + m * 100 + d;
} }
/** @type {any[]} */
const items = rawData?.result ?? []; const items = rawData?.result ?? [];
const lessons = []; const lessons = [];
for (const item of items) { for (const item of items) {
if (item?.activityType !== "Unterricht") continue; if (item?.activityType !== "Unterricht") continue;
// Subject name is typically in item.su[0].name.
const subject = item?.su?.[0]?.name ?? ""; const subject = item?.su?.[0]?.name ?? "";
const normalizedSubject = String(subject).trimStart(); const normalizedSubject = String(subject).trimStart();
const isSz = normalizedSubject.startsWith("SZ"); const isSz = normalizedSubject.startsWith("SZ");
// WebUntis uses item.code === "cancelled" for cancelled lessons.
const cancelled = const cancelled =
String(item?.code ?? "") String(item?.code ?? "")
.trim() .trim()
@@ -97,8 +151,8 @@ export const formatData = (rawData) => {
return a._endTimeRaw - b._endTimeRaw; return a._endTimeRaw - b._endTimeRaw;
}); });
// Group by same day + same start time. // 1) Group by same day + same start time.
// If any subject in a group starts with "SZ", collapse the whole group into the SZ lesson. // If any subject in a group starts with "SZ", collapse the whole group into ONE SZ entry.
const groups = new Map(); const groups = new Map();
for (const lesson of lessons) { for (const lesson of lessons) {
const key = `${lesson._dateRaw}-${lesson._startTimeRaw}`; const key = `${lesson._dateRaw}-${lesson._startTimeRaw}`;
@@ -126,6 +180,8 @@ export const formatData = (rawData) => {
(max, l) => (l._endTimeRaw > max ? l._endTimeRaw : max), (max, l) => (l._endTimeRaw > max ? l._endTimeRaw : max),
0, 0,
); );
// SZ cancelled only if ALL underlying entries are cancelled.
// If at least one is not cancelled, the SZ block is considered not cancelled.
const cancelled = group.every((l) => l.cancelled); const cancelled = group.every((l) => l.cancelled);
timetable.push({ timetable.push({
@@ -140,8 +196,8 @@ export const formatData = (rawData) => {
}); });
} }
// Merge double lessons (same subject/teacher/room/day) that are consecutive // 2) Merge double lessons: group by (date, weekday, subject, teacher, room)
// (allows a short gap, e.g. breaks). // and merge consecutive time blocks within each group.
const bucketKey = (e) => const bucketKey = (e) =>
`${e.date}|${e.weekday}|${e.subject}|${e.teacher}|${e.room}`; `${e.date}|${e.weekday}|${e.subject}|${e.teacher}|${e.room}`;
-4
View File
@@ -163,10 +163,6 @@ app.get("/api/get-timetable/", async (req, res) => {
console.error("formatData failed", formatError); console.error("formatData failed", formatError);
res.status(500).json({ error: "Failed to format timetable data" }); res.status(500).json({ error: "Failed to format timetable data" });
} }
// respond to client
res.json(timetableResponse.data?.result ?? null);
return;
} catch (error) { } catch (error) {
const status = error?.response?.status; const status = error?.response?.status;
const data = error?.response?.data; const data = error?.response?.data;