From 4f6b33f924e87cda738648e7dd6991a4a02749da Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Fri, 17 Apr 2026 22:46:47 +0200 Subject: [PATCH] simplified code and added documentation --- backend/formatter.js | 64 +++++++++++++++++++++++++++++++++++++++++--- backend/server.js | 4 --- 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/backend/formatter.js b/backend/formatter.js index a01e9f0..800014e 100644 --- a/backend/formatter.js +++ b/backend/formatter.js @@ -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) => { const weekdays = [ "Sonntag", @@ -9,8 +36,16 @@ export const formatData = (rawData) => { "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; + /** + * WebUntis times are typically numbers like 800, 855, 1435. + * Converts to "HH:MM". Returns "–" for missing/invalid values. + */ function formatTime(time) { if (time === null || time === undefined) return "–"; const n = Number(time); @@ -19,6 +54,10 @@ export const formatData = (rawData) => { 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) { if (date === null || date === undefined) return "–"; 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); } + /** + * Returns German weekday name for a WebUntis date (YYYYMMDD). + * Returns "–" for missing/invalid values. + */ function getWeekday(date) { if (date === null || date === undefined) return "–"; const str = String(date); @@ -35,6 +78,10 @@ export const formatData = (rawData) => { return weekdays[d.getDay()]; } + /** + * Parses "HH:MM" into minutes since midnight. + * Returns null for "–" or invalid values. + */ function parseTimeToMinutes(timeStr) { if (!timeStr || timeStr === "–") return null; const [h, m] = String(timeStr).split(":"); @@ -45,6 +92,10 @@ export const formatData = (rawData) => { 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) { if (!dateStr || dateStr === "–") return null; const [dd, mm, yyyy] = String(dateStr).split("."); @@ -57,16 +108,19 @@ export const formatData = (rawData) => { return y * 10000 + m * 100 + d; } + /** @type {any[]} */ const items = rawData?.result ?? []; const lessons = []; for (const item of items) { if (item?.activityType !== "Unterricht") continue; + // Subject name is typically in item.su[0].name. const subject = item?.su?.[0]?.name ?? "–"; const normalizedSubject = String(subject).trimStart(); const isSz = normalizedSubject.startsWith("SZ"); + // WebUntis uses item.code === "cancelled" for cancelled lessons. const cancelled = String(item?.code ?? "") .trim() @@ -97,8 +151,8 @@ export const formatData = (rawData) => { return a._endTimeRaw - b._endTimeRaw; }); - // Group by same day + same start time. - // If any subject in a group starts with "SZ", collapse the whole group into the SZ lesson. + // 1) Group by same day + same start time. + // If any subject in a group starts with "SZ", collapse the whole group into ONE SZ entry. const groups = new Map(); for (const lesson of lessons) { const key = `${lesson._dateRaw}-${lesson._startTimeRaw}`; @@ -126,6 +180,8 @@ export const formatData = (rawData) => { (max, l) => (l._endTimeRaw > max ? l._endTimeRaw : max), 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); timetable.push({ @@ -140,8 +196,8 @@ export const formatData = (rawData) => { }); } - // Merge double lessons (same subject/teacher/room/day) that are consecutive - // (allows a short gap, e.g. breaks). + // 2) Merge double lessons: group by (date, weekday, subject, teacher, room) + // and merge consecutive time blocks within each group. const bucketKey = (e) => `${e.date}|${e.weekday}|${e.subject}|${e.teacher}|${e.room}`; diff --git a/backend/server.js b/backend/server.js index bd91c6d..647a4c2 100644 --- a/backend/server.js +++ b/backend/server.js @@ -163,10 +163,6 @@ app.get("/api/get-timetable/", async (req, res) => { console.error("formatData failed", formatError); res.status(500).json({ error: "Failed to format timetable data" }); } - - // respond to client - res.json(timetableResponse.data?.result ?? null); - return; } catch (error) { const status = error?.response?.status; const data = error?.response?.data;