Files
psu-schedule/server/timetable.js

128 lines
4.9 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.
// A group's timetable by date, with week numbers already applied. Native and
// third-party clients use it instead of re-implementing the white/green week rules.
import { semesterView, addDays, weekday, weekIndexOf } from "../shared/weeks.js";
import { lessonKey, subgroupsOf } from "../shared/lessons.js";
import { occurrence, peopleAndRooms, roomsOn } from "./directory.js";
import { apiError } from "./errors.js";
const MAX_DAYS = 62;
// A whole semester with its session fits easily; the cap only guards against abuse.
const MAX_SEMESTER_DAYS = 200;
const DAY_MS = 86_400_000;
function lessonOut(lesson, group, people, date, status, calendar) {
const bell = calendar?.bells.find((b) => b.start === lesson.start);
const rooms = date ? roomsOn(people, date) : people.rooms;
return {
key: lessonKey(lesson, group),
start: lesson.start,
end: lesson.end || bell?.end || null,
pair: bell?.pair ?? null,
subject: lesson.subject,
type: lesson.type,
teachers: people.teachers,
rooms,
roomMoved: Boolean(date && rooms !== people.rooms),
distance: people.distance,
condition: lesson.condition,
variant: lesson.variant,
subgroup: lesson.subgroups.find((s) => s.group === group)?.label || null,
groups: lesson.groups,
uncertain: status === "maybe",
raw: lesson.raw,
};
}
/**
* days: one entry per date from `from`, with the lessons that take place that day.
* weekly: the Word rows by weekday, for when dates cannot be calculated.
*/
export function timetable({ file, schedule, calendar, group, from, days, range, today }) {
if (!schedule.groups.includes(group)) {
throw apiError(404, "group_not_found", `В документе нет группы ${group}.`);
}
let count = Math.min(Math.max(Number(days) || 7, 1), MAX_DAYS);
const lessons = schedule.lessons.filter((l) => l.groups.includes(group));
// A session schedule printed with a date on every day needs no week table: its own dates
// say when each lesson takes place, even in a year the table no longer covers.
const sessionDates = lessons.every((l) => l.date || l.dates?.length)
? lessons.flatMap((l) => (l.date ? [l.date] : l.dates)).sort()
: [];
const session = sessionDates.length
? {
id: `session-${sessionDates[0]}`,
label: schedule.period || "Сессия",
start: sessionDates[0],
end: sessionDates.at(-1),
}
: null;
// range=semester: every day of the semester `today` falls in, so a client can keep
// the whole term offline; a session schedule keeps its own span. Without either it
// keeps the plain from/days window.
if (range === "semester") {
const current = semesterView(calendar, schedule, today || from);
const span = current?.semester || session;
if (span) {
from = span.start;
const length = Math.round((Date.parse(span.end) - Date.parse(from)) / DAY_MS) + 1;
count = Math.min(Math.max(length, 1), MAX_SEMESTER_DAYS);
}
}
const entries = lessons.map((lesson) => ({
doc: { id: file.id, schedule },
lesson,
...peopleAndRooms(lesson, schedule.academicYear),
}));
const view = semesterView(calendar, schedule, from);
const views = new Map();
const out = [];
for (let i = 0; i < count; i++) {
const date = addDays(from, i);
const week = view ? view.weeks[weekIndexOf(view, date)] || null : null;
const dayLessons = [];
if (view || session)
for (const entry of entries) {
const status = occurrence(entry, date, calendar, views);
if (status === "yes" || status === "maybe")
dayLessons.push(lessonOut(entry.lesson, group, entry, date, status, calendar));
}
dayLessons.sort((a, b) => a.start.localeCompare(b.start));
out.push({
date,
weekday: weekday(date),
week: week ? { n: week.n, color: week.color } : null,
lessons: dayLessons,
});
}
const weekly = [1, 2, 3, 4, 5, 6, 7]
.map((day) => ({
day,
lessons: entries
.filter((e) => e.lesson.day === day && !e.lesson.date)
.map((e) => lessonOut(e.lesson, group, e, null, "yes", calendar))
.sort((a, b) => a.start.localeCompare(b.start)),
}))
.filter((d) => d.lessons.length);
return {
group,
document: {
id: file.id,
name: file.name,
faculty: file.faculty || schedule.source.faculty || "",
course: file.course || schedule.source.course || "",
modified: file.modified || schedule.source.modified || null,
},
period: schedule.period,
semester: view?.semester || session,
note: view || session
? null
: "Семестр этого расписания не найден в таблице недель: даты не рассчитаны, смотрите строки Word (weekly).",
subgroups: subgroupsOf(schedule, group),
bells: calendar?.bells || [],
from,
days: out,
weekly,
notes: schedule.notes,
};
}