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.
Files changed (80) hide show
  1. package/README.md +165 -193
  2. package/dist/browser.d.ts +1 -1
  3. package/dist/browser.js +1 -1
  4. package/dist/common/cache.js +30 -9
  5. package/dist/common/http.d.ts +6 -0
  6. package/dist/common/http.js +8 -1
  7. package/dist/common/parse.d.ts +1 -1
  8. package/dist/common/parse.js +2 -2
  9. package/dist/common/types.d.ts +8 -2
  10. package/dist/common/types.js +12 -0
  11. package/dist/core.d.ts +10 -0
  12. package/dist/core.js +5 -0
  13. package/dist/index.d.ts +5 -3
  14. package/dist/index.js +4 -4
  15. package/dist/lk/client.d.ts +8 -6
  16. package/dist/lk/client.js +51 -23
  17. package/dist/lk/types.d.ts +7 -7
  18. package/dist/node.d.ts +1 -0
  19. package/dist/node.js +1 -0
  20. package/dist/parsers.d.ts +3 -0
  21. package/dist/parsers.js +2 -0
  22. package/dist/tt/client.d.ts +55 -63
  23. package/dist/tt/client.js +477 -226
  24. package/dist/tt/domain/directory.d.ts +14 -0
  25. package/dist/tt/domain/directory.js +116 -0
  26. package/dist/tt/domain/ids.d.ts +7 -0
  27. package/dist/tt/domain/ids.js +20 -0
  28. package/dist/tt/domain/index.d.ts +7 -0
  29. package/dist/tt/domain/index.js +5 -0
  30. package/dist/tt/domain/normalize.d.ts +7 -0
  31. package/dist/tt/domain/normalize.js +65 -0
  32. package/dist/tt/domain/repository.d.ts +51 -0
  33. package/dist/tt/domain/repository.js +805 -0
  34. package/dist/tt/domain/schedule.d.ts +37 -0
  35. package/dist/tt/domain/schedule.js +292 -0
  36. package/dist/tt/domain/types.d.ts +175 -0
  37. package/dist/tt/domain/types.js +1 -0
  38. package/dist/tt/observations.d.ts +11 -0
  39. package/dist/tt/observations.js +131 -0
  40. package/dist/tt/parse/audience.d.ts +3 -3
  41. package/dist/tt/parse/audience.js +47 -15
  42. package/dist/tt/parse/entry-parts.d.ts +1 -0
  43. package/dist/tt/parse/entry-parts.js +5 -2
  44. package/dist/tt/parse/full-schedule.d.ts +9 -4
  45. package/dist/tt/parse/full-schedule.js +32 -41
  46. package/dist/tt/parse/groups.d.ts +1 -1
  47. package/dist/tt/parse/groups.js +1 -1
  48. package/dist/tt/parse/index.d.ts +4 -4
  49. package/dist/tt/parse/index.js +4 -4
  50. package/dist/tt/parse/lists.d.ts +6 -5
  51. package/dist/tt/parse/lists.js +7 -2
  52. package/dist/tt/parse/overlays.d.ts +5 -4
  53. package/dist/tt/parse/overlays.js +13 -14
  54. package/dist/tt/parse/teacher.d.ts +2 -3
  55. package/dist/tt/parse/teacher.js +14 -56
  56. package/dist/tt/parse/webinars.js +3 -4
  57. package/dist/tt/types.d.ts +54 -69
  58. package/dist/tt/utils/date.d.ts +4 -0
  59. package/dist/tt/utils/date.js +30 -1
  60. package/dist/tt/utils/index.d.ts +2 -3
  61. package/dist/tt/utils/index.js +2 -3
  62. package/dist/tt/utils/period.d.ts +4 -4
  63. package/dist/tt/utils/period.js +9 -8
  64. package/dist/tt/utils/semester.d.ts +4 -4
  65. package/dist/tt/utils/semester.js +25 -5
  66. package/dist/tt/utils/time-slots.d.ts +3 -5
  67. package/dist/tt/utils/time-slots.js +19 -34
  68. package/dist/tt/webinars.d.ts +7 -0
  69. package/dist/tt/webinars.js +38 -0
  70. package/docs/fixture-review.md +56 -0
  71. package/docs/testing.md +23 -0
  72. package/docs/v5-architecture.md +144 -0
  73. package/docs/v5-migration.md +451 -0
  74. package/package.json +17 -7
  75. package/dist/shared.d.ts +0 -9
  76. package/dist/shared.js +0 -7
  77. package/dist/tt/schedule.d.ts +0 -55
  78. package/dist/tt/schedule.js +0 -233
  79. package/dist/tt/utils/lessons.d.ts +0 -16
  80. 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 { AudienceInfo, FullScheduleDay } from "../types.js";
2
- export declare function parseAudienceInfo(html: string): AudienceInfo | null;
3
- export declare function parseAudienceFullSchedule(html: string): FullScheduleDay[];
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[];