import { germanDay, germanWeekday } from './dates.ts'; import type { UntisLesson, UntisTimetable } from './untis.ts'; /** * A school day as a note: one file, one heading per lesson. * * This is where the two systems meet for the third one. WebUntis is the only * place that knows which lessons a day actually holds — including that the * third period was cancelled and the fourth is a substitution — so a page that * asks someone to write up their day can hand them the day already laid out * instead of an empty box. The headings it writes are the ones * `subjectFromHeading` reads back, which is what makes each lesson separately * searchable afterwards. */ export interface DayLesson { /** The heading text, without its `##`. */ heading: string; subject?: string; start: string; end: string; periodId: number; /** A substitution: worth knowing while writing, since the teacher differs. */ changed: boolean; } /** `Montag, 15.09.2026` — what a day note is called. */ export function dayNoteTitle(date: string): string { return `${germanWeekday(date)}, ${germanDay(date)}`; } /** * One lesson's heading: `1. DE — 08:00–08:45 · Meier · R 204`. * * The subject leads, because that is the part a person scans for and the part * the parser reads back. Everything after the first `·` is context and may be * edited away without breaking anything. */ export function lessonHeading(lesson: UntisLesson, index: number): string { const subject = lesson.subjects[0]; const name = subject?.longName || subject?.name || 'Stunde'; const teachers = lesson.teachers.map((teacher) => teacher.name).join(', '); const rooms = lesson.rooms.map((room) => room.name).join(', '); return [ `${index + 1}. ${name} — ${lesson.start}–${lesson.end}`, teachers || undefined, rooms ? `R ${rooms}` : undefined, lesson.changed ? 'Vertretung' : undefined, ] .filter(Boolean) .join(' · '); } /** * The day's lessons, in order, as headings. * * Cancelled periods are left out: nothing was taught in them, and a heading * with nothing under it is worse than no heading. A substitution is kept and * marked, because it did happen and its teacher is not the usual one. */ export function dayLessons(timetable: UntisTimetable, date: string): DayLesson[] { const day = timetable.days.find((entry) => entry.date === date); const held = (day?.lessons ?? []).filter((lesson) => !lesson.cancelled); return held.map((lesson, index) => { const subject = lesson.subjects[0]; return { heading: lessonHeading(lesson, index), ...(subject?.longName || subject?.name ? { subject: subject!.longName || subject!.name } : {}), start: lesson.start, end: lesson.end, periodId: lesson.periodId, changed: lesson.changed, }; }); } /** * The starting text for a day's note: a heading per lesson, blank beneath. * * Blank rather than prompted — a placeholder line would have to be deleted in * every lesson of every day, and half of them would survive into the note. */ export function dayNoteSkeleton(lessons: DayLesson[]): string { if (lessons.length === 0) return ''; return `${lessons.map((lesson) => `## ${lesson.heading}\n`).join('\n')}`; } /** * The headings a note is missing, for a day whose timetable is known. * * Someone who starts a note before the day ends, or whose timetable changed * after they started, should be able to top it up without losing what they * wrote — so the page adds what is absent rather than rebuilding the note. */ export function missingHeadings(text: string, lessons: DayLesson[]): DayLesson[] { const present = new Set( text .split('\n') .map((line) => /^##\s+(.*\S)\s*$/.exec(line)?.[1]) .filter((heading): heading is string => Boolean(heading)) .map(headingKey), ); return lessons.filter((lesson) => !present.has(headingKey(lesson.heading))); } /** * What makes two headings "the same lesson". * * The period number and the subject, ignoring everything a person may have * rewritten — a heading edited from `2. DE — 08:50–09:35 · Meier` down to * `2. Deutsch` is still the second period, and adding it again would give the * day two. */ function headingKey(heading: string): string { const match = /^\s*(\d{1,2})\s*[.)]/.exec(heading); return match ? `#${match[1]}` : heading.trim().toLowerCase().replace(/\s+/g, ' '); }