chuvsu-js 4.1.5 → 5.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 +165 -205
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +1 -1
- package/dist/common/cache.js +30 -9
- package/dist/common/http.d.ts +6 -0
- package/dist/common/http.js +8 -1
- package/dist/common/parse.d.ts +1 -1
- package/dist/common/parse.js +3 -3
- package/dist/common/types.d.ts +8 -2
- package/dist/common/types.js +12 -0
- package/dist/core.d.ts +10 -0
- package/dist/core.js +5 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -4
- package/dist/lk/client.d.ts +8 -6
- package/dist/lk/client.js +51 -23
- package/dist/lk/types.d.ts +7 -7
- package/dist/node.d.ts +1 -0
- package/dist/node.js +1 -0
- package/dist/parsers.d.ts +3 -0
- package/dist/parsers.js +2 -0
- package/dist/tt/client.d.ts +55 -63
- package/dist/tt/client.js +477 -226
- package/dist/tt/domain/directory.d.ts +14 -0
- package/dist/tt/domain/directory.js +116 -0
- package/dist/tt/domain/ids.d.ts +7 -0
- package/dist/tt/domain/ids.js +20 -0
- package/dist/tt/domain/index.d.ts +7 -0
- package/dist/tt/domain/index.js +5 -0
- package/dist/tt/domain/normalize.d.ts +7 -0
- package/dist/tt/domain/normalize.js +65 -0
- package/dist/tt/domain/repository.d.ts +51 -0
- package/dist/tt/domain/repository.js +805 -0
- package/dist/tt/domain/schedule.d.ts +37 -0
- package/dist/tt/domain/schedule.js +292 -0
- package/dist/tt/domain/types.d.ts +175 -0
- package/dist/tt/domain/types.js +1 -0
- package/dist/tt/observations.d.ts +11 -0
- package/dist/tt/observations.js +131 -0
- package/dist/tt/parse/audience.d.ts +3 -3
- package/dist/tt/parse/audience.js +58 -34
- package/dist/tt/parse/entry-parts.d.ts +8 -0
- package/dist/tt/parse/entry-parts.js +52 -0
- package/dist/tt/parse/full-schedule.d.ts +9 -4
- package/dist/tt/parse/full-schedule.js +49 -51
- package/dist/tt/parse/groups.d.ts +1 -1
- package/dist/tt/parse/groups.js +1 -1
- package/dist/tt/parse/index.d.ts +4 -4
- package/dist/tt/parse/index.js +4 -4
- package/dist/tt/parse/lists.d.ts +6 -5
- package/dist/tt/parse/lists.js +7 -2
- package/dist/tt/parse/overlays.d.ts +5 -4
- package/dist/tt/parse/overlays.js +13 -14
- package/dist/tt/parse/patterns.d.ts +2 -2
- package/dist/tt/parse/patterns.js +3 -3
- package/dist/tt/parse/teacher.d.ts +2 -3
- package/dist/tt/parse/teacher.js +21 -65
- package/dist/tt/parse/webinars.js +3 -4
- package/dist/tt/types.d.ts +56 -69
- package/dist/tt/utils/date.d.ts +4 -0
- package/dist/tt/utils/date.js +30 -1
- package/dist/tt/utils/index.d.ts +2 -3
- package/dist/tt/utils/index.js +2 -3
- package/dist/tt/utils/period.d.ts +4 -4
- package/dist/tt/utils/period.js +9 -8
- package/dist/tt/utils/semester.d.ts +4 -4
- package/dist/tt/utils/semester.js +25 -5
- package/dist/tt/utils/time-slots.d.ts +3 -5
- package/dist/tt/utils/time-slots.js +19 -34
- package/dist/tt/webinars.d.ts +7 -0
- package/dist/tt/webinars.js +38 -0
- package/docs/fixture-review.md +56 -0
- package/docs/testing.md +23 -0
- package/docs/v5-architecture.md +144 -0
- package/docs/v5-migration.md +451 -0
- package/package.json +19 -7
- package/dist/shared.d.ts +0 -9
- package/dist/shared.js +0 -7
- package/dist/tt/schedule.d.ts +0 -55
- package/dist/tt/schedule.js +0 -233
- package/dist/tt/utils/lessons.d.ts +0 -16
- package/dist/tt/utils/lessons.js +0 -173
package/dist/tt/parse/teacher.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { parseHtml, parseTime, parseWeekParity, parseWeeks, text, } from "../../common/parse.js";
|
|
2
|
-
import {
|
|
3
|
-
import { parseSemesterScheduleWith } from "./full-schedule.js";
|
|
2
|
+
import { parseSemesterScheduleWith, parseSessionScheduleWith, } from "./full-schedule.js";
|
|
4
3
|
import { parseGroupsString } from "./groups.js";
|
|
5
4
|
import { parseSubstituteForDiv, parseSubstitutionDiv, parseTransferDiv, } from "./overlays.js";
|
|
6
5
|
import { FLEXIBLE_LESSON_TYPE_RE_I, LESSON_TYPE_RE, SUBGROUP_RE, WEEKS_RE, } from "./patterns.js";
|
|
7
|
-
|
|
8
|
-
export function
|
|
6
|
+
import { containsGroupCode, hasDistanceMarker, linesAfterSubject, parseEntryRoom, stripDistanceMarker, } from "./entry-parts.js";
|
|
7
|
+
export function parseTeacherSchedule(html) {
|
|
9
8
|
const doc = parseHtml(html);
|
|
10
|
-
const edType = educationType ?? 1 /* EducationType.HigherEducation */;
|
|
11
9
|
if (doc.querySelector('td[id^="trd2"]')) {
|
|
12
|
-
return
|
|
10
|
+
return parseSessionScheduleWith(doc, parseTeacherSessionEntry);
|
|
13
11
|
}
|
|
14
12
|
return parseSemesterScheduleWith(doc, parseTeacherSemesterEntry);
|
|
15
13
|
}
|
|
@@ -56,61 +54,24 @@ function parseTeacherSemesterEntry(el) {
|
|
|
56
54
|
return null;
|
|
57
55
|
const typeMatch = cleanText.match(LESSON_TYPE_RE);
|
|
58
56
|
const weeksMatch = cleanText.match(WEEKS_RE);
|
|
59
|
-
const
|
|
60
|
-
const
|
|
57
|
+
const room = parseEntryRoom(cleanHtml, subject);
|
|
58
|
+
const groupsLine = linesAfterSubject(cleanHtml, subject).find((line) => containsGroupCode(stripDistanceMarker(line))) ?? "";
|
|
61
59
|
const subgroupMatch = cleanText.match(SUBGROUP_RE);
|
|
62
60
|
const weekParity = parseWeekParity(cleanHtml);
|
|
61
|
+
const groups = parseGroupsString(stripDistanceMarker(groupsLine));
|
|
63
62
|
return {
|
|
64
|
-
room:
|
|
63
|
+
room: room || undefined,
|
|
65
64
|
subject,
|
|
66
65
|
type: typeMatch?.[1] ?? "",
|
|
67
66
|
weeks: parseWeeks(weeksMatch?.[1] ?? ""),
|
|
68
|
-
|
|
69
|
-
groups: parseGroupsString(groupsMatch?.[1]),
|
|
67
|
+
groups: groups.length > 0 ? groups : undefined,
|
|
70
68
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
71
69
|
weekParity,
|
|
72
|
-
isDistance:
|
|
70
|
+
isDistance: hasDistanceMarker(cleanText) || hasDistanceMarker(room ?? ""),
|
|
73
71
|
substitutions: substitutions.length > 0 ? substitutions : undefined,
|
|
74
72
|
possibleChanges,
|
|
75
73
|
};
|
|
76
74
|
}
|
|
77
|
-
function parseTeacherSessionSchedule(doc, educationType) {
|
|
78
|
-
const days = [];
|
|
79
|
-
for (const dateCell of doc.querySelectorAll('td[id^="trd2"]')) {
|
|
80
|
-
const id = dateCell.getAttribute("id") ?? "";
|
|
81
|
-
const dateMatch = id.match(/trd(\d{4})(\d{2})(\d{2})/);
|
|
82
|
-
if (!dateMatch)
|
|
83
|
-
continue;
|
|
84
|
-
const year = parseInt(dateMatch[1]);
|
|
85
|
-
const month = parseInt(dateMatch[2]) - 1;
|
|
86
|
-
const dayNum = parseInt(dateMatch[3]);
|
|
87
|
-
const date = new Date(year, month, dayNum);
|
|
88
|
-
const cellHtml = dateCell.innerHTML ?? "";
|
|
89
|
-
const brMatch = cellHtml.match(/<br\s*\/?>\s*(.+)/i);
|
|
90
|
-
const weekday = brMatch ? brMatch[1].trim() : "";
|
|
91
|
-
const dataCell = dateCell.nextElementSibling;
|
|
92
|
-
if (!dataCell?.matches("td.trdata:not(.trfd)")) {
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
const slots = [];
|
|
96
|
-
for (const entryRow of dataCell.querySelectorAll("table tr")) {
|
|
97
|
-
const td = entryRow.querySelector("td") ?? entryRow;
|
|
98
|
-
const entry = parseTeacherSessionEntry(td);
|
|
99
|
-
if (!entry)
|
|
100
|
-
continue;
|
|
101
|
-
slots.push({
|
|
102
|
-
number: getLessonNumber(entry.timeStart, educationType),
|
|
103
|
-
timeStart: entry.timeStart,
|
|
104
|
-
timeEnd: entry.timeEnd,
|
|
105
|
-
entries: [entry.entry],
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
if (slots.length > 0) {
|
|
109
|
-
days.push({ weekday, date, slots });
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return days;
|
|
113
|
-
}
|
|
114
75
|
function parseTeacherSessionEntry(td) {
|
|
115
76
|
const fullHtml = td.innerHTML ?? "";
|
|
116
77
|
const plainText = text(td);
|
|
@@ -121,34 +82,29 @@ function parseTeacherSessionEntry(td) {
|
|
|
121
82
|
const subject = subjectEl ? text(subjectEl) : "";
|
|
122
83
|
if (!subject)
|
|
123
84
|
return null;
|
|
124
|
-
const
|
|
125
|
-
const room = roomMatch ? roomMatch[1].trim() : "";
|
|
85
|
+
const room = parseEntryRoom(fullHtml, subject);
|
|
126
86
|
const typeMatch = plainText.match(FLEXIBLE_LESSON_TYPE_RE_I);
|
|
127
87
|
const type = typeMatch ? typeMatch[1].replace(/\.$/, "").toLowerCase() : "";
|
|
128
88
|
const subgroupMatch = plainText.match(SUBGROUP_RE);
|
|
129
89
|
const timeMatch = fullHtml.match(/<br\s*\/?>\s*(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/);
|
|
130
90
|
if (!timeMatch)
|
|
131
91
|
return null;
|
|
132
|
-
const parts = fullHtml
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
!/^\d{2}:\d{2}\s*-\s*\d{2}:\d{2}$/.test(part)) ?? "";
|
|
92
|
+
const parts = linesAfterSubject(fullHtml, subject);
|
|
93
|
+
const groupsPart = parts.find((part) => stripDistanceMarker(part).length > 0 &&
|
|
94
|
+
!/^\d{2}:\d{2}\s*-\s*\d{2}:\d{2}$/.test(part) &&
|
|
95
|
+
containsGroupCode(stripDistanceMarker(part))) ?? "";
|
|
96
|
+
const groups = parseGroupsString(stripDistanceMarker(groupsPart));
|
|
138
97
|
return {
|
|
139
|
-
|
|
140
|
-
room,
|
|
98
|
+
lesson: {
|
|
99
|
+
room: room || undefined,
|
|
141
100
|
subject,
|
|
142
101
|
type,
|
|
143
|
-
|
|
144
|
-
teacher: { name: "" },
|
|
145
|
-
groups: parseGroupsString(groupsPart),
|
|
102
|
+
groups: groups.length > 0 ? groups : undefined,
|
|
146
103
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
147
|
-
isDistance:
|
|
104
|
+
isDistance: hasDistanceMarker(plainText) || hasDistanceMarker(room ?? ""),
|
|
148
105
|
possibleChanges,
|
|
149
106
|
},
|
|
150
|
-
|
|
151
|
-
timeEnd: parseTime(timeMatch[2]),
|
|
107
|
+
time: { start: parseTime(timeMatch[1]), end: parseTime(timeMatch[2]) },
|
|
152
108
|
};
|
|
153
109
|
}
|
|
154
110
|
export function parseTeacherInfo(html) {
|
|
@@ -6,7 +6,7 @@ function parseDateValue(value) {
|
|
|
6
6
|
const match = value?.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
7
7
|
if (!match)
|
|
8
8
|
return undefined;
|
|
9
|
-
return
|
|
9
|
+
return `${match[1]}-${match[2]}-${match[3]}`;
|
|
10
10
|
}
|
|
11
11
|
function parseTimeRange(raw) {
|
|
12
12
|
const match = raw.match(/(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/);
|
|
@@ -71,10 +71,9 @@ function parseWebinarRows(doc, tableSelector, scheduled, fallbackDate) {
|
|
|
71
71
|
id: joinMatch?.[1] ?? "",
|
|
72
72
|
idType: joinMatch ? parseInt(joinMatch[2]) : 1,
|
|
73
73
|
scheduled,
|
|
74
|
-
date,
|
|
74
|
+
scheduledDate: date,
|
|
75
75
|
slotNumber,
|
|
76
|
-
|
|
77
|
-
timeEnd: timeRange.end,
|
|
76
|
+
time: timeRange,
|
|
78
77
|
subject: parsed.subject,
|
|
79
78
|
type: parsed.type,
|
|
80
79
|
teacher: parseTeacher(parsed.teacherRaw),
|
package/dist/tt/types.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { BlobAdapter, CacheAdapter } from "../common/cache.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { TimetableRepositoryAdapter, TimetableRepositorySnapshot } from "./domain/types.js";
|
|
3
|
+
import type { TimetableRepository } from "./domain/repository.js";
|
|
4
|
+
import type { TimeRange, WeekRange, Teacher, EducationLevel, AcademicPeriod, LocalDate } from "../common/types.js";
|
|
3
5
|
export interface Faculty {
|
|
4
6
|
id: number;
|
|
5
7
|
name: string;
|
|
@@ -10,11 +12,11 @@ export interface Group {
|
|
|
10
12
|
specialty?: string;
|
|
11
13
|
profile?: string;
|
|
12
14
|
}
|
|
13
|
-
export interface
|
|
15
|
+
export interface Room {
|
|
14
16
|
id: number;
|
|
15
17
|
name: string;
|
|
16
18
|
}
|
|
17
|
-
export interface
|
|
19
|
+
export interface RoomInfo {
|
|
18
20
|
name: string;
|
|
19
21
|
/** Building letter/name, e.g. "Б". */
|
|
20
22
|
building?: string;
|
|
@@ -39,7 +41,7 @@ export interface AudienceInfo {
|
|
|
39
41
|
/** A date-specific substitution (room and/or teacher change). */
|
|
40
42
|
export interface Substitution {
|
|
41
43
|
/** The date this substitution applies to. */
|
|
42
|
-
date:
|
|
44
|
+
date: LocalDate;
|
|
43
45
|
/** New room, if changed. */
|
|
44
46
|
room?: string;
|
|
45
47
|
/** Whether the substitution moves the lesson online. */
|
|
@@ -50,34 +52,37 @@ export interface Substitution {
|
|
|
50
52
|
/** Info about a lesson this teacher is substituting for another teacher. */
|
|
51
53
|
export interface SubstituteForInfo {
|
|
52
54
|
/** The date this substitute lesson takes place. */
|
|
53
|
-
date:
|
|
55
|
+
date: LocalDate;
|
|
54
56
|
/** The original teacher being replaced. */
|
|
55
|
-
originalTeacher
|
|
57
|
+
originalTeacher?: Teacher;
|
|
56
58
|
}
|
|
57
59
|
/** Info about a lesson transferred from another date/slot. */
|
|
58
60
|
export interface TransferInfo {
|
|
59
61
|
/** Date when this lesson takes place (target). */
|
|
60
|
-
targetDate:
|
|
62
|
+
targetDate: LocalDate;
|
|
61
63
|
/** Original date the lesson was moved from. */
|
|
62
|
-
fromDate:
|
|
64
|
+
fromDate: LocalDate;
|
|
63
65
|
/** Original slot number (пара). */
|
|
64
66
|
fromSlot: number;
|
|
65
67
|
/** Subject name (used to match the source entry). */
|
|
66
68
|
subject: string;
|
|
67
69
|
}
|
|
68
|
-
export interface
|
|
69
|
-
room
|
|
70
|
+
export interface ParsedLesson {
|
|
71
|
+
/** Missing means this source projection does not expose room information. */
|
|
72
|
+
room?: string | null;
|
|
70
73
|
subject: string;
|
|
71
74
|
type: string;
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
/** Recurrence range; absent for concrete session lessons or unknown evidence. */
|
|
76
|
+
weeks?: WeekRange;
|
|
77
|
+
/** Missing means this source projection does not expose teacher information. */
|
|
78
|
+
teacher?: Teacher | null;
|
|
74
79
|
/**
|
|
75
80
|
* Group names for this lesson (e.g. `["КТ-42-25 (АихС)", "КТ-41-25"]`).
|
|
76
81
|
* Parenthesized annotations that are part of the group name are preserved;
|
|
77
|
-
* service markers like "(N подгруппа)" are stripped and moved to {@link
|
|
82
|
+
* service markers like "(N подгруппа)" are stripped and moved to {@link ParsedLesson.subgroup}.
|
|
78
83
|
* Empty array if no groups are listed.
|
|
79
84
|
*/
|
|
80
|
-
groups
|
|
85
|
+
groups?: string[] | null;
|
|
81
86
|
subgroup?: number;
|
|
82
87
|
weekParity?: "even" | "odd";
|
|
83
88
|
/** True when the lesson is explicitly marked as дистанционно / ДОТ. */
|
|
@@ -91,54 +96,23 @@ export interface ScheduleEntry {
|
|
|
91
96
|
/** Whether this entry is marked as potentially changing (class="want"). */
|
|
92
97
|
possibleChanges?: boolean;
|
|
93
98
|
}
|
|
94
|
-
export interface
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
export interface ParsedScheduleBlock {
|
|
100
|
+
/** Portal ordinal claim. It is independent from the time claim. */
|
|
101
|
+
slotNumber?: number;
|
|
102
|
+
/** Missing when the source does not expose a trustworthy time range. */
|
|
103
|
+
time?: TimeRange;
|
|
104
|
+
lessons: ParsedLesson[];
|
|
99
105
|
}
|
|
100
|
-
export interface
|
|
106
|
+
export interface ParsedScheduleDay {
|
|
101
107
|
weekday: string;
|
|
102
|
-
date?:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
export interface LessonTime {
|
|
111
|
-
date: Date;
|
|
112
|
-
hours: number;
|
|
113
|
-
minutes: number;
|
|
114
|
-
}
|
|
115
|
-
export interface Lesson {
|
|
116
|
-
number: number;
|
|
117
|
-
start: LessonTime;
|
|
118
|
-
end: LessonTime;
|
|
119
|
-
subject: string;
|
|
120
|
-
type: string;
|
|
121
|
-
room: string;
|
|
122
|
-
teacher: Teacher;
|
|
123
|
-
/** Group names for this lesson. See {@link ScheduleEntry.groups}. */
|
|
124
|
-
groups: string[];
|
|
125
|
-
weeks: WeekRange;
|
|
126
|
-
subgroup?: number;
|
|
127
|
-
weekParity?: "even" | "odd";
|
|
128
|
-
/** True when the lesson is explicitly marked as дистанционно / ДОТ. */
|
|
129
|
-
isDistance?: boolean;
|
|
130
|
-
/** Matched webinar metadata, if attached by `attachWebinarsToLessons`. */
|
|
131
|
-
webinar?: Webinar;
|
|
132
|
-
/** If a substitution was applied, the original room. */
|
|
133
|
-
originalRoom?: string;
|
|
134
|
-
/** If a substitution was applied, the original teacher. */
|
|
135
|
-
originalTeacher?: Teacher;
|
|
136
|
-
/** Transfer info if this lesson was moved from another date/slot. */
|
|
137
|
-
transfer?: TransferInfo;
|
|
138
|
-
/** If this lesson is a substitute (замена вместо), the original teacher. */
|
|
139
|
-
substituteFor?: SubstituteForInfo;
|
|
140
|
-
/** Whether this lesson is marked as potentially changing. */
|
|
141
|
-
possibleChanges?: boolean;
|
|
108
|
+
date?: LocalDate;
|
|
109
|
+
/** True when portal marks this weekday as a self-study day. */
|
|
110
|
+
isSelfStudyDay?: boolean;
|
|
111
|
+
blocks: ParsedScheduleBlock[];
|
|
112
|
+
}
|
|
113
|
+
export interface StandardScheduleBlock {
|
|
114
|
+
slotNumber: number;
|
|
115
|
+
time: TimeRange;
|
|
142
116
|
}
|
|
143
117
|
/** Teacher info from the schedule page header. */
|
|
144
118
|
export interface TeacherInfo {
|
|
@@ -160,10 +134,9 @@ export interface Webinar {
|
|
|
160
134
|
idType: number;
|
|
161
135
|
/** True for "Вебинары по расписанию"; false for external webinars. */
|
|
162
136
|
scheduled: boolean;
|
|
163
|
-
|
|
137
|
+
scheduledDate?: LocalDate;
|
|
164
138
|
slotNumber?: number;
|
|
165
|
-
|
|
166
|
-
timeEnd: Time;
|
|
139
|
+
time: TimeRange;
|
|
167
140
|
subject: string;
|
|
168
141
|
type: string;
|
|
169
142
|
teacher: Teacher;
|
|
@@ -178,18 +151,32 @@ export interface CacheConfig {
|
|
|
178
151
|
schedule?: number;
|
|
179
152
|
faculties?: number;
|
|
180
153
|
groups?: number;
|
|
181
|
-
|
|
182
|
-
|
|
154
|
+
rooms?: number;
|
|
155
|
+
roomNames?: number;
|
|
183
156
|
teachers?: number;
|
|
184
157
|
teacherInfo?: number;
|
|
185
158
|
teacherPhotos?: number;
|
|
186
|
-
|
|
187
|
-
|
|
159
|
+
roomInfo?: number;
|
|
160
|
+
roomImages?: number;
|
|
188
161
|
webinars?: number;
|
|
189
162
|
}
|
|
190
|
-
export interface
|
|
191
|
-
|
|
163
|
+
export interface TimetableClientOptions {
|
|
164
|
+
educationLevel?: EducationLevel;
|
|
192
165
|
cache?: number | CacheConfig;
|
|
193
166
|
cacheAdapter?: CacheAdapter;
|
|
194
167
|
blobAdapter?: BlobAdapter;
|
|
168
|
+
/** Existing canonical repository, useful for dependency injection/browser handoff. */
|
|
169
|
+
repository?: TimetableRepository;
|
|
170
|
+
/** Persistent canonical identity storage. Independent from the TTL cache. */
|
|
171
|
+
repositoryAdapter?: TimetableRepositoryAdapter;
|
|
172
|
+
}
|
|
173
|
+
export interface GetScheduleOptions {
|
|
174
|
+
periods?: readonly AcademicPeriod[];
|
|
175
|
+
}
|
|
176
|
+
export type EntityResolutionStrategy = "cache-only" | "search";
|
|
177
|
+
export interface DirectoryPreloadOptions {
|
|
178
|
+
teachers?: boolean;
|
|
179
|
+
rooms?: boolean;
|
|
180
|
+
facultyIds?: number[];
|
|
195
181
|
}
|
|
182
|
+
export type { TimetableRepositorySnapshot };
|
package/dist/tt/utils/date.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import type { LocalDate } from "../../common/types.js";
|
|
1
2
|
export declare function getWeekdayName(weekday: number): string;
|
|
2
3
|
/** Monday of the week containing `date` (at 00:00 local time). */
|
|
3
4
|
export declare function getMonday(date: Date): Date;
|
|
4
5
|
/** True if `a` and `b` fall on the same calendar day (local time). */
|
|
5
6
|
export declare function isSameDay(a: Date, b: Date): boolean;
|
|
7
|
+
export declare function formatLocalDate(date: Date): LocalDate;
|
|
8
|
+
export declare function isLocalDate(value: string): value is LocalDate;
|
|
9
|
+
export declare function parseLocalDate(value: LocalDate): Date;
|
package/dist/tt/utils/date.js
CHANGED
|
@@ -8,10 +8,15 @@ const WEEKDAY_NAMES = [
|
|
|
8
8
|
"Суббота",
|
|
9
9
|
];
|
|
10
10
|
export function getWeekdayName(weekday) {
|
|
11
|
-
|
|
11
|
+
if (!Number.isInteger(weekday) || weekday < 0 || weekday > 6) {
|
|
12
|
+
throw new RangeError("Weekday must be an integer from 0 to 6");
|
|
13
|
+
}
|
|
14
|
+
return WEEKDAY_NAMES[weekday];
|
|
12
15
|
}
|
|
13
16
|
/** Monday of the week containing `date` (at 00:00 local time). */
|
|
14
17
|
export function getMonday(date) {
|
|
18
|
+
if (!Number.isFinite(date.getTime()))
|
|
19
|
+
throw new RangeError("Invalid date");
|
|
15
20
|
const d = new Date(date);
|
|
16
21
|
const day = d.getDay();
|
|
17
22
|
const diff = day === 0 ? -6 : 1 - day;
|
|
@@ -25,3 +30,27 @@ export function isSameDay(a, b) {
|
|
|
25
30
|
a.getMonth() === b.getMonth() &&
|
|
26
31
|
a.getDate() === b.getDate());
|
|
27
32
|
}
|
|
33
|
+
export function formatLocalDate(date) {
|
|
34
|
+
const year = date.getFullYear();
|
|
35
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
36
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
37
|
+
return `${year}-${month}-${day}`;
|
|
38
|
+
}
|
|
39
|
+
export function isLocalDate(value) {
|
|
40
|
+
const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/u);
|
|
41
|
+
if (!match)
|
|
42
|
+
return false;
|
|
43
|
+
const year = Number(match[1]);
|
|
44
|
+
const month = Number(match[2]);
|
|
45
|
+
const day = Number(match[3]);
|
|
46
|
+
const date = new Date(year, month - 1, day);
|
|
47
|
+
return date.getFullYear() === year &&
|
|
48
|
+
date.getMonth() === month - 1 &&
|
|
49
|
+
date.getDate() === day;
|
|
50
|
+
}
|
|
51
|
+
export function parseLocalDate(value) {
|
|
52
|
+
if (!isLocalDate(value))
|
|
53
|
+
throw new RangeError(`Invalid local date: ${value}`);
|
|
54
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
55
|
+
return new Date(year, month - 1, day);
|
|
56
|
+
}
|
package/dist/tt/utils/index.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
export { getMonday, getWeekdayName, isSameDay } from "./date.js";
|
|
1
|
+
export { formatLocalDate, getMonday, getWeekdayName, isSameDay, isLocalDate, parseLocalDate, } from "./date.js";
|
|
2
2
|
export { getAdjacentSemester, getCurrentPeriod, isSessionPeriod, } from "./period.js";
|
|
3
3
|
export { getSemesterStart, getSemesterWeeks, getWeekNumber, } from "./semester.js";
|
|
4
|
-
export {
|
|
5
|
-
export { attachWebinarsToLessons, collectTransfers, filterSlots, matchWebinarToLesson, slotsToLessons, sortLessons, suppressTransferredLessons, } from "./lessons.js";
|
|
4
|
+
export { getStandardScheduleBlocks } from "./time-slots.js";
|
|
6
5
|
export { getCompensatingWorkDays, getEffectiveHolidays, getHolidayTransfers, isHoliday, RUSSIAN_HOLIDAYS, } from "./holidays.js";
|
|
7
6
|
export type { Holiday, HolidayTransfer } from "./holidays.js";
|
package/dist/tt/utils/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
export { getMonday, getWeekdayName, isSameDay } from "./date.js";
|
|
1
|
+
export { formatLocalDate, getMonday, getWeekdayName, isSameDay, isLocalDate, parseLocalDate, } from "./date.js";
|
|
2
2
|
export { getAdjacentSemester, getCurrentPeriod, isSessionPeriod, } from "./period.js";
|
|
3
3
|
export { getSemesterStart, getSemesterWeeks, getWeekNumber, } from "./semester.js";
|
|
4
|
-
export {
|
|
5
|
-
export { attachWebinarsToLessons, collectTransfers, filterSlots, matchWebinarToLesson, slotsToLessons, sortLessons, suppressTransferredLessons, } from "./lessons.js";
|
|
4
|
+
export { getStandardScheduleBlocks } from "./time-slots.js";
|
|
6
5
|
export { getCompensatingWorkDays, getEffectiveHolidays, getHolidayTransfers, isHoliday, RUSSIAN_HOLIDAYS, } from "./holidays.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AcademicPeriod } from "../../common/types.js";
|
|
2
2
|
export declare function getCurrentPeriod(opts?: {
|
|
3
3
|
date?: Date;
|
|
4
|
-
}):
|
|
5
|
-
export declare function isSessionPeriod(period:
|
|
6
|
-
export declare function getAdjacentSemester(session:
|
|
4
|
+
}): AcademicPeriod;
|
|
5
|
+
export declare function isSessionPeriod(period: AcademicPeriod): boolean;
|
|
6
|
+
export declare function getAdjacentSemester(session: AcademicPeriod): AcademicPeriod;
|
package/dist/tt/utils/period.js
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
|
+
import { AcademicPeriod } from "../../common/types.js";
|
|
1
2
|
export function getCurrentPeriod(opts) {
|
|
2
3
|
const date = opts?.date ?? new Date();
|
|
3
4
|
const month = date.getMonth();
|
|
4
5
|
const day = date.getDate();
|
|
5
6
|
// Dec 25+ and Jan -> Winter session (зимняя сессия)
|
|
6
7
|
if (month === 0 || (month === 11 && day >= 25))
|
|
7
|
-
return
|
|
8
|
+
return AcademicPeriod.WinterSession;
|
|
8
9
|
// Feb-May -> Spring semester (весенний семестр)
|
|
9
10
|
if (month >= 1 && month <= 4)
|
|
10
|
-
return
|
|
11
|
+
return AcademicPeriod.SpringSemester;
|
|
11
12
|
// Jun-Aug -> Summer session (летняя сессия)
|
|
12
13
|
if (month >= 5 && month <= 7)
|
|
13
|
-
return
|
|
14
|
+
return AcademicPeriod.SummerSession;
|
|
14
15
|
// Sep - Dec 24 -> Fall semester (осенний семестр)
|
|
15
|
-
return
|
|
16
|
+
return AcademicPeriod.FallSemester;
|
|
16
17
|
}
|
|
17
18
|
export function isSessionPeriod(period) {
|
|
18
|
-
return period ===
|
|
19
|
+
return period === AcademicPeriod.WinterSession || period === AcademicPeriod.SummerSession;
|
|
19
20
|
}
|
|
20
21
|
export function getAdjacentSemester(session) {
|
|
21
|
-
return session ===
|
|
22
|
-
?
|
|
23
|
-
:
|
|
22
|
+
return session === AcademicPeriod.WinterSession
|
|
23
|
+
? AcademicPeriod.FallSemester
|
|
24
|
+
: AcademicPeriod.SpringSemester;
|
|
24
25
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AcademicPeriod } from "../../common/types.js";
|
|
2
2
|
import type { SemesterWeek } from "../types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Start date of a semester.
|
|
@@ -8,7 +8,7 @@ import type { SemesterWeek } from "../types.js";
|
|
|
8
8
|
* academic year instead of the calendar year.
|
|
9
9
|
*/
|
|
10
10
|
export declare function getSemesterStart(opts: {
|
|
11
|
-
period:
|
|
11
|
+
period: AcademicPeriod;
|
|
12
12
|
year?: number;
|
|
13
13
|
date?: Date;
|
|
14
14
|
}): Date;
|
|
@@ -17,14 +17,14 @@ export declare function getSemesterStart(opts: {
|
|
|
17
17
|
* Week 1 is the calendar week containing the semester start date.
|
|
18
18
|
*/
|
|
19
19
|
export declare function getSemesterWeeks(opts: {
|
|
20
|
-
period:
|
|
20
|
+
period: AcademicPeriod;
|
|
21
21
|
year?: number;
|
|
22
22
|
date?: Date;
|
|
23
23
|
weekCount?: number;
|
|
24
24
|
}): SemesterWeek[];
|
|
25
25
|
/** Current week number within a semester. */
|
|
26
26
|
export declare function getWeekNumber(opts: {
|
|
27
|
-
period:
|
|
27
|
+
period: AcademicPeriod;
|
|
28
28
|
year?: number;
|
|
29
29
|
date?: Date;
|
|
30
30
|
}): number;
|
|
@@ -1,4 +1,15 @@
|
|
|
1
|
+
import { AcademicPeriod } from "../../common/types.js";
|
|
1
2
|
import { getMonday } from "./date.js";
|
|
3
|
+
function assertSemesterPeriod(period) {
|
|
4
|
+
if (period !== AcademicPeriod.FallSemester &&
|
|
5
|
+
period !== AcademicPeriod.SpringSemester) {
|
|
6
|
+
throw new RangeError("Academic period must be a semester");
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function calendarDayNumber(date) {
|
|
10
|
+
return Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) /
|
|
11
|
+
(24 * 60 * 60 * 1000);
|
|
12
|
+
}
|
|
2
13
|
function getAcademicYearStartYear(date) {
|
|
3
14
|
const year = date.getFullYear();
|
|
4
15
|
const month = date.getMonth();
|
|
@@ -6,11 +17,17 @@ function getAcademicYearStartYear(date) {
|
|
|
6
17
|
return month >= 8 ? year : year - 1;
|
|
7
18
|
}
|
|
8
19
|
function resolveSemesterYear(opts) {
|
|
9
|
-
|
|
20
|
+
assertSemesterPeriod(opts.period);
|
|
21
|
+
if (opts.year != null) {
|
|
22
|
+
if (!Number.isInteger(opts.year))
|
|
23
|
+
throw new RangeError("Invalid semester year");
|
|
10
24
|
return opts.year;
|
|
25
|
+
}
|
|
11
26
|
const baseDate = opts.date ?? new Date();
|
|
27
|
+
if (!Number.isFinite(baseDate.getTime()))
|
|
28
|
+
throw new RangeError("Invalid date");
|
|
12
29
|
const academicYearStart = getAcademicYearStartYear(baseDate);
|
|
13
|
-
return opts.period ===
|
|
30
|
+
return opts.period === AcademicPeriod.FallSemester
|
|
14
31
|
? academicYearStart
|
|
15
32
|
: academicYearStart + 1;
|
|
16
33
|
}
|
|
@@ -23,7 +40,7 @@ function resolveSemesterYear(opts) {
|
|
|
23
40
|
*/
|
|
24
41
|
export function getSemesterStart(opts) {
|
|
25
42
|
const year = resolveSemesterYear(opts);
|
|
26
|
-
if (opts.period ===
|
|
43
|
+
if (opts.period === AcademicPeriod.FallSemester) {
|
|
27
44
|
return new Date(year, 8, 1); // September 1
|
|
28
45
|
}
|
|
29
46
|
// Spring: first Monday of February
|
|
@@ -40,6 +57,9 @@ export function getSemesterStart(opts) {
|
|
|
40
57
|
*/
|
|
41
58
|
export function getSemesterWeeks(opts) {
|
|
42
59
|
const weekCount = opts.weekCount ?? 17;
|
|
60
|
+
if (!Number.isInteger(weekCount) || weekCount < 1) {
|
|
61
|
+
throw new RangeError("Week count must be a positive integer");
|
|
62
|
+
}
|
|
43
63
|
const semesterStart = getSemesterStart(opts);
|
|
44
64
|
const startMonday = getMonday(semesterStart);
|
|
45
65
|
const weeks = [];
|
|
@@ -59,6 +79,6 @@ export function getWeekNumber(opts) {
|
|
|
59
79
|
const semesterStart = getSemesterStart({ ...opts, date });
|
|
60
80
|
const startMonday = getMonday(semesterStart);
|
|
61
81
|
const targetMonday = getMonday(date);
|
|
62
|
-
const diff = targetMonday
|
|
63
|
-
return Math.floor(diff /
|
|
82
|
+
const diff = calendarDayNumber(targetMonday) - calendarDayNumber(startMonday);
|
|
83
|
+
return Math.floor(diff / 7) + 1;
|
|
64
84
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
export declare function getTimeSlots(educationType: EducationType): LessonTimeSlot[];
|
|
5
|
-
export declare function getLessonNumber(time: Time, educationType: EducationType): number;
|
|
1
|
+
import { EducationLevel } from "../../common/types.js";
|
|
2
|
+
import type { StandardScheduleBlock } from "../types.js";
|
|
3
|
+
export declare function getStandardScheduleBlocks(educationLevel: EducationLevel): StandardScheduleBlock[];
|
|
@@ -1,41 +1,26 @@
|
|
|
1
|
+
import { EducationLevel } from "../../common/types.js";
|
|
1
2
|
const VO_TIME_SLOTS = [
|
|
2
|
-
{
|
|
3
|
-
{
|
|
4
|
-
{
|
|
5
|
-
{
|
|
6
|
-
{
|
|
7
|
-
{
|
|
8
|
-
{
|
|
9
|
-
{
|
|
3
|
+
{ slotNumber: 1, time: { start: { hours: 8, minutes: 20 }, end: { hours: 9, minutes: 40 } } },
|
|
4
|
+
{ slotNumber: 2, time: { start: { hours: 9, minutes: 50 }, end: { hours: 11, minutes: 10 } } },
|
|
5
|
+
{ slotNumber: 3, time: { start: { hours: 11, minutes: 40 }, end: { hours: 13, minutes: 0 } } },
|
|
6
|
+
{ slotNumber: 4, time: { start: { hours: 13, minutes: 30 }, end: { hours: 14, minutes: 50 } } },
|
|
7
|
+
{ slotNumber: 5, time: { start: { hours: 15, minutes: 0 }, end: { hours: 16, minutes: 20 } } },
|
|
8
|
+
{ slotNumber: 6, time: { start: { hours: 16, minutes: 40 }, end: { hours: 18, minutes: 0 } } },
|
|
9
|
+
{ slotNumber: 7, time: { start: { hours: 18, minutes: 10 }, end: { hours: 19, minutes: 30 } } },
|
|
10
|
+
{ slotNumber: 8, time: { start: { hours: 19, minutes: 40 }, end: { hours: 21, minutes: 0 } } },
|
|
10
11
|
];
|
|
11
12
|
const SPO_TIME_SLOTS = [
|
|
12
|
-
{
|
|
13
|
-
{
|
|
14
|
-
{
|
|
15
|
-
{
|
|
16
|
-
{
|
|
17
|
-
{
|
|
18
|
-
{
|
|
13
|
+
{ slotNumber: 1, time: { start: { hours: 8, minutes: 10 }, end: { hours: 9, minutes: 40 } } },
|
|
14
|
+
{ slotNumber: 2, time: { start: { hours: 9, minutes: 55 }, end: { hours: 11, minutes: 25 } } },
|
|
15
|
+
{ slotNumber: 3, time: { start: { hours: 11, minutes: 55 }, end: { hours: 13, minutes: 25 } } },
|
|
16
|
+
{ slotNumber: 4, time: { start: { hours: 13, minutes: 40 }, end: { hours: 15, minutes: 10 } } },
|
|
17
|
+
{ slotNumber: 5, time: { start: { hours: 15, minutes: 25 }, end: { hours: 16, minutes: 55 } } },
|
|
18
|
+
{ slotNumber: 6, time: { start: { hours: 17, minutes: 10 }, end: { hours: 18, minutes: 40 } } },
|
|
19
|
+
{ slotNumber: 7, time: { start: { hours: 18, minutes: 55 }, end: { hours: 20, minutes: 25 } } },
|
|
19
20
|
];
|
|
20
|
-
export function
|
|
21
|
-
|
|
21
|
+
export function getStandardScheduleBlocks(educationLevel) {
|
|
22
|
+
const blocks = educationLevel === EducationLevel.VocationalEducation
|
|
22
23
|
? SPO_TIME_SLOTS
|
|
23
24
|
: VO_TIME_SLOTS;
|
|
24
|
-
|
|
25
|
-
function timeToMinutes(t) {
|
|
26
|
-
return t.hours * 60 + t.minutes;
|
|
27
|
-
}
|
|
28
|
-
export function getLessonNumber(time, educationType) {
|
|
29
|
-
const slots = getTimeSlots(educationType);
|
|
30
|
-
const target = timeToMinutes(time);
|
|
31
|
-
let closest = slots[0];
|
|
32
|
-
let minDiff = Math.abs(timeToMinutes(closest.start) - target);
|
|
33
|
-
for (let i = 1; i < slots.length; i++) {
|
|
34
|
-
const diff = Math.abs(timeToMinutes(slots[i].start) - target);
|
|
35
|
-
if (diff < minDiff) {
|
|
36
|
-
minDiff = diff;
|
|
37
|
-
closest = slots[i];
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return closest.number;
|
|
25
|
+
return structuredClone(blocks);
|
|
41
26
|
}
|