Files
psu-schedule/server/places.js
Zakhar Timoshenko 297091cb0e feat: Add Settings and Subjects screens with theme reveal functionality
- Implemented SettingsScreen for user preferences including theme selection, reminders, and notifications.
- Created SubjectsScreen to display subjects with upcoming lessons and associated details.
- Introduced ThemeReveal for a smooth transition between light and dark themes.
- Added support for managing notes and tasks related to subjects.
- Enhanced UI components for better user experience and accessibility.
2026-10-24 02:47:37 +03:00

89 lines
4.1 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.
// Towns of a lesson, as Word cells name them. Most lessons are in Novopolotsk; some faculties
// teach in Polotsk, and a cell says when a lesson or a whole day is in the other town:
// "Занятия в Полоцке" over a day's first row (that day, the column's lessons are in Polotsk),
// "Новополоцк" or "г.Новополоцк" on a line of its own, "246(Новополоцк)" after a room.
// Towns a cell may name without "г.": a bare capitalised word is otherwise a subject.
const TOWNS = "Новополоцк|Полоцк|Междуречье|Витебск|Минск";
const TOWN = String.raw`(?:г\.\s*([А-ЯЁ][а-яё]+(?:-[А-ЯЁ][а-яё]+)?)|(${TOWNS})е?)`;
// "Занятия в Полоцке", "Занятия проходят в г. Новополоцк": the rest of the line, if any, is a lesson.
const NOTE = new RegExp(
String.raw`^[Зз]анятия\s+(?:(?:проход[яи]т|будут\s+проходить)\s+)?в\s+(?:г\.\s*)?([А-ЯЁ][а-яё]+(?:-[А-ЯЁ][а-яё]+)?)(?=$|[\s.,;:!–—-])[.!]?`,
"u",
);
// A line that starts with the town: "г.Новополоцк", or "г. Полоцк, Дом офицеров" with the venue.
const LINE = new RegExp(String.raw`^\(?${TOWN}\)?(?:[.,]?$|(,\s*\S.*))`, "u");
// "246(Новополоцк)", "121 (г.Новополоцк)".
const AFTER_ROOM = new RegExp(String.raw`(?<=\d[А-ЯЁа-яё]{0,2})\s*\(${TOWN}\)`, "u");
/** "Полоцке" → "Полоцк", "Витебске" → "Витебск"; "Междуречье" stays. */
export const townName = (word) => word.replace(/(?<=(?:к|ст))е$/u, "");
/**
* A note that a day is in another town, on a line of the cell: `{text, place, rest}`, the
* cell without the note in `rest`; null when the cell has none.
*/
export function placeNote(text) {
const lines = (text || "").split("\n");
for (const [i, line] of lines.entries()) {
const m = line.trim().match(NOTE);
if (!m) continue;
const rest = [...lines.slice(0, i), line.trim().slice(m[0].length).trim(), ...lines.slice(i + 1)]
.filter((l) => l.trim())
.join("\n");
return { text: m[0].replace(/[.!]$/, "").replace(/\s+/g, " "), place: townName(m[1]), rest };
}
return null;
}
/** The town a lesson's own text names, and the text without it: `{place, rest}`. */
export function lessonPlace(text) {
let place = null;
const rest = (text || "")
.split("\n")
.filter((line) => {
const m = line.trim().match(LINE);
if (m && !place) place = townName(m[1] || m[2]);
// The town alone goes; a venue after it stays in the text.
return !m || m[3];
})
.join("\n")
.replace(AFTER_ROOM, (_, named, town) => {
place ||= townName(named || town);
return "";
});
return { place, rest };
}
/**
* The town of a lesson from the notes of its day: for every group of the lesson, a note for its
* subgroup or the whole group, or — for a lesson of the whole group — notes in every subgroup
* that name the same town. `labelsOf(group)`: the group's subgroup labels.
*/
export function placeOf(lesson, places, labelsOf) {
let found = null;
for (const group of lesson.groups) {
const notes = places.filter(
(p) =>
p.day === lesson.day &&
(!p.date || p.date === lesson.date) &&
p.groups.includes(group) &&
// A note from the middle of the day holds from its row on.
(!p.from || lesson.start >= p.from),
);
if (!notes.length) return null;
const label = lesson.subgroups.find((s) => s.group === group)?.label;
const own = (p) => p.subgroups.filter((s) => s.group === group).map((s) => s.label);
let place = notes.find((p) => !own(p).length || (label && own(p).includes(label)))?.place;
if (!place && !label) {
const towns = new Map(notes.flatMap((p) => own(p).map((l) => [l, p.place])));
const labels = labelsOf(group);
if (labels.length && labels.every((l) => towns.has(l)) && new Set(towns.values()).size === 1)
place = towns.get(labels[0]);
}
if (!place || (found && found !== place)) return null;
found = place;
}
return found;
}