chuvsu-js 4.2.0 → 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 -193
- 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 +2 -2
- 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 +47 -15
- package/dist/tt/parse/entry-parts.d.ts +1 -0
- package/dist/tt/parse/entry-parts.js +5 -2
- package/dist/tt/parse/full-schedule.d.ts +9 -4
- package/dist/tt/parse/full-schedule.js +32 -41
- 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/teacher.d.ts +2 -3
- package/dist/tt/parse/teacher.js +14 -56
- package/dist/tt/parse/webinars.js +3 -4
- package/dist/tt/types.d.ts +54 -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 +17 -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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { AcademicPeriod } from "../../common/types.js";
|
|
2
|
+
import { type Holiday, type HolidayTransfer } from "../utils/index.js";
|
|
3
|
+
import { TimetableRepository } from "./repository.js";
|
|
4
|
+
import type { LessonOccurrence, LessonSeries, ScheduleOwner } from "./types.js";
|
|
5
|
+
export interface ScheduleQueryOptions {
|
|
6
|
+
subgroup?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ScheduleWeekdayOptions extends ScheduleQueryOptions {
|
|
9
|
+
week?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface ScheduleOptions {
|
|
12
|
+
period?: AcademicPeriod;
|
|
13
|
+
holidays?: Holiday[] | null;
|
|
14
|
+
holidayTransfers?: HolidayTransfer[];
|
|
15
|
+
}
|
|
16
|
+
export declare class Schedule {
|
|
17
|
+
readonly repository: TimetableRepository;
|
|
18
|
+
readonly owner: ScheduleOwner;
|
|
19
|
+
readonly academicYearStartYear: number;
|
|
20
|
+
readonly period: AcademicPeriod;
|
|
21
|
+
readonly holidays: Holiday[];
|
|
22
|
+
readonly holidayTransfers: HolidayTransfer[];
|
|
23
|
+
constructor(repository: TimetableRepository, owner: ScheduleOwner, academicYearStartYear: number, options?: ScheduleOptions);
|
|
24
|
+
get revision(): number;
|
|
25
|
+
series(): LessonSeries[];
|
|
26
|
+
on(date: Date, options?: ScheduleQueryOptions): LessonOccurrence[];
|
|
27
|
+
week(week?: number, options?: ScheduleQueryOptions): LessonOccurrence[];
|
|
28
|
+
weekday(weekday: number, options?: ScheduleWeekdayOptions): LessonOccurrence[];
|
|
29
|
+
today(options?: ScheduleQueryOptions): LessonOccurrence[];
|
|
30
|
+
tomorrow(options?: ScheduleQueryOptions): LessonOccurrence[];
|
|
31
|
+
thisWeek(options?: ScheduleQueryOptions): LessonOccurrence[];
|
|
32
|
+
current(options?: ScheduleQueryOptions): LessonOccurrence | null;
|
|
33
|
+
getWeekNumber(date?: Date): number;
|
|
34
|
+
getSemesterWeeks(weekCount?: number): import("../types.js").SemesterWeek[];
|
|
35
|
+
getSemesterStart(): Date;
|
|
36
|
+
private isInAcademicYear;
|
|
37
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { AcademicPeriod } from "../../common/types.js";
|
|
2
|
+
import { getAdjacentSemester, getCurrentPeriod, getMonday, getSemesterStart, getSemesterWeeks, getWeekNumber, formatLocalDate, isHoliday, parseLocalDate, RUSSIAN_HOLIDAYS, } from "../utils/index.js";
|
|
3
|
+
import { occurrenceIdForSeries } from "./ids.js";
|
|
4
|
+
import { entityKey, normalizeScheduleText } from "./normalize.js";
|
|
5
|
+
function dateAt(date, time) {
|
|
6
|
+
const value = new Date(date);
|
|
7
|
+
value.setHours(time.hours, time.minutes, 0, 0);
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
function semesterCalendarYear(series) {
|
|
11
|
+
return series.period === AcademicPeriod.FallSemester
|
|
12
|
+
? series.academicYearStartYear
|
|
13
|
+
: series.academicYearStartYear + 1;
|
|
14
|
+
}
|
|
15
|
+
function recurrenceIncludes(series, date) {
|
|
16
|
+
if (date.getDay() !== series.recurrence.weekday)
|
|
17
|
+
return null;
|
|
18
|
+
const week = getWeekNumber({
|
|
19
|
+
period: series.period,
|
|
20
|
+
year: semesterCalendarYear(series),
|
|
21
|
+
date,
|
|
22
|
+
});
|
|
23
|
+
const from = series.recurrence.weeks?.from ?? 1;
|
|
24
|
+
const to = series.recurrence.weeks?.to ?? 17;
|
|
25
|
+
if (week < from || week > to)
|
|
26
|
+
return null;
|
|
27
|
+
if (series.recurrence.parity === "even" && week % 2 !== 0)
|
|
28
|
+
return null;
|
|
29
|
+
if (series.recurrence.parity === "odd" && week % 2 === 0)
|
|
30
|
+
return null;
|
|
31
|
+
return week;
|
|
32
|
+
}
|
|
33
|
+
function attendanceMatchesSubgroup(groups, owner, subgroup) {
|
|
34
|
+
if (subgroup == null)
|
|
35
|
+
return true;
|
|
36
|
+
if (owner.type === "group") {
|
|
37
|
+
const ownerKey = entityKey(owner.group);
|
|
38
|
+
const attendance = groups.filter((value) => entityKey(value.group) === ownerKey);
|
|
39
|
+
return attendance.some((value) => value.subgroup == null || value.subgroup === subgroup);
|
|
40
|
+
}
|
|
41
|
+
return groups.some((value) => value.subgroup == null || value.subgroup === subgroup);
|
|
42
|
+
}
|
|
43
|
+
function occurrenceMatchesOwner(occurrence, owner) {
|
|
44
|
+
if (owner.type === "group") {
|
|
45
|
+
const key = entityKey(owner.group);
|
|
46
|
+
return occurrence.groups.values.some((value) => entityKey(value.group) === key);
|
|
47
|
+
}
|
|
48
|
+
if (owner.type === "teacher") {
|
|
49
|
+
const key = entityKey(owner.teacher);
|
|
50
|
+
return occurrence.teachers.values.some((value) => entityKey(value) === key);
|
|
51
|
+
}
|
|
52
|
+
const key = entityKey(owner.room);
|
|
53
|
+
return occurrence.rooms.values.some((value) => entityKey(value) === key);
|
|
54
|
+
}
|
|
55
|
+
function attendanceOverlaps(left, right) {
|
|
56
|
+
if (left.length === 0 || right.length === 0)
|
|
57
|
+
return undefined;
|
|
58
|
+
return left.some((leftValue) => right.some((rightValue) => entityKey(leftValue.group) === entityKey(rightValue.group) &&
|
|
59
|
+
(leftValue.subgroup == null ||
|
|
60
|
+
rightValue.subgroup == null ||
|
|
61
|
+
leftValue.subgroup === rightValue.subgroup)));
|
|
62
|
+
}
|
|
63
|
+
function sortOccurrences(left, right) {
|
|
64
|
+
return (left.scheduledDate.localeCompare(right.scheduledDate) ||
|
|
65
|
+
((left.time?.start.hours ?? 24) * 60 + (left.time?.start.minutes ?? 0)) -
|
|
66
|
+
((right.time?.start.hours ?? 24) * 60 + (right.time?.start.minutes ?? 0)) ||
|
|
67
|
+
(left.slotNumber ?? Number.MAX_SAFE_INTEGER) -
|
|
68
|
+
(right.slotNumber ?? Number.MAX_SAFE_INTEGER) ||
|
|
69
|
+
left.subject.localeCompare(right.subject) ||
|
|
70
|
+
left.id.localeCompare(right.id));
|
|
71
|
+
}
|
|
72
|
+
export class Schedule {
|
|
73
|
+
repository;
|
|
74
|
+
owner;
|
|
75
|
+
academicYearStartYear;
|
|
76
|
+
period;
|
|
77
|
+
holidays;
|
|
78
|
+
holidayTransfers;
|
|
79
|
+
constructor(repository, owner, academicYearStartYear, options) {
|
|
80
|
+
this.repository = repository;
|
|
81
|
+
this.owner = structuredClone(owner);
|
|
82
|
+
this.academicYearStartYear = academicYearStartYear;
|
|
83
|
+
this.period = options?.period ?? getCurrentPeriod();
|
|
84
|
+
this.holidays = structuredClone(options?.holidays ?? RUSSIAN_HOLIDAYS);
|
|
85
|
+
this.holidayTransfers = (options?.holidayTransfers ?? []).map((value) => ({
|
|
86
|
+
dayOff: new Date(value.dayOff),
|
|
87
|
+
workDay: value.workDay == null ? null : new Date(value.workDay),
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
get revision() {
|
|
91
|
+
return this.repository.revision;
|
|
92
|
+
}
|
|
93
|
+
series() {
|
|
94
|
+
return this.repository.getSeries({
|
|
95
|
+
owner: this.owner,
|
|
96
|
+
academicYearStartYear: this.academicYearStartYear,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
on(date, options) {
|
|
100
|
+
if (!this.isInAcademicYear(date))
|
|
101
|
+
return [];
|
|
102
|
+
const holiday = isHoliday(date, this.holidays, this.holidayTransfers);
|
|
103
|
+
const localDate = formatLocalDate(date);
|
|
104
|
+
const direct = this.repository.getDirectOccurrences({
|
|
105
|
+
owner: this.owner,
|
|
106
|
+
academicYearStartYear: this.academicYearStartYear,
|
|
107
|
+
});
|
|
108
|
+
const movedSources = direct
|
|
109
|
+
.filter((value) => value.movedFrom)
|
|
110
|
+
.map((value) => ({
|
|
111
|
+
date: value.movedFrom.date,
|
|
112
|
+
slotNumber: value.movedFrom.slotNumber,
|
|
113
|
+
subject: value.subject,
|
|
114
|
+
groups: value.groups.values,
|
|
115
|
+
}));
|
|
116
|
+
const occurrences = [];
|
|
117
|
+
for (const series of this.series()) {
|
|
118
|
+
if (holiday)
|
|
119
|
+
continue;
|
|
120
|
+
const week = recurrenceIncludes(series, date);
|
|
121
|
+
if (week == null)
|
|
122
|
+
continue;
|
|
123
|
+
if (movedSources.some((source) => source.date === localDate &&
|
|
124
|
+
source.slotNumber === series.slotNumber &&
|
|
125
|
+
normalizeScheduleText(source.subject) ===
|
|
126
|
+
normalizeScheduleText(series.subject) &&
|
|
127
|
+
attendanceOverlaps(source.groups, series.groups.values) !== false)) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
let rooms = series.rooms;
|
|
131
|
+
let teachers = series.teachers;
|
|
132
|
+
let isDistance = series.isDistance;
|
|
133
|
+
let originalRooms;
|
|
134
|
+
let originalTeachers;
|
|
135
|
+
const substitution = series.substitutions.find((value) => value.date === localDate);
|
|
136
|
+
if (substitution?.rooms) {
|
|
137
|
+
originalRooms = rooms;
|
|
138
|
+
rooms = { values: substitution.rooms, completeness: "complete" };
|
|
139
|
+
}
|
|
140
|
+
if (substitution?.teachers) {
|
|
141
|
+
originalTeachers = teachers;
|
|
142
|
+
teachers = { values: substitution.teachers, completeness: "complete" };
|
|
143
|
+
}
|
|
144
|
+
if (substitution?.isDistance != null) {
|
|
145
|
+
isDistance = substitution.isDistance;
|
|
146
|
+
}
|
|
147
|
+
const occurrence = {
|
|
148
|
+
id: occurrenceIdForSeries(series.id, week),
|
|
149
|
+
seriesId: series.id,
|
|
150
|
+
academicYearStartYear: series.academicYearStartYear,
|
|
151
|
+
period: series.period,
|
|
152
|
+
academicWeek: week,
|
|
153
|
+
nominalDate: localDate,
|
|
154
|
+
scheduledDate: localDate,
|
|
155
|
+
subject: series.subject,
|
|
156
|
+
type: series.type,
|
|
157
|
+
slotNumber: series.slotNumber,
|
|
158
|
+
time: series.time,
|
|
159
|
+
groups: series.groups,
|
|
160
|
+
teachers,
|
|
161
|
+
rooms,
|
|
162
|
+
isDistance,
|
|
163
|
+
possibleChanges: series.possibleChanges,
|
|
164
|
+
status: "scheduled",
|
|
165
|
+
originalRooms,
|
|
166
|
+
originalTeachers,
|
|
167
|
+
sources: series.sources,
|
|
168
|
+
};
|
|
169
|
+
if (occurrenceMatchesOwner(occurrence, this.owner) &&
|
|
170
|
+
attendanceMatchesSubgroup(occurrence.groups.values, this.owner, options?.subgroup)) {
|
|
171
|
+
occurrences.push(occurrence);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (const occurrence of direct) {
|
|
175
|
+
if (occurrence.scheduledDate !== localDate)
|
|
176
|
+
continue;
|
|
177
|
+
if (!occurrenceMatchesOwner(occurrence, this.owner))
|
|
178
|
+
continue;
|
|
179
|
+
if (!attendanceMatchesSubgroup(occurrence.groups.values, this.owner, options?.subgroup)) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
occurrences.push(occurrence);
|
|
183
|
+
}
|
|
184
|
+
return occurrences.sort(sortOccurrences);
|
|
185
|
+
}
|
|
186
|
+
week(week, options) {
|
|
187
|
+
const period = this.period === AcademicPeriod.WinterSession ||
|
|
188
|
+
this.period === AcademicPeriod.SummerSession
|
|
189
|
+
? getAdjacentSemester(this.period)
|
|
190
|
+
: this.period;
|
|
191
|
+
const year = period === AcademicPeriod.FallSemester
|
|
192
|
+
? this.academicYearStartYear
|
|
193
|
+
: this.academicYearStartYear + 1;
|
|
194
|
+
let monday;
|
|
195
|
+
if (week != null) {
|
|
196
|
+
if (!Number.isInteger(week) || week < 1) {
|
|
197
|
+
throw new RangeError("Academic week must be a positive integer");
|
|
198
|
+
}
|
|
199
|
+
const data = getSemesterWeeks({ period, year, weekCount: week }).find((value) => value.week === week);
|
|
200
|
+
if (!data)
|
|
201
|
+
return [];
|
|
202
|
+
monday = data.start;
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
monday = getMonday(new Date());
|
|
206
|
+
}
|
|
207
|
+
const result = [];
|
|
208
|
+
for (let day = 0; day < 7; day++) {
|
|
209
|
+
const date = new Date(monday);
|
|
210
|
+
date.setDate(monday.getDate() + day);
|
|
211
|
+
result.push(...this.on(date, options));
|
|
212
|
+
}
|
|
213
|
+
return result.sort(sortOccurrences);
|
|
214
|
+
}
|
|
215
|
+
weekday(weekday, options) {
|
|
216
|
+
if (!Number.isInteger(weekday) || weekday < 0 || weekday > 6) {
|
|
217
|
+
throw new RangeError("Weekday must be an integer from 0 to 6");
|
|
218
|
+
}
|
|
219
|
+
if (options?.week != null) {
|
|
220
|
+
return this.week(options.week, options).filter((value) => parseLocalDate(value.scheduledDate).getDay() === weekday);
|
|
221
|
+
}
|
|
222
|
+
const now = new Date();
|
|
223
|
+
const date = new Date(now);
|
|
224
|
+
date.setDate(now.getDate() + weekday - now.getDay());
|
|
225
|
+
date.setHours(0, 0, 0, 0);
|
|
226
|
+
return this.on(date, options);
|
|
227
|
+
}
|
|
228
|
+
today(options) {
|
|
229
|
+
return this.on(new Date(), options);
|
|
230
|
+
}
|
|
231
|
+
tomorrow(options) {
|
|
232
|
+
const date = new Date();
|
|
233
|
+
date.setDate(date.getDate() + 1);
|
|
234
|
+
return this.on(date, options);
|
|
235
|
+
}
|
|
236
|
+
thisWeek(options) {
|
|
237
|
+
return this.week(undefined, options);
|
|
238
|
+
}
|
|
239
|
+
current(options) {
|
|
240
|
+
const now = new Date();
|
|
241
|
+
return (this.on(now, options).find((value) => {
|
|
242
|
+
if (!value.time)
|
|
243
|
+
return false;
|
|
244
|
+
const start = dateAt(now, value.time.start);
|
|
245
|
+
const end = dateAt(now, value.time.end);
|
|
246
|
+
return now >= start && now <= end;
|
|
247
|
+
}) ?? null);
|
|
248
|
+
}
|
|
249
|
+
getWeekNumber(date) {
|
|
250
|
+
const period = this.period === AcademicPeriod.WinterSession ||
|
|
251
|
+
this.period === AcademicPeriod.SummerSession
|
|
252
|
+
? getAdjacentSemester(this.period)
|
|
253
|
+
: this.period;
|
|
254
|
+
return getWeekNumber({
|
|
255
|
+
period,
|
|
256
|
+
year: period === AcademicPeriod.FallSemester
|
|
257
|
+
? this.academicYearStartYear
|
|
258
|
+
: this.academicYearStartYear + 1,
|
|
259
|
+
date,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
getSemesterWeeks(weekCount) {
|
|
263
|
+
const period = this.period === AcademicPeriod.WinterSession ||
|
|
264
|
+
this.period === AcademicPeriod.SummerSession
|
|
265
|
+
? getAdjacentSemester(this.period)
|
|
266
|
+
: this.period;
|
|
267
|
+
return getSemesterWeeks({
|
|
268
|
+
period,
|
|
269
|
+
year: period === AcademicPeriod.FallSemester
|
|
270
|
+
? this.academicYearStartYear
|
|
271
|
+
: this.academicYearStartYear + 1,
|
|
272
|
+
weekCount,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
getSemesterStart() {
|
|
276
|
+
const period = this.period === AcademicPeriod.WinterSession ||
|
|
277
|
+
this.period === AcademicPeriod.SummerSession
|
|
278
|
+
? getAdjacentSemester(this.period)
|
|
279
|
+
: this.period;
|
|
280
|
+
return getSemesterStart({
|
|
281
|
+
period,
|
|
282
|
+
year: period === AcademicPeriod.FallSemester
|
|
283
|
+
? this.academicYearStartYear
|
|
284
|
+
: this.academicYearStartYear + 1,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
isInAcademicYear(date) {
|
|
288
|
+
const start = new Date(this.academicYearStartYear, 8, 1);
|
|
289
|
+
const end = new Date(this.academicYearStartYear + 1, 7, 31, 23, 59, 59, 999);
|
|
290
|
+
return date >= start && date <= end;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { AcademicPeriod, LocalDate, TimeRange, WeekRange } from "../../common/types.js";
|
|
2
|
+
export type LessonSeriesId = string;
|
|
3
|
+
export type LessonId = string;
|
|
4
|
+
export interface NamedEntityRef {
|
|
5
|
+
id?: number;
|
|
6
|
+
name: string;
|
|
7
|
+
}
|
|
8
|
+
export interface GroupRef extends NamedEntityRef {
|
|
9
|
+
specialty?: string;
|
|
10
|
+
profile?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface TeacherRef extends NamedEntityRef {
|
|
13
|
+
position?: string;
|
|
14
|
+
degree?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface RoomRef extends NamedEntityRef {
|
|
17
|
+
building?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface GroupAttendance {
|
|
20
|
+
group: GroupRef;
|
|
21
|
+
subgroup?: number;
|
|
22
|
+
}
|
|
23
|
+
export type ScheduleOwner = {
|
|
24
|
+
type: "group";
|
|
25
|
+
group: GroupRef;
|
|
26
|
+
} | {
|
|
27
|
+
type: "teacher";
|
|
28
|
+
teacher: TeacherRef;
|
|
29
|
+
} | {
|
|
30
|
+
type: "room";
|
|
31
|
+
room: RoomRef;
|
|
32
|
+
};
|
|
33
|
+
export type RelationCompleteness = "unknown" | "partial" | "complete";
|
|
34
|
+
export interface RelationSet<T> {
|
|
35
|
+
values: T[];
|
|
36
|
+
completeness: RelationCompleteness;
|
|
37
|
+
}
|
|
38
|
+
export interface LessonRecurrence {
|
|
39
|
+
weekday: number;
|
|
40
|
+
weeks?: WeekRange;
|
|
41
|
+
parity?: "even" | "odd";
|
|
42
|
+
}
|
|
43
|
+
export interface LessonSubstitution {
|
|
44
|
+
date: LocalDate;
|
|
45
|
+
rooms?: RoomRef[];
|
|
46
|
+
teachers?: TeacherRef[];
|
|
47
|
+
isDistance?: boolean;
|
|
48
|
+
}
|
|
49
|
+
export interface LessonTransfer {
|
|
50
|
+
fromDate: LocalDate;
|
|
51
|
+
fromSlot: number;
|
|
52
|
+
targetDate: LocalDate;
|
|
53
|
+
}
|
|
54
|
+
export interface LessonSourceRef {
|
|
55
|
+
sourceKey: string;
|
|
56
|
+
observationKey: string;
|
|
57
|
+
observedAt: Date;
|
|
58
|
+
owner: ScheduleOwner;
|
|
59
|
+
}
|
|
60
|
+
export interface LessonSeries {
|
|
61
|
+
id: LessonSeriesId;
|
|
62
|
+
academicYearStartYear: number;
|
|
63
|
+
period: AcademicPeriod;
|
|
64
|
+
subject: string;
|
|
65
|
+
type: string;
|
|
66
|
+
slotNumber?: number;
|
|
67
|
+
time?: TimeRange;
|
|
68
|
+
recurrence: LessonRecurrence;
|
|
69
|
+
groups: RelationSet<GroupAttendance>;
|
|
70
|
+
teachers: RelationSet<TeacherRef>;
|
|
71
|
+
rooms: RelationSet<RoomRef>;
|
|
72
|
+
isDistance: boolean;
|
|
73
|
+
possibleChanges: boolean;
|
|
74
|
+
substitutions: LessonSubstitution[];
|
|
75
|
+
sources: LessonSourceRef[];
|
|
76
|
+
}
|
|
77
|
+
export type LessonStatus = "scheduled" | "moved" | "cancelled";
|
|
78
|
+
export interface LessonOccurrence {
|
|
79
|
+
id: LessonId;
|
|
80
|
+
seriesId?: LessonSeriesId;
|
|
81
|
+
academicYearStartYear: number;
|
|
82
|
+
period: AcademicPeriod;
|
|
83
|
+
academicWeek?: number;
|
|
84
|
+
nominalDate: LocalDate;
|
|
85
|
+
scheduledDate: LocalDate;
|
|
86
|
+
subject: string;
|
|
87
|
+
type: string;
|
|
88
|
+
slotNumber?: number;
|
|
89
|
+
time?: TimeRange;
|
|
90
|
+
groups: RelationSet<GroupAttendance>;
|
|
91
|
+
teachers: RelationSet<TeacherRef>;
|
|
92
|
+
rooms: RelationSet<RoomRef>;
|
|
93
|
+
isDistance: boolean;
|
|
94
|
+
possibleChanges: boolean;
|
|
95
|
+
status: LessonStatus;
|
|
96
|
+
movedFrom?: {
|
|
97
|
+
date: LocalDate;
|
|
98
|
+
slotNumber?: number;
|
|
99
|
+
};
|
|
100
|
+
originalRooms?: RelationSet<RoomRef>;
|
|
101
|
+
originalTeachers?: RelationSet<TeacherRef>;
|
|
102
|
+
sources: LessonSourceRef[];
|
|
103
|
+
}
|
|
104
|
+
interface ObservationBase {
|
|
105
|
+
/** Stable only inside one source page projection. */
|
|
106
|
+
key: string;
|
|
107
|
+
subject: string;
|
|
108
|
+
type: string;
|
|
109
|
+
slotNumber?: number;
|
|
110
|
+
time?: TimeRange;
|
|
111
|
+
groups: RelationSet<GroupAttendance>;
|
|
112
|
+
teachers: RelationSet<TeacherRef>;
|
|
113
|
+
rooms: RelationSet<RoomRef>;
|
|
114
|
+
isDistance?: boolean;
|
|
115
|
+
possibleChanges?: boolean;
|
|
116
|
+
substitutions?: LessonSubstitution[];
|
|
117
|
+
}
|
|
118
|
+
export interface SeriesObservation extends ObservationBase {
|
|
119
|
+
kind: "series";
|
|
120
|
+
recurrence: LessonRecurrence;
|
|
121
|
+
}
|
|
122
|
+
export interface OccurrenceObservation extends ObservationBase {
|
|
123
|
+
kind: "occurrence";
|
|
124
|
+
date: LocalDate;
|
|
125
|
+
transfer?: LessonTransfer;
|
|
126
|
+
}
|
|
127
|
+
export type ScheduleObservation = SeriesObservation | OccurrenceObservation;
|
|
128
|
+
export interface ScheduleSourceSnapshot {
|
|
129
|
+
sourceKey: string;
|
|
130
|
+
owner: ScheduleOwner;
|
|
131
|
+
academicYearStartYear: number;
|
|
132
|
+
period: AcademicPeriod;
|
|
133
|
+
observedAt: Date;
|
|
134
|
+
observations: ScheduleObservation[];
|
|
135
|
+
}
|
|
136
|
+
export interface IngestResult {
|
|
137
|
+
revision: number;
|
|
138
|
+
seriesIds: LessonSeriesId[];
|
|
139
|
+
lessonIds: LessonId[];
|
|
140
|
+
created: number;
|
|
141
|
+
updated: number;
|
|
142
|
+
removedObservations: number;
|
|
143
|
+
}
|
|
144
|
+
export interface SerializedScheduleSourceSnapshot extends Omit<ScheduleSourceSnapshot, "observedAt" | "observations"> {
|
|
145
|
+
observedAt: string;
|
|
146
|
+
observations: SerializedScheduleObservation[];
|
|
147
|
+
}
|
|
148
|
+
export type SerializedScheduleObservation = ScheduleObservation;
|
|
149
|
+
export interface TimetableRepositorySnapshot {
|
|
150
|
+
schemaVersion: 5;
|
|
151
|
+
revision: number;
|
|
152
|
+
directory: TimetableDirectorySnapshot;
|
|
153
|
+
sources: SerializedScheduleSourceSnapshot[];
|
|
154
|
+
links: Array<{
|
|
155
|
+
sourceKey: string;
|
|
156
|
+
observationKey: string;
|
|
157
|
+
kind: "series" | "occurrence";
|
|
158
|
+
id: string;
|
|
159
|
+
}>;
|
|
160
|
+
}
|
|
161
|
+
export interface TimetableDirectorySnapshot {
|
|
162
|
+
groups: GroupRef[];
|
|
163
|
+
teachers: TeacherRef[];
|
|
164
|
+
rooms: RoomRef[];
|
|
165
|
+
}
|
|
166
|
+
export interface TimetableRepositoryAdapter {
|
|
167
|
+
load(): Promise<TimetableRepositorySnapshot | null>;
|
|
168
|
+
compareAndSet(expectedRevision: number, snapshot: TimetableRepositorySnapshot): Promise<boolean>;
|
|
169
|
+
clear?(): Promise<void>;
|
|
170
|
+
}
|
|
171
|
+
export interface LessonIdGenerator {
|
|
172
|
+
seriesId(): LessonSeriesId;
|
|
173
|
+
lessonId(): LessonId;
|
|
174
|
+
}
|
|
175
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ParsedScheduleDay } from "./types.js";
|
|
2
|
+
import type { ScheduleOwner, ScheduleSourceSnapshot } from "./domain/types.js";
|
|
3
|
+
export interface CreateScheduleSnapshotOptions {
|
|
4
|
+
sourceKey: string;
|
|
5
|
+
owner: ScheduleOwner;
|
|
6
|
+
academicYearStartYear: number;
|
|
7
|
+
period: ScheduleSourceSnapshot["period"];
|
|
8
|
+
observedAt?: Date;
|
|
9
|
+
days: ParsedScheduleDay[];
|
|
10
|
+
}
|
|
11
|
+
export declare function createScheduleSourceSnapshot(options: CreateScheduleSnapshotOptions): ScheduleSourceSnapshot;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { parseLocalDate } from "./utils/index.js";
|
|
2
|
+
const WEEKDAY_INDEX = new Map([
|
|
3
|
+
["воскресенье", 0],
|
|
4
|
+
["понедельник", 1],
|
|
5
|
+
["вторник", 2],
|
|
6
|
+
["среда", 3],
|
|
7
|
+
["четверг", 4],
|
|
8
|
+
["пятница", 5],
|
|
9
|
+
["суббота", 6],
|
|
10
|
+
]);
|
|
11
|
+
function teacherRef(value) {
|
|
12
|
+
if (!value.name)
|
|
13
|
+
return null;
|
|
14
|
+
return { ...value };
|
|
15
|
+
}
|
|
16
|
+
function relationSet(values, explicitlyEmpty = false) {
|
|
17
|
+
return {
|
|
18
|
+
values: values ?? [],
|
|
19
|
+
completeness: explicitlyEmpty ? "complete" : values?.length ? "partial" : "unknown",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function groupsFor(entry, owner) {
|
|
23
|
+
const visible = entry.groups?.map((name) => ({
|
|
24
|
+
group: { name },
|
|
25
|
+
subgroup: entry.subgroup,
|
|
26
|
+
})) ?? [];
|
|
27
|
+
const groups = relationSet(visible, entry.groups === null || Array.isArray(entry.groups) && entry.groups.length === 0);
|
|
28
|
+
if (owner.type === "group") {
|
|
29
|
+
groups.values.unshift({ group: owner.group, subgroup: entry.subgroup });
|
|
30
|
+
if (groups.completeness === "unknown")
|
|
31
|
+
groups.completeness = "partial";
|
|
32
|
+
}
|
|
33
|
+
return groups;
|
|
34
|
+
}
|
|
35
|
+
function teachersFor(entry, owner) {
|
|
36
|
+
const teacher = entry.teacher ? teacherRef(entry.teacher) : null;
|
|
37
|
+
const teachers = relationSet(teacher ? [teacher] : undefined, entry.teacher === null);
|
|
38
|
+
if (owner.type === "teacher") {
|
|
39
|
+
teachers.values.unshift(owner.teacher);
|
|
40
|
+
if (teachers.completeness === "unknown")
|
|
41
|
+
teachers.completeness = "partial";
|
|
42
|
+
}
|
|
43
|
+
return teachers;
|
|
44
|
+
}
|
|
45
|
+
function roomsFor(entry, owner) {
|
|
46
|
+
const rooms = relationSet(entry.room ? [{ name: entry.room }] : undefined, entry.room === null);
|
|
47
|
+
if (owner.type === "room") {
|
|
48
|
+
rooms.values.unshift(owner.room);
|
|
49
|
+
if (rooms.completeness === "unknown")
|
|
50
|
+
rooms.completeness = "partial";
|
|
51
|
+
}
|
|
52
|
+
return rooms;
|
|
53
|
+
}
|
|
54
|
+
function substitutionsFor(substitutions) {
|
|
55
|
+
if (!substitutions?.length)
|
|
56
|
+
return undefined;
|
|
57
|
+
return substitutions.map((value) => ({
|
|
58
|
+
date: value.date,
|
|
59
|
+
rooms: value.room ? [{ name: value.room }] : undefined,
|
|
60
|
+
teachers: value.teacher
|
|
61
|
+
? [teacherRef(value.teacher)].filter((teacher) => teacher != null)
|
|
62
|
+
: undefined,
|
|
63
|
+
isDistance: value.isDistance,
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
function observationBase(key, entry, block, owner) {
|
|
67
|
+
return {
|
|
68
|
+
key,
|
|
69
|
+
subject: entry.subject,
|
|
70
|
+
type: entry.type,
|
|
71
|
+
slotNumber: block.slotNumber,
|
|
72
|
+
time: block.time,
|
|
73
|
+
groups: groupsFor(entry, owner),
|
|
74
|
+
teachers: teachersFor(entry, owner),
|
|
75
|
+
rooms: roomsFor(entry, owner),
|
|
76
|
+
isDistance: entry.isDistance,
|
|
77
|
+
possibleChanges: entry.possibleChanges,
|
|
78
|
+
substitutions: substitutionsFor(entry.substitutions),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function occurrenceFrom(key, entry, block, date, owner) {
|
|
82
|
+
return {
|
|
83
|
+
kind: "occurrence",
|
|
84
|
+
...observationBase(key, entry, block, owner),
|
|
85
|
+
date,
|
|
86
|
+
transfer: entry.transfer
|
|
87
|
+
? {
|
|
88
|
+
fromDate: entry.transfer.fromDate,
|
|
89
|
+
fromSlot: entry.transfer.fromSlot,
|
|
90
|
+
targetDate: entry.transfer.targetDate,
|
|
91
|
+
}
|
|
92
|
+
: undefined,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function seriesFrom(key, entry, block, weekday, owner) {
|
|
96
|
+
return {
|
|
97
|
+
kind: "series",
|
|
98
|
+
...observationBase(key, entry, block, owner),
|
|
99
|
+
recurrence: {
|
|
100
|
+
weekday,
|
|
101
|
+
weeks: entry.weeks,
|
|
102
|
+
parity: entry.weekParity,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export function createScheduleSourceSnapshot(options) {
|
|
107
|
+
const observations = [];
|
|
108
|
+
for (const [dayIndex, day] of options.days.entries()) {
|
|
109
|
+
const weekday = (day.date ? parseLocalDate(day.date).getDay() : undefined) ??
|
|
110
|
+
WEEKDAY_INDEX.get(day.weekday.toLowerCase());
|
|
111
|
+
if (weekday == null)
|
|
112
|
+
continue;
|
|
113
|
+
for (const [blockIndex, block] of day.blocks.entries()) {
|
|
114
|
+
for (const [lessonIndex, entry] of block.lessons.entries()) {
|
|
115
|
+
const key = `day:${dayIndex}:block:${blockIndex}:lesson:${lessonIndex}`;
|
|
116
|
+
const directDate = entry.transfer?.targetDate ?? entry.substituteFor?.date ?? day.date;
|
|
117
|
+
observations.push(directDate
|
|
118
|
+
? occurrenceFrom(key, entry, block, directDate, options.owner)
|
|
119
|
+
: seriesFrom(key, entry, block, weekday, options.owner));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
sourceKey: options.sourceKey,
|
|
125
|
+
owner: options.owner,
|
|
126
|
+
academicYearStartYear: options.academicYearStartYear,
|
|
127
|
+
period: options.period,
|
|
128
|
+
observedAt: options.observedAt ?? new Date(),
|
|
129
|
+
observations,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function
|
|
3
|
-
export declare function
|
|
1
|
+
import type { RoomInfo, ParsedScheduleDay } from "../types.js";
|
|
2
|
+
export declare function parseRoomInfo(html: string): RoomInfo | null;
|
|
3
|
+
export declare function parseRoomSchedule(html: string): ParsedScheduleDay[];
|