feat: enhance lesson plan handling with new parser and color-coded room assignments
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
weekday,
|
||||
weekIndexOf,
|
||||
} from "../shared/weeks.js";
|
||||
import { planFor as planForWeek } from "../shared/lessons.js";
|
||||
|
||||
let index = null,
|
||||
building = null;
|
||||
@@ -87,10 +88,8 @@ export function peopleAndRooms(lesson, year) {
|
||||
};
|
||||
}
|
||||
|
||||
const inWeeks = (ranges, n) => !ranges || (n != null && ranges.some(([a, b]) => n >= a && (b === null || n <= b)));
|
||||
/** The lesson's plan entries for a week `{n, color}`: by its number, its colour or both. */
|
||||
const planFor = (entry, week) =>
|
||||
week && entry.plan?.length ? entry.plan.filter((p) => inWeeks(p.weeks, week.n) && (!p.color || p.color === week.color)) : [];
|
||||
const planFor = (entry, week) => planForWeek(entry.plan, week);
|
||||
|
||||
/**
|
||||
* Rooms a lesson uses on a date: a dated move wins, then the room for that week ("1-9н. 502н",
|
||||
|
||||
@@ -10,7 +10,7 @@ import { parseWeekRanges, parseTextDates, DATE_LIST, weekday } from "../shared/w
|
||||
import { roomMoves, onlyPlace, weekPlan, partsByWeek } from "./rooms.js";
|
||||
import { placeNote, lessonPlace, placeOf } from "./places.js";
|
||||
const run = promisify(execFile);
|
||||
export const PARSER_VERSION = 14;
|
||||
export const PARSER_VERSION = 15;
|
||||
const days = [
|
||||
"понедельник",
|
||||
"вторник",
|
||||
|
||||
@@ -51,8 +51,9 @@ export function roomMoves(text, year) {
|
||||
return { moves, rest };
|
||||
}
|
||||
|
||||
// A week mark opening a part of a line: "1-9н.", "с 10н", "14-17н.", "7,9,11н.".
|
||||
const WEEK_MARK = /(?<=^|[\s,;])(?:(?:с|со)\s*)?\d{1,2}(?:\s*[-–,]\s*\d{1,2})*\s*н(?:ед)?(?![а-яё])\.?/giu;
|
||||
// A week mark opening a part of a line: "1-9н.", "с 10н", "14-17н.", "7,9,11н.". Not a room
|
||||
// with its week's colour: "32н-з.н." is room 32Н in green weeks.
|
||||
const WEEK_MARK = /(?<=^|[\s,;])(?:(?:с|со)\s*)?\d{1,2}(?:\s*[-–,]\s*\d{1,2})*\s*н(?:ед)?(?![а-яё])(?!\.?\s*[-–]?\s*[бз](?:ел)?\.?\s*н)\.?/giu;
|
||||
|
||||
/** A line cut where each week mark begins: "1-13н-332, 14-17н.-502н" → two parts. */
|
||||
export function partsByWeek(text) {
|
||||
@@ -63,6 +64,8 @@ export function partsByWeek(text) {
|
||||
const PART_MARK = /^(?:(?:с|со)\s*)?\d{1,2}(?:\s*[-–,]\s*\d{1,2})*\s*н(?:ед)?\.?/iu;
|
||||
// "бел.н.504н/зел.н.524н", "б.н.301Г/з.н.317Д": a room for white weeks and one for green ones.
|
||||
const BY_COLOR = /б(?:ел)?\.?\s*н(?:ед)?\.?\s*([^/\s]+)\s*\/\s*з(?:ел)?\.?\s*н(?:ед)?\.?\s*([^\s,;]+)/iu;
|
||||
// "ауд.533н-б.н.,420н-з.н.": the colour after each room.
|
||||
const ROOM_COLOR = new RegExp(`(${ROOM_TEXT})\\s*[-–]?\\s*(б|з)(?:ел)?\\.?\\s*н(?:ед)?\\.?(?![а-яё])`, "giu");
|
||||
|
||||
const roomsIn = (text) =>
|
||||
tokens(text.replace(PERSON, " ").replace(/\([^)]*\)/g, " "))
|
||||
@@ -83,9 +86,15 @@ export function weekPlan(text) {
|
||||
const rest = part.slice(mark.length);
|
||||
const teachers = [...rest.matchAll(PERSON)].map((m) => personName(m[0]));
|
||||
const color = rest.match(BY_COLOR);
|
||||
const byRoom = [...rest.matchAll(ROOM_COLOR)];
|
||||
if (color) {
|
||||
plan.push({ weeks, color: "white", rooms: roomsIn(color[1]), teachers });
|
||||
plan.push({ weeks, color: "green", rooms: roomsIn(color[2]), teachers });
|
||||
} else if (byRoom.length) {
|
||||
for (const [name, letter] of [["white", "б"], ["green", "з"]]) {
|
||||
const rooms = byRoom.filter((m) => m[2].toLowerCase() === letter).map((m) => roomKey(m[1]));
|
||||
if (rooms.length) plan.push({ weeks, color: name, rooms, teachers });
|
||||
}
|
||||
} else if (mark) {
|
||||
const rooms = roomsIn(rest);
|
||||
if (rooms.length || teachers.length) plan.push({ weeks, color: null, rooms, teachers });
|
||||
@@ -106,6 +115,7 @@ export function onlyPlace(text) {
|
||||
const left = (text || "")
|
||||
.replace(PERSON, " ")
|
||||
.replace(BY_COLOR, " ")
|
||||
.replace(ROOM_COLOR, " ")
|
||||
.replace(/(?:(?:с|со)\s*)?\d{1,2}(?:\s*[-–,]\s*\d{1,2})*\s*н(?:ед)?\.?/giu, " ")
|
||||
.replace(/ауд\.?|а\./giu, " ")
|
||||
.replace(/[А-ЯA-Z]\d-\d{2}|\d{2,4}\s?[А-Яа-я]{0,2}(?![а-яё])/gu, " ");
|
||||
|
||||
@@ -4,6 +4,8 @@ import { request, MIN_REFRESH } from "./source.js";
|
||||
import { DATA } from "./paths.js";
|
||||
|
||||
const data = DATA + "parsed/";
|
||||
// The downloaded Word files: a new parser reads them again without asking the university.
|
||||
const originals = DATA + "documents/";
|
||||
// When each document was last downloaded, and the downloads under way: a forced refresh
|
||||
// downloads a document at most once per MIN_REFRESH, and simultaneous ones share a download.
|
||||
const fetchedAt = new Map(),
|
||||
@@ -16,11 +18,12 @@ export async function scheduleFor(file, { refresh = false } = {}) {
|
||||
if (!refresh)
|
||||
try {
|
||||
const cached = JSON.parse(await readFile(cache, "utf8"));
|
||||
if (
|
||||
cached.schemaVersion === PARSER_VERSION &&
|
||||
cached.source.modified === file.modified
|
||||
)
|
||||
return cached;
|
||||
if (cached.source.modified === file.modified) {
|
||||
if (cached.schemaVersion === PARSER_VERSION) return cached;
|
||||
// The same file read by an older parser: parse the saved copy again.
|
||||
const saved = await readFile(originals + file.id).catch(() => null);
|
||||
if (saved) return parse(file, saved, cache);
|
||||
}
|
||||
} catch {}
|
||||
if (!loading.has(file.id))
|
||||
loading.set(
|
||||
@@ -33,6 +36,12 @@ export async function scheduleFor(file, { refresh = false } = {}) {
|
||||
async function download(file, cache) {
|
||||
const buffer = await request(file.path);
|
||||
fetchedAt.set(file.id, Date.now());
|
||||
await mkdir(originals, { recursive: true });
|
||||
await writeFile(originals + file.id, buffer);
|
||||
return parse(file, buffer, cache);
|
||||
}
|
||||
|
||||
async function parse(file, buffer, cache) {
|
||||
const parsed = await parseWord(buffer, {
|
||||
name: file.name,
|
||||
url: file.url,
|
||||
|
||||
Reference in New Issue
Block a user