Files
watch-untis/backend/formatter.js
T
2026-04-17 21:30:46 +02:00

217 lines
6.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export const formatData = (rawData) => {
const weekdays = [
"Sonntag",
"Montag",
"Dienstag",
"Mittwoch",
"Donnerstag",
"Freitag",
"Samstag",
];
const MERGE_GAP_MINUTES = 15;
function formatTime(time) {
if (time === null || time === undefined) return "";
const n = Number(time);
if (!Number.isFinite(n)) return "";
const str = String(time).padStart(4, "0");
return str.slice(0, 2) + ":" + str.slice(2);
}
function formatDate(date) {
if (date === null || date === undefined) return "";
const str = String(date);
if (str.length < 8) return "";
return str.slice(6, 8) + "." + str.slice(4, 6) + "." + str.slice(0, 4);
}
function getWeekday(date) {
if (date === null || date === undefined) return "";
const str = String(date);
if (str.length < 8) return "";
const d = new Date(str.slice(0, 4), str.slice(4, 6) - 1, str.slice(6, 8));
if (Number.isNaN(d.getTime())) return "";
return weekdays[d.getDay()];
}
function parseTimeToMinutes(timeStr) {
if (!timeStr || timeStr === "") return null;
const [h, m] = String(timeStr).split(":");
const hh = Number(h);
const mm = Number(m);
if (!Number.isFinite(hh) || !Number.isFinite(mm)) return null;
if (hh < 0 || hh > 23 || mm < 0 || mm > 59) return null;
return hh * 60 + mm;
}
function dateKeyFromFormatted(dateStr) {
if (!dateStr || dateStr === "") return null;
const [dd, mm, yyyy] = String(dateStr).split(".");
const d = Number(dd);
const m = Number(mm);
const y = Number(yyyy);
if (!Number.isFinite(d) || !Number.isFinite(m) || !Number.isFinite(y)) {
return null;
}
return y * 10000 + m * 100 + d;
}
const items = rawData?.result ?? [];
const lessons = [];
for (const item of items) {
if (item?.activityType !== "Unterricht") continue;
const subject = item?.su?.[0]?.name ?? "";
const normalizedSubject = String(subject).trimStart();
const isSz = normalizedSubject.startsWith("SZ");
const cancelled =
String(item?.code ?? "")
.trim()
.toLowerCase() === "cancelled";
lessons.push({
room: item?.ro?.[0]?.name ?? "",
teacher: item?.te?.[0]?.name ?? "",
subject,
startTime: formatTime(item?.startTime),
endTime: formatTime(item?.endTime),
weekday: getWeekday(item?.date),
date: formatDate(item?.date),
cancelled,
// internal fields for grouping/sorting
_dateRaw: Number(item?.date) || 0,
_startTimeRaw: Number(item?.startTime) || 0,
_endTimeRaw: Number(item?.endTime) || 0,
_isSz: isSz,
});
}
lessons.sort((a, b) => {
if (a._dateRaw !== b._dateRaw) return a._dateRaw - b._dateRaw;
if (a._startTimeRaw !== b._startTimeRaw)
return a._startTimeRaw - b._startTimeRaw;
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.
const groups = new Map();
for (const lesson of lessons) {
const key = `${lesson._dateRaw}-${lesson._startTimeRaw}`;
const group = groups.get(key);
if (group) group.push(lesson);
else groups.set(key, [lesson]);
}
const timetable = [];
for (const group of groups.values()) {
const hasSz = group.some((l) => l._isSz);
if (!hasSz) {
for (const lesson of group) {
const { _dateRaw, _startTimeRaw, _endTimeRaw, _isSz, ...publicLesson } =
lesson;
timetable.push(publicLesson);
}
continue;
}
// Normalize: if any SZ* exists, show exactly one SZ block for that slot.
const base = group.find((l) => l._isSz) ?? group[0];
const maxEndRaw = group.reduce(
(max, l) => (l._endTimeRaw > max ? l._endTimeRaw : max),
0,
);
const cancelled = group.every((l) => l.cancelled);
timetable.push({
room: "-",
teacher: "-",
subject: "SZ",
startTime: base.startTime,
endTime: maxEndRaw ? formatTime(maxEndRaw) : base.endTime,
weekday: base.weekday,
date: base.date,
cancelled,
});
}
// Merge double lessons (same subject/teacher/room/day) that are consecutive
// (allows a short gap, e.g. breaks).
const bucketKey = (e) =>
`${e.date}|${e.weekday}|${e.subject}|${e.teacher}|${e.room}`;
const buckets = new Map();
for (const entry of timetable) {
const key = bucketKey(entry);
const arr = buckets.get(key);
if (arr) arr.push(entry);
else buckets.set(key, [entry]);
}
const merged = [];
for (const arr of buckets.values()) {
arr.sort((a, b) => {
const as = parseTimeToMinutes(a.startTime) ?? 0;
const bs = parseTimeToMinutes(b.startTime) ?? 0;
if (as !== bs) return as - bs;
const ae = parseTimeToMinutes(a.endTime) ?? 0;
const be = parseTimeToMinutes(b.endTime) ?? 0;
return ae - be;
});
const mergedArr = [];
for (const entry of arr) {
const prev = mergedArr[mergedArr.length - 1];
if (!prev) {
mergedArr.push(entry);
continue;
}
const prevEnd = parseTimeToMinutes(prev.endTime);
const currStart = parseTimeToMinutes(entry.startTime);
const currEnd = parseTimeToMinutes(entry.endTime);
const canMergeTimes =
prevEnd !== null &&
currStart !== null &&
currEnd !== null &&
currStart >= prevEnd &&
currStart - prevEnd <= MERGE_GAP_MINUTES;
if (canMergeTimes) {
prev.endTime = entry.endTime;
prev.cancelled = Boolean(prev.cancelled) || Boolean(entry.cancelled);
continue;
}
mergedArr.push(entry);
}
merged.push(...mergedArr);
}
merged.sort((a, b) => {
const ad = dateKeyFromFormatted(a.date) ?? 0;
const bd = dateKeyFromFormatted(b.date) ?? 0;
if (ad !== bd) return ad - bd;
const as = parseTimeToMinutes(a.startTime) ?? 0;
const bs = parseTimeToMinutes(b.startTime) ?? 0;
if (as !== bs) return as - bs;
// stable-ish: keep similar subjects grouped
const subj = String(a.subject).localeCompare(String(b.subject));
if (subj !== 0) return subj;
const room = String(a.room).localeCompare(String(b.room));
if (room !== 0) return room;
return String(a.teacher).localeCompare(String(b.teacher));
});
return merged;
};