simplified code and added documentation
This commit is contained in:
+60
-4
@@ -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}`;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user