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; };