chuvsu-js 2.8.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/browser.d.ts +1 -5
- package/dist/browser.js +1 -2
- package/dist/index.d.ts +1 -8
- package/dist/index.js +2 -3
- package/dist/shared.d.ts +9 -0
- package/dist/shared.js +7 -0
- package/dist/tt/client.d.ts +1 -0
- package/dist/tt/client.js +25 -16
- package/dist/tt/parse/audience.d.ts +3 -0
- package/dist/tt/parse/audience.js +150 -0
- package/dist/tt/parse/full-schedule.d.ts +4 -0
- package/dist/tt/parse/full-schedule.js +190 -0
- package/dist/tt/parse/groups.d.ts +15 -0
- package/dist/tt/parse/groups.js +28 -0
- package/dist/tt/parse/index.d.ts +5 -0
- package/dist/tt/parse/index.js +5 -0
- package/dist/tt/parse/lists.d.ts +11 -0
- package/dist/tt/parse/lists.js +80 -0
- package/dist/tt/parse/overlays.d.ts +10 -0
- package/dist/tt/parse/overlays.js +104 -0
- package/dist/tt/parse/teacher.d.ts +4 -0
- package/dist/tt/parse/teacher.js +166 -0
- package/dist/tt/schedule.d.ts +1 -2
- package/dist/tt/schedule.js +2 -8
- package/dist/tt/types.d.ts +10 -4
- package/dist/tt/utils/date.d.ts +5 -0
- package/dist/tt/utils/date.js +27 -0
- package/dist/tt/{utils.d.ts → utils/holidays.d.ts} +5 -55
- package/dist/tt/utils/holidays.js +197 -0
- package/dist/tt/utils/index.d.ts +7 -0
- package/dist/tt/utils/index.js +6 -0
- package/dist/tt/utils/lessons.d.ts +14 -0
- package/dist/tt/utils/lessons.js +123 -0
- package/dist/tt/utils/period.d.ts +6 -0
- package/dist/tt/utils/period.js +24 -0
- package/dist/tt/utils/semester.d.ts +25 -0
- package/dist/tt/utils/semester.js +47 -0
- package/dist/tt/utils/time-slots.d.ts +5 -0
- package/dist/tt/utils/time-slots.js +41 -0
- package/package.json +1 -1
- package/dist/tt/parse.d.ts +0 -16
- package/dist/tt/parse.js +0 -671
- package/dist/tt/utils.js +0 -521
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { parseTeacher, text } from "../../common/parse.js";
|
|
2
|
+
import { parseGroupsString } from "./groups.js";
|
|
3
|
+
export function parseDate(dd, mm, yyyy) {
|
|
4
|
+
return new Date(parseInt(yyyy), parseInt(mm) - 1, parseInt(dd));
|
|
5
|
+
}
|
|
6
|
+
export function parseTransferDiv(div) {
|
|
7
|
+
const divText = text(div);
|
|
8
|
+
const divHtml = div.innerHTML ?? "";
|
|
9
|
+
const m = divText.match(/(\d{2})\.(\d{2})\.(\d{4})\s*перенос\s*c\s*(\d{2})\.(\d{2})\.(\d{4})\s*\((\d+)\s*пара\)/);
|
|
10
|
+
if (!m)
|
|
11
|
+
return null;
|
|
12
|
+
const targetDate = parseDate(m[1], m[2], m[3]);
|
|
13
|
+
const fromDate = parseDate(m[4], m[5], m[6]);
|
|
14
|
+
const fromSlot = parseInt(m[7]);
|
|
15
|
+
const subjectEl = div.querySelector('span[style*="color: blue"]');
|
|
16
|
+
const subject = subjectEl ? text(subjectEl) : "";
|
|
17
|
+
if (!subject)
|
|
18
|
+
return null;
|
|
19
|
+
const roomMatch = divHtml.match(/([А-Яа-яA-Za-z]-\d+)/);
|
|
20
|
+
const typeMatch = divText.match(/\((лк|пр|лб|зач|экз|зчО|кр|конс)\)/);
|
|
21
|
+
// Teacher: last text line that isn't a subgroup marker
|
|
22
|
+
const parts = divHtml.split(/<br\s*\/?>/);
|
|
23
|
+
let teacherPart = "";
|
|
24
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
25
|
+
const clean = parts[i].replace(/<[^>]*>/g, "").trim();
|
|
26
|
+
if (clean && !/подгруппа/.test(clean)) {
|
|
27
|
+
teacherPart = clean;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const transfer = { targetDate, fromDate, fromSlot, subject };
|
|
32
|
+
const subgroupMatch = divText.match(/(\d+)\s*подгруппа/);
|
|
33
|
+
return {
|
|
34
|
+
transfer,
|
|
35
|
+
entry: {
|
|
36
|
+
room: roomMatch?.[1] ?? "",
|
|
37
|
+
subject,
|
|
38
|
+
type: typeMatch?.[1] ?? "",
|
|
39
|
+
weeks: { from: 0, to: 0 },
|
|
40
|
+
teacher: parseTeacher(teacherPart),
|
|
41
|
+
groups: [],
|
|
42
|
+
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
43
|
+
transfer,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function parseSubstitutionDiv(div) {
|
|
48
|
+
const divText = text(div);
|
|
49
|
+
const divHtml = div.innerHTML ?? "";
|
|
50
|
+
const m = divText.match(/(\d{2})\.(\d{2})\.(\d{4})\s*замена\s*на:/);
|
|
51
|
+
if (!m)
|
|
52
|
+
return null;
|
|
53
|
+
const date = parseDate(m[1], m[2], m[3]);
|
|
54
|
+
let room;
|
|
55
|
+
let teacher;
|
|
56
|
+
const roomMatch = divHtml.match(/Аудитория:\s*<span[^>]*>([^<]+)<\/span>/);
|
|
57
|
+
if (roomMatch)
|
|
58
|
+
room = roomMatch[1].trim();
|
|
59
|
+
const teacherMatch = divHtml.match(/Преподаватель:\s*<span[^>]*>([^<]+)<\/span>/);
|
|
60
|
+
if (teacherMatch)
|
|
61
|
+
teacher = parseTeacher(teacherMatch[1].trim());
|
|
62
|
+
return { date, room, teacher };
|
|
63
|
+
}
|
|
64
|
+
export function parseSubstituteForDiv(div) {
|
|
65
|
+
const divText = text(div);
|
|
66
|
+
const divHtml = div.innerHTML ?? "";
|
|
67
|
+
const m = divText.match(/(\d{2})\.(\d{2})\.(\d{4})\s*замена\s*вместо:/);
|
|
68
|
+
if (!m)
|
|
69
|
+
return null;
|
|
70
|
+
const date = parseDate(m[1], m[2], m[3]);
|
|
71
|
+
// Original teacher: first blue span (right after "замена вместо:")
|
|
72
|
+
const origTeacherMatch = divHtml.match(/замена\s*вместо:\s*<\/b><\/span>\s*<span[^>]*>([^<]+)<\/span>/);
|
|
73
|
+
const originalTeacher = origTeacherMatch
|
|
74
|
+
? parseTeacher(origTeacherMatch[1].trim())
|
|
75
|
+
: { name: "" };
|
|
76
|
+
// Subject: second blue span
|
|
77
|
+
const subjectEl = div.querySelectorAll('span[style*="color: blue"]');
|
|
78
|
+
let subject = "";
|
|
79
|
+
for (const el of subjectEl) {
|
|
80
|
+
const t = text(el);
|
|
81
|
+
if (t && t !== origTeacherMatch?.[1]?.trim()) {
|
|
82
|
+
subject = t;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!subject)
|
|
87
|
+
return null;
|
|
88
|
+
const roomMatch = divHtml.match(/(?:<br\s*\/?>)\s*([А-Яа-яA-Za-z]-\d+)/);
|
|
89
|
+
const typeMatch = divText.match(/\((лк|пр|лб|зач|экз|зчО|кр|конс)\)/);
|
|
90
|
+
const groupsMatch = divHtml.match(/\((?:лк|пр|лб|зач|экз|зчО|кр|конс)\)\s*(?:<br\s*\/?>)\s*([^<]+?)(?:\s*<i|$)/);
|
|
91
|
+
const subgroupMatch = divText.match(/(\d+)\s*подгруппа/);
|
|
92
|
+
return {
|
|
93
|
+
entry: {
|
|
94
|
+
room: roomMatch?.[1] ?? "",
|
|
95
|
+
subject,
|
|
96
|
+
type: typeMatch?.[1] ?? "",
|
|
97
|
+
weeks: { from: 0, to: 0 },
|
|
98
|
+
teacher: { name: "" },
|
|
99
|
+
groups: parseGroupsString(groupsMatch?.[1]),
|
|
100
|
+
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
101
|
+
substituteFor: { date, originalTeacher },
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { EducationType } from "../../common/types.js";
|
|
2
|
+
import type { FullScheduleDay, TeacherInfo } from "../types.js";
|
|
3
|
+
export declare function parseTeacherFullSchedule(html: string, educationType?: EducationType): FullScheduleDay[];
|
|
4
|
+
export declare function parseTeacherInfo(html: string): TeacherInfo | null;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { parseHtml, parseTime, parseWeekParity, parseWeeks, text, } from "../../common/parse.js";
|
|
2
|
+
import { getLessonNumber } from "../utils/index.js";
|
|
3
|
+
import { parseSemesterScheduleWith } from "./full-schedule.js";
|
|
4
|
+
import { parseGroupsString } from "./groups.js";
|
|
5
|
+
import { parseSubstituteForDiv, parseSubstitutionDiv, parseTransferDiv, } from "./overlays.js";
|
|
6
|
+
export function parseTeacherFullSchedule(html, educationType) {
|
|
7
|
+
const doc = parseHtml(html);
|
|
8
|
+
const edType = educationType ?? 1 /* EducationType.HigherEducation */;
|
|
9
|
+
if (doc.querySelector('td[id^="trd2"]')) {
|
|
10
|
+
return parseTeacherSessionSchedule(doc, edType);
|
|
11
|
+
}
|
|
12
|
+
return parseSemesterScheduleWith(doc, parseTeacherSemesterEntry);
|
|
13
|
+
}
|
|
14
|
+
function parseTeacherSemesterEntry(el) {
|
|
15
|
+
const td = el.querySelector("td") ?? el;
|
|
16
|
+
const fullHtml = td.innerHTML ?? "";
|
|
17
|
+
const plainText = text(td);
|
|
18
|
+
if (!plainText)
|
|
19
|
+
return null;
|
|
20
|
+
const possibleChanges = (td.getAttribute("class") ?? "").includes("want") || undefined;
|
|
21
|
+
const redDivs = td.querySelectorAll('div[style*="border: 2px solid red"]');
|
|
22
|
+
for (const div of redDivs) {
|
|
23
|
+
const result = parseTransferDiv(div);
|
|
24
|
+
if (result) {
|
|
25
|
+
if (possibleChanges)
|
|
26
|
+
result.entry.possibleChanges = true;
|
|
27
|
+
return result.entry;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// "замена вместо:" (substitute lesson for another teacher)
|
|
31
|
+
for (const div of redDivs) {
|
|
32
|
+
const result = parseSubstituteForDiv(div);
|
|
33
|
+
if (result) {
|
|
34
|
+
if (possibleChanges)
|
|
35
|
+
result.entry.possibleChanges = true;
|
|
36
|
+
return result.entry;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const substitutions = [];
|
|
40
|
+
for (const div of redDivs) {
|
|
41
|
+
const sub = parseSubstitutionDiv(div);
|
|
42
|
+
if (sub)
|
|
43
|
+
substitutions.push(sub);
|
|
44
|
+
}
|
|
45
|
+
let cleanHtml = fullHtml;
|
|
46
|
+
let cleanText = plainText;
|
|
47
|
+
for (const div of redDivs) {
|
|
48
|
+
cleanHtml = cleanHtml.replace(div.outerHTML ?? "", "");
|
|
49
|
+
cleanText = cleanText.replace(text(div), "");
|
|
50
|
+
}
|
|
51
|
+
const subjectEl = td.querySelector('span[style*="color: blue"]');
|
|
52
|
+
const subject = subjectEl ? text(subjectEl) : "";
|
|
53
|
+
if (!subject)
|
|
54
|
+
return null;
|
|
55
|
+
const typeMatch = cleanText.match(/\((лк|пр|лб|зач|экз|зчО|кр|конс)\)/);
|
|
56
|
+
const weeksMatch = cleanText.match(/\(([^)]*нед\.?[^)]*)\)/);
|
|
57
|
+
const roomMatch = cleanHtml.match(/(?:<sup>[^<]*<\/sup>)?([А-Яа-яA-Za-z]-\d+)/);
|
|
58
|
+
const groupsMatch = cleanHtml.match(/<br\s*\/?>\s*([^<]+?)(?:<br|<\/td|<div|<i|$)/);
|
|
59
|
+
const subgroupMatch = cleanText.match(/(\d+)\s*подгруппа/);
|
|
60
|
+
const weekParity = parseWeekParity(cleanHtml);
|
|
61
|
+
return {
|
|
62
|
+
room: roomMatch?.[1] ?? "",
|
|
63
|
+
subject,
|
|
64
|
+
type: typeMatch?.[1] ?? "",
|
|
65
|
+
weeks: parseWeeks(weeksMatch?.[1] ?? ""),
|
|
66
|
+
teacher: { name: "" },
|
|
67
|
+
groups: parseGroupsString(groupsMatch?.[1]),
|
|
68
|
+
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
69
|
+
weekParity,
|
|
70
|
+
substitutions: substitutions.length > 0 ? substitutions : undefined,
|
|
71
|
+
possibleChanges,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function parseTeacherSessionSchedule(doc, educationType) {
|
|
75
|
+
const days = [];
|
|
76
|
+
for (const dateCell of doc.querySelectorAll('td[id^="trd2"]')) {
|
|
77
|
+
const id = dateCell.getAttribute("id") ?? "";
|
|
78
|
+
const dateMatch = id.match(/trd(\d{4})(\d{2})(\d{2})/);
|
|
79
|
+
if (!dateMatch)
|
|
80
|
+
continue;
|
|
81
|
+
const year = parseInt(dateMatch[1]);
|
|
82
|
+
const month = parseInt(dateMatch[2]) - 1;
|
|
83
|
+
const dayNum = parseInt(dateMatch[3]);
|
|
84
|
+
const date = new Date(year, month, dayNum);
|
|
85
|
+
const cellHtml = dateCell.innerHTML ?? "";
|
|
86
|
+
const brMatch = cellHtml.match(/<br\s*\/?>\s*(.+)/i);
|
|
87
|
+
const weekday = brMatch ? brMatch[1].trim() : "";
|
|
88
|
+
const row = dateCell.parentElement;
|
|
89
|
+
if (!row)
|
|
90
|
+
continue;
|
|
91
|
+
const dataCell = row.querySelector("td.trdata:not(.trfd)");
|
|
92
|
+
if (!dataCell)
|
|
93
|
+
continue;
|
|
94
|
+
const slots = [];
|
|
95
|
+
for (const entryRow of dataCell.querySelectorAll("table tr")) {
|
|
96
|
+
const td = entryRow.querySelector("td") ?? entryRow;
|
|
97
|
+
const entry = parseTeacherSessionEntry(td);
|
|
98
|
+
if (!entry)
|
|
99
|
+
continue;
|
|
100
|
+
slots.push({
|
|
101
|
+
number: getLessonNumber(entry.timeStart, educationType),
|
|
102
|
+
timeStart: entry.timeStart,
|
|
103
|
+
timeEnd: entry.timeEnd,
|
|
104
|
+
entries: [entry.entry],
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (slots.length > 0) {
|
|
108
|
+
days.push({ weekday, date, slots });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return days;
|
|
112
|
+
}
|
|
113
|
+
function parseTeacherSessionEntry(td) {
|
|
114
|
+
const fullHtml = td.innerHTML ?? "";
|
|
115
|
+
const plainText = text(td);
|
|
116
|
+
if (!plainText)
|
|
117
|
+
return null;
|
|
118
|
+
const subjectEl = td.querySelector('span[style*="color: blue"]');
|
|
119
|
+
const subject = subjectEl ? text(subjectEl) : "";
|
|
120
|
+
if (!subject)
|
|
121
|
+
return null;
|
|
122
|
+
const roomMatch = fullHtml.match(/^([^<]*?)\s*<span/);
|
|
123
|
+
const room = roomMatch ? roomMatch[1].trim() : "";
|
|
124
|
+
const typeMatch = plainText.match(/\((лк|пр|лб|зач|экз|зчО|кр|конс\.?|Экз)\)/i);
|
|
125
|
+
const type = typeMatch ? typeMatch[1].replace(/\.$/, "").toLowerCase() : "";
|
|
126
|
+
// Groups: text between </span> type and <br>time
|
|
127
|
+
const groupsMatch = fullHtml.match(/\((?:лк|пр|лб|зач|экз|зчО|кр|конс\.?|Экз)\)\s*([^<]+?)\s*<br/i);
|
|
128
|
+
const timeMatch = fullHtml.match(/<br\s*\/?>\s*(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/);
|
|
129
|
+
if (!timeMatch)
|
|
130
|
+
return null;
|
|
131
|
+
return {
|
|
132
|
+
entry: {
|
|
133
|
+
room,
|
|
134
|
+
subject,
|
|
135
|
+
type,
|
|
136
|
+
weeks: { from: 0, to: 0 },
|
|
137
|
+
teacher: { name: "" },
|
|
138
|
+
groups: parseGroupsString(groupsMatch?.[1]),
|
|
139
|
+
},
|
|
140
|
+
timeStart: parseTime(timeMatch[1]),
|
|
141
|
+
timeEnd: parseTime(timeMatch[2]),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
export function parseTeacherInfo(html) {
|
|
145
|
+
const doc = parseHtml(html);
|
|
146
|
+
const nameEl = doc.querySelector(".htextb");
|
|
147
|
+
if (!nameEl)
|
|
148
|
+
return null;
|
|
149
|
+
const nameHtml = nameEl.innerHTML ?? "";
|
|
150
|
+
const nameMatch = nameHtml.match(/^([^<]+)/);
|
|
151
|
+
const name = nameMatch?.[1]?.trim() ?? "";
|
|
152
|
+
if (!name)
|
|
153
|
+
return null;
|
|
154
|
+
const degreeEl = nameEl.querySelector('span[style*="color: blue"]');
|
|
155
|
+
const degree = degreeEl ? text(degreeEl).trim() : undefined;
|
|
156
|
+
const deptEl = doc.querySelector(".htext");
|
|
157
|
+
const department = deptEl ? text(deptEl).trim() : undefined;
|
|
158
|
+
const photoImg = doc.querySelector("#photosrc");
|
|
159
|
+
const photoUrl = photoImg?.getAttribute("src") || undefined;
|
|
160
|
+
return {
|
|
161
|
+
name,
|
|
162
|
+
degree: degree || undefined,
|
|
163
|
+
department: department || undefined,
|
|
164
|
+
photoUrl,
|
|
165
|
+
};
|
|
166
|
+
}
|
package/dist/tt/schedule.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FullScheduleDay, SemesterWeek, Lesson } from "./types.js";
|
|
2
2
|
import { Period, EducationType } from "../common/types.js";
|
|
3
|
-
import { type Holiday, type HolidayTransfer } from "./utils.js";
|
|
3
|
+
import { type Holiday, type HolidayTransfer } from "./utils/index.js";
|
|
4
4
|
export declare class Schedule {
|
|
5
5
|
readonly groupId: number;
|
|
6
6
|
readonly scheduleMap: Map<number, FullScheduleDay[]>;
|
|
@@ -23,7 +23,6 @@ export declare class Schedule {
|
|
|
23
23
|
getDays(period: Period): FullScheduleDay[];
|
|
24
24
|
private getSlotsForWeekday;
|
|
25
25
|
private getDateForWeekday;
|
|
26
|
-
private static isSameDay;
|
|
27
26
|
forDay(weekday: number, opts?: {
|
|
28
27
|
subgroup?: number;
|
|
29
28
|
week?: number;
|
package/dist/tt/schedule.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { collectTransfers, filterSlots, getAdjacentSemester, getCurrentPeriod, getMonday, getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, isHoliday, isSameDay, isSessionPeriod, RUSSIAN_HOLIDAYS, slotsToLessons, sortLessons, suppressTransferredLessons, } from "./utils/index.js";
|
|
2
2
|
export class Schedule {
|
|
3
3
|
groupId;
|
|
4
4
|
scheduleMap;
|
|
@@ -60,12 +60,6 @@ export class Schedule {
|
|
|
60
60
|
date.setHours(0, 0, 0, 0);
|
|
61
61
|
return date;
|
|
62
62
|
}
|
|
63
|
-
// --- Session helpers (date-based) ---
|
|
64
|
-
static isSameDay(a, b) {
|
|
65
|
-
return (a.getFullYear() === b.getFullYear() &&
|
|
66
|
-
a.getMonth() === b.getMonth() &&
|
|
67
|
-
a.getDate() === b.getDate());
|
|
68
|
-
}
|
|
69
63
|
// --- Public query methods ---
|
|
70
64
|
forDay(weekday, opts) {
|
|
71
65
|
const period = this.period;
|
|
@@ -93,7 +87,7 @@ export class Schedule {
|
|
|
93
87
|
// 1. Check all periods for date-based (session) entries matching this date
|
|
94
88
|
for (const [, d] of this.scheduleMap) {
|
|
95
89
|
for (const day of d) {
|
|
96
|
-
if (day.date &&
|
|
90
|
+
if (day.date && isSameDay(day.date, date)) {
|
|
97
91
|
lessons.push(...slotsToLessons(day.slots, date, { isTeacherSchedule: this.isTeacherSchedule }));
|
|
98
92
|
}
|
|
99
93
|
}
|
package/dist/tt/types.d.ts
CHANGED
|
@@ -68,8 +68,13 @@ export interface ScheduleEntry {
|
|
|
68
68
|
type: string;
|
|
69
69
|
weeks: WeekRange;
|
|
70
70
|
teacher: Teacher;
|
|
71
|
-
/**
|
|
72
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Group names for this lesson (e.g. `["КТ-42-25 (АихС)", "КТ-41-25"]`).
|
|
73
|
+
* Parenthesized annotations that are part of the group name are preserved;
|
|
74
|
+
* service markers like "(N подгруппа)" are stripped and moved to {@link ScheduleEntry.subgroup}.
|
|
75
|
+
* Empty array if no groups are listed.
|
|
76
|
+
*/
|
|
77
|
+
groups: string[];
|
|
73
78
|
subgroup?: number;
|
|
74
79
|
weekParity?: "even" | "odd";
|
|
75
80
|
/** Date-specific substitutions (замена на). */
|
|
@@ -110,8 +115,8 @@ export interface Lesson {
|
|
|
110
115
|
type: string;
|
|
111
116
|
room: string;
|
|
112
117
|
teacher: Teacher;
|
|
113
|
-
/**
|
|
114
|
-
groups
|
|
118
|
+
/** Group names for this lesson. See {@link ScheduleEntry.groups}. */
|
|
119
|
+
groups: string[];
|
|
115
120
|
weeks: WeekRange;
|
|
116
121
|
subgroup?: number;
|
|
117
122
|
weekParity?: "even" | "odd";
|
|
@@ -147,6 +152,7 @@ export interface CacheConfig {
|
|
|
147
152
|
teacherInfo?: number;
|
|
148
153
|
teacherPhotos?: number;
|
|
149
154
|
audienceInfo?: number;
|
|
155
|
+
audienceImages?: number;
|
|
150
156
|
}
|
|
151
157
|
export interface TtClientOptions {
|
|
152
158
|
educationType?: EducationType;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function getWeekdayName(weekday: number): string;
|
|
2
|
+
/** Monday of the week containing `date` (at 00:00 local time). */
|
|
3
|
+
export declare function getMonday(date: Date): Date;
|
|
4
|
+
/** True if `a` and `b` fall on the same calendar day (local time). */
|
|
5
|
+
export declare function isSameDay(a: Date, b: Date): boolean;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const WEEKDAY_NAMES = [
|
|
2
|
+
"Воскресенье",
|
|
3
|
+
"Понедельник",
|
|
4
|
+
"Вторник",
|
|
5
|
+
"Среда",
|
|
6
|
+
"Четверг",
|
|
7
|
+
"Пятница",
|
|
8
|
+
"Суббота",
|
|
9
|
+
];
|
|
10
|
+
export function getWeekdayName(weekday) {
|
|
11
|
+
return WEEKDAY_NAMES[weekday] ?? "";
|
|
12
|
+
}
|
|
13
|
+
/** Monday of the week containing `date` (at 00:00 local time). */
|
|
14
|
+
export function getMonday(date) {
|
|
15
|
+
const d = new Date(date);
|
|
16
|
+
const day = d.getDay();
|
|
17
|
+
const diff = day === 0 ? -6 : 1 - day;
|
|
18
|
+
d.setDate(d.getDate() + diff);
|
|
19
|
+
d.setHours(0, 0, 0, 0);
|
|
20
|
+
return d;
|
|
21
|
+
}
|
|
22
|
+
/** True if `a` and `b` fall on the same calendar day (local time). */
|
|
23
|
+
export function isSameDay(a, b) {
|
|
24
|
+
return (a.getFullYear() === b.getFullYear() &&
|
|
25
|
+
a.getMonth() === b.getMonth() &&
|
|
26
|
+
a.getDate() === b.getDate());
|
|
27
|
+
}
|
|
@@ -1,55 +1,5 @@
|
|
|
1
|
-
import type { FullScheduleDay, FullScheduleSlot, ScheduleEntry, SemesterWeek, Lesson, LessonTimeSlot } from "./types.js";
|
|
2
|
-
import { Period, EducationType } from "../common/types.js";
|
|
3
|
-
import type { Time } from "../common/types.js";
|
|
4
|
-
export declare function getCurrentPeriod(opts?: {
|
|
5
|
-
date?: Date;
|
|
6
|
-
}): Period;
|
|
7
|
-
export declare function isSessionPeriod(period: Period): boolean;
|
|
8
|
-
export declare function sortLessons(a: Lesson, b: Lesson): number;
|
|
9
|
-
export declare function getWeekdayName(weekday: number): string;
|
|
10
|
-
export declare function getMonday(date: Date): Date;
|
|
11
|
-
/**
|
|
12
|
-
* Get the start date of a semester.
|
|
13
|
-
* Fall: September 1 of the given year.
|
|
14
|
-
* Spring: first Monday of February of the given year.
|
|
15
|
-
*/
|
|
16
|
-
export declare function getSemesterStart(opts: {
|
|
17
|
-
period: Period;
|
|
18
|
-
year?: number;
|
|
19
|
-
}): Date;
|
|
20
|
-
/**
|
|
21
|
-
* Get all weeks in a semester with their start/end dates.
|
|
22
|
-
* Week 0 starts from the semester start date.
|
|
23
|
-
*/
|
|
24
|
-
export declare function getSemesterWeeks(opts: {
|
|
25
|
-
period: Period;
|
|
26
|
-
year?: number;
|
|
27
|
-
weekCount?: number;
|
|
28
|
-
}): SemesterWeek[];
|
|
29
|
-
/**
|
|
30
|
-
* Get the current week number within a semester.
|
|
31
|
-
*/
|
|
32
|
-
export declare function getWeekNumber(opts: {
|
|
33
|
-
period: Period;
|
|
34
|
-
date?: Date;
|
|
35
|
-
}): number;
|
|
36
|
-
export declare function filterSlots(slots: FullScheduleSlot[], opts?: {
|
|
37
|
-
subgroup?: number;
|
|
38
|
-
week?: number;
|
|
39
|
-
date?: Date;
|
|
40
|
-
}): FullScheduleSlot[];
|
|
41
|
-
export declare function slotsToLessons(slots: FullScheduleSlot[], date: Date, opts?: {
|
|
42
|
-
isTeacherSchedule?: boolean;
|
|
43
|
-
}): Lesson[];
|
|
44
|
-
/** Collect all transfer entries from schedule days. */
|
|
45
|
-
export declare function collectTransfers(days: FullScheduleDay[]): ScheduleEntry[];
|
|
46
|
-
/** Remove lessons whose source date/slot match a transfer. */
|
|
47
|
-
export declare function suppressTransferredLessons(lessons: Lesson[], transfers: ScheduleEntry[], date: Date): Lesson[];
|
|
48
|
-
export declare function getTimeSlots(educationType: EducationType): LessonTimeSlot[];
|
|
49
|
-
export declare function getLessonNumber(time: Time, educationType: EducationType): number;
|
|
50
|
-
export declare function getAdjacentSemester(session: Period): Period;
|
|
51
1
|
export interface Holiday {
|
|
52
|
-
/** Month number, 1
|
|
2
|
+
/** Month number, 1-12. */
|
|
53
3
|
month: number;
|
|
54
4
|
/** Day of month. */
|
|
55
5
|
day: number;
|
|
@@ -75,7 +25,7 @@ export declare const RUSSIAN_HOLIDAYS: Holiday[];
|
|
|
75
25
|
* 1. All holidays in the list are non-working days.
|
|
76
26
|
* 2. For non-January holidays: if a holiday falls on Sat/Sun, the day off
|
|
77
27
|
* automatically transfers to the next working day.
|
|
78
|
-
* 3. For January holidays (1
|
|
28
|
+
* 3. For January holidays (1-8): weekend transfers are NOT automatic —
|
|
79
29
|
* they are decided by annual government decree. Pass them via `transfers`.
|
|
80
30
|
* 4. Bridge days: if a non-January holiday falls on Tue, Mon is day off
|
|
81
31
|
* (preceding Sat works); if on Thu, Fri is day off (following Sat works).
|
|
@@ -87,17 +37,17 @@ export declare const RUSSIAN_HOLIDAYS: Holiday[];
|
|
|
87
37
|
*/
|
|
88
38
|
export declare function getEffectiveHolidays(year: number, holidays?: Holiday[], transfers?: HolidayTransfer[], sixDayWeek?: boolean): Date[];
|
|
89
39
|
/**
|
|
90
|
-
*
|
|
40
|
+
* All transfers (auto bridges + explicit) for a given year.
|
|
91
41
|
* Useful for getting compensating work days (Saturdays that become working).
|
|
92
42
|
*/
|
|
93
43
|
export declare function getHolidayTransfers(year: number, holidays: Holiday[] | undefined, transfers: HolidayTransfer[] | undefined, sixDayWeek: boolean): HolidayTransfer[];
|
|
94
44
|
/**
|
|
95
|
-
*
|
|
45
|
+
* List of compensating work days (e.g. Saturdays that become working).
|
|
96
46
|
* Includes both auto-computed bridge compensations and explicit transfers.
|
|
97
47
|
*/
|
|
98
48
|
export declare function getCompensatingWorkDays(year: number, holidays: Holiday[] | undefined, transfers: HolidayTransfer[] | undefined, sixDayWeek: boolean): Date[];
|
|
99
49
|
/**
|
|
100
|
-
*
|
|
50
|
+
* True if the given date is a non-working holiday,
|
|
101
51
|
* including transferred holidays when they fall on weekends (Art. 112 ТК РФ)
|
|
102
52
|
* and auto-computed bridge days.
|
|
103
53
|
* Pass an empty array for `holidays` to disable holiday checking.
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { isSameDay } from "./date.js";
|
|
2
|
+
/** Russian non-working public holidays (Статья 112 ТК РФ). */
|
|
3
|
+
export const RUSSIAN_HOLIDAYS = [
|
|
4
|
+
{ month: 1, day: 1, name: "Новый год" },
|
|
5
|
+
{ month: 1, day: 2, name: "Новогодние каникулы" },
|
|
6
|
+
{ month: 1, day: 3, name: "Новогодние каникулы" },
|
|
7
|
+
{ month: 1, day: 4, name: "Новогодние каникулы" },
|
|
8
|
+
{ month: 1, day: 5, name: "Новогодние каникулы" },
|
|
9
|
+
{ month: 1, day: 6, name: "Новогодние каникулы" },
|
|
10
|
+
{ month: 1, day: 7, name: "Рождество Христово" },
|
|
11
|
+
{ month: 1, day: 8, name: "Новогодние каникулы" },
|
|
12
|
+
{ month: 2, day: 23, name: "День защитника Отечества" },
|
|
13
|
+
{ month: 3, day: 8, name: "Международный женский день" },
|
|
14
|
+
{ month: 5, day: 1, name: "Праздник Весны и Труда" },
|
|
15
|
+
{ month: 5, day: 9, name: "День Победы" },
|
|
16
|
+
{ month: 6, day: 12, name: "День России" },
|
|
17
|
+
{ month: 11, day: 4, name: "День народного единства" },
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* January holiday dates (1-8) are excluded from automatic weekend transfer
|
|
21
|
+
* per Art. 112 ТК РФ. Their transfers are decided by government decree.
|
|
22
|
+
*/
|
|
23
|
+
function isJanuaryHoliday(h) {
|
|
24
|
+
return h.month === 1 && h.day >= 1 && h.day <= 8;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Bridge-day transfers for non-January holidays.
|
|
28
|
+
*
|
|
29
|
+
* Pattern (consistent across government decrees):
|
|
30
|
+
* - Holiday on Tuesday -> Monday becomes day off, preceding Saturday is work day
|
|
31
|
+
* - Holiday on Thursday -> Friday becomes day off, following Saturday is work day
|
|
32
|
+
* (only for 5-day week; 6-day week has Saturday classes, so no gap to bridge)
|
|
33
|
+
*
|
|
34
|
+
* January holidays are excluded (their 2 transfers are unpredictable).
|
|
35
|
+
*/
|
|
36
|
+
function computeBridgeDays(year, holidays, effectiveDays, sixDayWeek) {
|
|
37
|
+
const bridges = [];
|
|
38
|
+
for (const h of holidays) {
|
|
39
|
+
if (isJanuaryHoliday(h))
|
|
40
|
+
continue;
|
|
41
|
+
const date = new Date(year, h.month - 1, h.day);
|
|
42
|
+
const dow = date.getDay();
|
|
43
|
+
if (dow === 2) {
|
|
44
|
+
// Tuesday -> Monday off, preceding Saturday works
|
|
45
|
+
const monday = new Date(date);
|
|
46
|
+
monday.setDate(date.getDate() - 1);
|
|
47
|
+
const saturday = new Date(date);
|
|
48
|
+
saturday.setDate(date.getDate() - 3);
|
|
49
|
+
if (!effectiveDays.some((d) => isSameDay(d, monday))) {
|
|
50
|
+
bridges.push({
|
|
51
|
+
dayOff: monday,
|
|
52
|
+
workDay: sixDayWeek ? null : saturday,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if (dow === 4 && !sixDayWeek) {
|
|
57
|
+
// Thursday -> Friday off, following Saturday works
|
|
58
|
+
// Only for 5-day week: 6-day week has no gap (Saturday is a work day)
|
|
59
|
+
const friday = new Date(date);
|
|
60
|
+
friday.setDate(date.getDate() + 1);
|
|
61
|
+
const saturday = new Date(date);
|
|
62
|
+
saturday.setDate(date.getDate() + 2);
|
|
63
|
+
if (!effectiveDays.some((d) => isSameDay(d, friday))) {
|
|
64
|
+
bridges.push({ dayOff: friday, workDay: saturday });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return bridges;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Compute effective non-working holiday dates for a given year.
|
|
72
|
+
*
|
|
73
|
+
* Rules (Art. 112 ТК РФ):
|
|
74
|
+
* 1. All holidays in the list are non-working days.
|
|
75
|
+
* 2. For non-January holidays: if a holiday falls on Sat/Sun, the day off
|
|
76
|
+
* automatically transfers to the next working day.
|
|
77
|
+
* 3. For January holidays (1-8): weekend transfers are NOT automatic —
|
|
78
|
+
* they are decided by annual government decree. Pass them via `transfers`.
|
|
79
|
+
* 4. Bridge days: if a non-January holiday falls on Tue, Mon is day off
|
|
80
|
+
* (preceding Sat works); if on Thu, Fri is day off (following Sat works).
|
|
81
|
+
* Computed automatically, can be overridden via `transfers`.
|
|
82
|
+
* 5. Government decree transfers (`transfers`) add extra days off and
|
|
83
|
+
* override auto-computed bridge days.
|
|
84
|
+
* 6. For 6-day week (`sixDayWeek`): Saturday is a work day, so
|
|
85
|
+
* Thursday bridges don't apply and Saturday holidays don't auto-transfer.
|
|
86
|
+
*/
|
|
87
|
+
export function getEffectiveHolidays(year, holidays = RUSSIAN_HOLIDAYS, transfers = [], sixDayWeek = true) {
|
|
88
|
+
const originalDates = holidays.map((h) => new Date(year, h.month - 1, h.day));
|
|
89
|
+
const isOriginalHoliday = (d) => originalDates.some((od) => isSameDay(od, d));
|
|
90
|
+
const effectiveDays = [...originalDates];
|
|
91
|
+
// Auto-transfer: only non-January holidays that fall on weekends
|
|
92
|
+
const nonJanuaryOnWeekend = holidays
|
|
93
|
+
.filter((h) => !isJanuaryHoliday(h))
|
|
94
|
+
.map((h) => new Date(year, h.month - 1, h.day))
|
|
95
|
+
.filter((d) => d.getDay() === 0 || d.getDay() === 6)
|
|
96
|
+
.sort((a, b) => a.getTime() - b.getTime());
|
|
97
|
+
for (const holiday of nonJanuaryOnWeekend) {
|
|
98
|
+
let candidate = new Date(holiday);
|
|
99
|
+
if (candidate.getDay() === 6) {
|
|
100
|
+
candidate.setDate(candidate.getDate() + 2);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
candidate.setDate(candidate.getDate() + 1);
|
|
104
|
+
}
|
|
105
|
+
while (candidate.getDay() === 0 ||
|
|
106
|
+
candidate.getDay() === 6 ||
|
|
107
|
+
isOriginalHoliday(candidate) ||
|
|
108
|
+
effectiveDays.some((ed) => isSameDay(ed, candidate))) {
|
|
109
|
+
candidate.setDate(candidate.getDate() + 1);
|
|
110
|
+
}
|
|
111
|
+
effectiveDays.push(new Date(candidate));
|
|
112
|
+
}
|
|
113
|
+
// Bridge days (auto-computed, can be overridden)
|
|
114
|
+
const autoBridges = computeBridgeDays(year, holidays, effectiveDays, sixDayWeek);
|
|
115
|
+
// Merge: explicit transfers override auto-computed bridges
|
|
116
|
+
const allTransfers = [...autoBridges];
|
|
117
|
+
for (const t of transfers) {
|
|
118
|
+
const idx = allTransfers.findIndex((b) => isSameDay(b.dayOff, t.dayOff));
|
|
119
|
+
if (idx !== -1) {
|
|
120
|
+
allTransfers[idx] = t;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
allTransfers.push(t);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const t of allTransfers) {
|
|
127
|
+
if (!effectiveDays.some((d) => isSameDay(d, t.dayOff))) {
|
|
128
|
+
effectiveDays.push(t.dayOff);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return effectiveDays.sort((a, b) => a.getTime() - b.getTime());
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* All transfers (auto bridges + explicit) for a given year.
|
|
135
|
+
* Useful for getting compensating work days (Saturdays that become working).
|
|
136
|
+
*/
|
|
137
|
+
export function getHolidayTransfers(year, holidays = RUSSIAN_HOLIDAYS, transfers = [], sixDayWeek) {
|
|
138
|
+
const originalDates = holidays.map((h) => new Date(year, h.month - 1, h.day));
|
|
139
|
+
const effectiveDays = [...originalDates];
|
|
140
|
+
// Replay weekend transfers to build effectiveDays for bridge computation
|
|
141
|
+
const nonJanuaryOnWeekend = holidays
|
|
142
|
+
.filter((h) => !isJanuaryHoliday(h))
|
|
143
|
+
.map((h) => new Date(year, h.month - 1, h.day))
|
|
144
|
+
.filter((d) => d.getDay() === 0 || d.getDay() === 6)
|
|
145
|
+
.sort((a, b) => a.getTime() - b.getTime());
|
|
146
|
+
const isOriginalHoliday = (d) => originalDates.some((od) => isSameDay(od, d));
|
|
147
|
+
for (const holiday of nonJanuaryOnWeekend) {
|
|
148
|
+
let candidate = new Date(holiday);
|
|
149
|
+
if (candidate.getDay() === 6) {
|
|
150
|
+
candidate.setDate(candidate.getDate() + 2);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
candidate.setDate(candidate.getDate() + 1);
|
|
154
|
+
}
|
|
155
|
+
while (candidate.getDay() === 0 ||
|
|
156
|
+
candidate.getDay() === 6 ||
|
|
157
|
+
isOriginalHoliday(candidate) ||
|
|
158
|
+
effectiveDays.some((ed) => isSameDay(ed, candidate))) {
|
|
159
|
+
candidate.setDate(candidate.getDate() + 1);
|
|
160
|
+
}
|
|
161
|
+
effectiveDays.push(new Date(candidate));
|
|
162
|
+
}
|
|
163
|
+
const autoBridges = computeBridgeDays(year, holidays, effectiveDays, sixDayWeek);
|
|
164
|
+
const allTransfers = [...autoBridges];
|
|
165
|
+
for (const t of transfers) {
|
|
166
|
+
const idx = allTransfers.findIndex((b) => isSameDay(b.dayOff, t.dayOff));
|
|
167
|
+
if (idx !== -1) {
|
|
168
|
+
allTransfers[idx] = t;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
allTransfers.push(t);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return allTransfers;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* List of compensating work days (e.g. Saturdays that become working).
|
|
178
|
+
* Includes both auto-computed bridge compensations and explicit transfers.
|
|
179
|
+
*/
|
|
180
|
+
export function getCompensatingWorkDays(year, holidays = RUSSIAN_HOLIDAYS, transfers = [], sixDayWeek) {
|
|
181
|
+
return getHolidayTransfers(year, holidays, transfers, sixDayWeek)
|
|
182
|
+
.filter((t) => t.workDay != null)
|
|
183
|
+
.map((t) => t.workDay);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* True if the given date is a non-working holiday,
|
|
187
|
+
* including transferred holidays when they fall on weekends (Art. 112 ТК РФ)
|
|
188
|
+
* and auto-computed bridge days.
|
|
189
|
+
* Pass an empty array for `holidays` to disable holiday checking.
|
|
190
|
+
*/
|
|
191
|
+
export function isHoliday(date, holidays = RUSSIAN_HOLIDAYS, transfers = [], sixDayWeek = true) {
|
|
192
|
+
if (holidays.length === 0 && transfers.length === 0)
|
|
193
|
+
return false;
|
|
194
|
+
const year = date.getFullYear();
|
|
195
|
+
const effectiveDays = getEffectiveHolidays(year, holidays, transfers, sixDayWeek);
|
|
196
|
+
return effectiveDays.some((d) => isSameDay(d, date));
|
|
197
|
+
}
|