chuvsu-js 4.2.0 → 5.0.1

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 +25 -18
  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
@@ -1,55 +0,0 @@
1
- import type { FullScheduleDay, SemesterWeek, Lesson } from "./types.js";
2
- import { Period, EducationType } from "../common/types.js";
3
- import { type Holiday, type HolidayTransfer } from "./utils/index.js";
4
- export declare class Schedule {
5
- readonly groupId: number;
6
- readonly scheduleMap: Map<number, FullScheduleDay[]>;
7
- readonly educationType: EducationType;
8
- /** Whether this is a teacher schedule (affects substitution handling). */
9
- readonly isTeacherSchedule: boolean;
10
- /** List of holidays to exclude from schedule queries. Pass `[]` to disable. */
11
- readonly holidays: Holiday[];
12
- /** Government decree day-off transfers (Постановление Правительства). */
13
- readonly holidayTransfers: HolidayTransfer[];
14
- /** Academic year represented by this schedule (e.g. 2026 for 2026/2027). */
15
- readonly academicYearStartYear: number;
16
- private _period?;
17
- constructor(groupId: number, scheduleMap: Map<number, FullScheduleDay[]>, period?: Period, educationType?: EducationType, holidays?: Holiday[] | null, holidayTransfers?: HolidayTransfer[], isTeacherSchedule?: boolean, academicYearStartYear?: number);
18
- /** Current (or fixed) period for this schedule. */
19
- get period(): Period;
20
- /** Days for the current period. */
21
- get days(): FullScheduleDay[];
22
- /** All periods that have data in this schedule. */
23
- get periods(): Period[];
24
- /** Get days for a specific period. */
25
- getDays(period: Period): FullScheduleDay[];
26
- private getSemesterYear;
27
- private getSlotsForWeekday;
28
- private getDateForWeekday;
29
- private isInCurrentAcademicYear;
30
- forDay(weekday: number, opts?: {
31
- subgroup?: number;
32
- week?: number;
33
- }): Lesson[];
34
- forDate(date: Date, opts?: {
35
- subgroup?: number;
36
- }): Lesson[];
37
- forWeek(week?: number, opts?: {
38
- subgroup?: number;
39
- }): Lesson[];
40
- today(opts?: {
41
- subgroup?: number;
42
- }): Lesson[];
43
- tomorrow(opts?: {
44
- subgroup?: number;
45
- }): Lesson[];
46
- thisWeek(opts?: {
47
- subgroup?: number;
48
- }): Lesson[];
49
- currentLesson(opts?: {
50
- subgroup?: number;
51
- }): Lesson | null;
52
- getWeekNumber(date?: Date): number;
53
- getSemesterWeeks(weekCount?: number): SemesterWeek[];
54
- getSemesterStart(): Date;
55
- }
@@ -1,233 +0,0 @@
1
- import { collectTransfers, filterSlots, getAdjacentSemester, getCurrentPeriod, getMonday, getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, isHoliday, isSameDay, isSessionPeriod, RUSSIAN_HOLIDAYS, slotsToLessons, sortLessons, suppressTransferredLessons, } from "./utils/index.js";
2
- export class Schedule {
3
- groupId;
4
- scheduleMap;
5
- educationType;
6
- /** Whether this is a teacher schedule (affects substitution handling). */
7
- isTeacherSchedule;
8
- /** List of holidays to exclude from schedule queries. Pass `[]` to disable. */
9
- holidays;
10
- /** Government decree day-off transfers (Постановление Правительства). */
11
- holidayTransfers;
12
- /** Academic year represented by this schedule (e.g. 2026 for 2026/2027). */
13
- academicYearStartYear;
14
- _period;
15
- constructor(groupId, scheduleMap, period, educationType, holidays, holidayTransfers, isTeacherSchedule, academicYearStartYear) {
16
- this.groupId = groupId;
17
- this.scheduleMap = scheduleMap;
18
- this.educationType = educationType ?? 1 /* EducationType.HigherEducation */;
19
- this.isTeacherSchedule = isTeacherSchedule ?? false;
20
- this._period = period;
21
- this.holidays = holidays ?? RUSSIAN_HOLIDAYS;
22
- this.holidayTransfers = holidayTransfers ?? [];
23
- if (academicYearStartYear != null) {
24
- this.academicYearStartYear = academicYearStartYear;
25
- }
26
- else {
27
- const now = new Date();
28
- this.academicYearStartYear =
29
- now.getMonth() >= 8 ? now.getFullYear() : now.getFullYear() - 1;
30
- }
31
- }
32
- /** Current (or fixed) period for this schedule. */
33
- get period() {
34
- return this._period ?? getCurrentPeriod();
35
- }
36
- /** Days for the current period. */
37
- get days() {
38
- return this.scheduleMap.get(this.period) ?? [];
39
- }
40
- /** All periods that have data in this schedule. */
41
- get periods() {
42
- return [...this.scheduleMap.keys()];
43
- }
44
- /** Get days for a specific period. */
45
- getDays(period) {
46
- return this.scheduleMap.get(period) ?? [];
47
- }
48
- getSemesterYear(period) {
49
- const semester = isSessionPeriod(period)
50
- ? getAdjacentSemester(period)
51
- : period;
52
- return semester === 1 /* Period.FallSemester */
53
- ? this.academicYearStartYear
54
- : this.academicYearStartYear + 1;
55
- }
56
- // --- Semester helpers (weekday-based) ---
57
- getSlotsForWeekday(weekday, days, opts) {
58
- const dayName = getWeekdayName(weekday);
59
- const day = days.find((d) => d.weekday.toLowerCase() === dayName.toLowerCase());
60
- if (!day)
61
- return [];
62
- return filterSlots(day.slots, opts);
63
- }
64
- getDateForWeekday(weekday, period, week) {
65
- if (week != null) {
66
- const startMonday = getMonday(getSemesterStart({ period, year: this.getSemesterYear(period) }));
67
- const date = new Date(startMonday);
68
- date.setDate(startMonday.getDate() +
69
- (week - 1) * 7 +
70
- (weekday === 0 ? 6 : weekday - 1));
71
- return date;
72
- }
73
- const now = new Date();
74
- const currentDay = now.getDay();
75
- const diff = weekday - currentDay;
76
- const date = new Date(now);
77
- date.setDate(now.getDate() + diff);
78
- date.setHours(0, 0, 0, 0);
79
- return date;
80
- }
81
- isInCurrentAcademicYear(date) {
82
- const startYear = this.academicYearStartYear;
83
- const start = new Date(startYear, 8, 1);
84
- const end = new Date(startYear + 1, 7, 31, 23, 59, 59, 999);
85
- return date >= start && date <= end;
86
- }
87
- // --- Public query methods ---
88
- forDay(weekday, opts) {
89
- const period = this.period;
90
- const days = this.getDays(period);
91
- if (isSessionPeriod(period)) {
92
- // For sessions, return all entries on days matching this weekday
93
- const lessons = [];
94
- const dayName = getWeekdayName(weekday);
95
- for (const d of days) {
96
- if (d.weekday.toLowerCase() === dayName.toLowerCase() && d.date) {
97
- const slots = filterSlots(d.slots, { subgroup: opts?.subgroup });
98
- lessons.push(...slotsToLessons(slots, d.date, {
99
- isTeacherSchedule: this.isTeacherSchedule,
100
- }));
101
- }
102
- }
103
- return lessons.sort(sortLessons);
104
- }
105
- const slots = this.getSlotsForWeekday(weekday, days, opts);
106
- const date = this.getDateForWeekday(weekday, period, opts?.week);
107
- return slotsToLessons(slots, date, { isTeacherSchedule: this.isTeacherSchedule });
108
- }
109
- forDate(date, opts) {
110
- if (!this.isInCurrentAcademicYear(date))
111
- return [];
112
- if (isHoliday(date, this.holidays, this.holidayTransfers))
113
- return [];
114
- const period = getCurrentPeriod({ date });
115
- const lessons = [];
116
- // 1. Check all periods for date-based (session) entries matching this date
117
- for (const [, d] of this.scheduleMap) {
118
- for (const day of d) {
119
- if (day.date && isSameDay(day.date, date)) {
120
- const slots = filterSlots(day.slots, { subgroup: opts?.subgroup });
121
- lessons.push(...slotsToLessons(slots, date, {
122
- isTeacherSchedule: this.isTeacherSchedule,
123
- }));
124
- }
125
- }
126
- }
127
- // 2. Check applicable semester for weekday-based entries
128
- const semesterPeriod = isSessionPeriod(period)
129
- ? getAdjacentSemester(period)
130
- : period;
131
- const semesterDays = this.getDays(semesterPeriod);
132
- if (semesterDays.length > 0) {
133
- const week = getWeekNumber({
134
- period: semesterPeriod,
135
- year: this.getSemesterYear(semesterPeriod),
136
- date,
137
- });
138
- if (week >= 1 && week <= 17) {
139
- const weekday = date.getDay();
140
- const slots = this.getSlotsForWeekday(weekday, semesterDays, {
141
- subgroup: opts?.subgroup,
142
- week,
143
- date,
144
- });
145
- lessons.push(...slotsToLessons(slots, date, { isTeacherSchedule: this.isTeacherSchedule }));
146
- }
147
- // 3. Suppress lessons that were transferred away from this date
148
- const transfers = collectTransfers(semesterDays);
149
- if (transfers.length > 0) {
150
- return suppressTransferredLessons(lessons, transfers, date).sort(sortLessons);
151
- }
152
- }
153
- return lessons.sort(sortLessons);
154
- }
155
- forWeek(week, opts) {
156
- const period = this.period;
157
- // Determine the Monday of the target week
158
- let mondayDate;
159
- if (week != null && !isSessionPeriod(period)) {
160
- const semesterWeeks = getSemesterWeeks({
161
- period,
162
- year: this.getSemesterYear(period),
163
- weekCount: week,
164
- });
165
- const weekData = semesterWeeks.find((w) => w.week === week);
166
- mondayDate = weekData ? weekData.start : getMonday(new Date());
167
- }
168
- else {
169
- mondayDate = getMonday(new Date());
170
- }
171
- const lessons = [];
172
- for (let i = 0; i < 7; i++) {
173
- const date = new Date(mondayDate);
174
- date.setDate(mondayDate.getDate() + i);
175
- date.setHours(0, 0, 0, 0);
176
- lessons.push(...this.forDate(date, opts));
177
- }
178
- return lessons.sort(sortLessons);
179
- }
180
- today(opts) {
181
- return this.forDate(new Date(), opts).sort(sortLessons);
182
- }
183
- tomorrow(opts) {
184
- const date = new Date();
185
- date.setDate(date.getDate() + 1);
186
- return this.forDate(date, opts).sort(sortLessons);
187
- }
188
- thisWeek(opts) {
189
- return this.forWeek(undefined, opts).sort(sortLessons);
190
- }
191
- currentLesson(opts) {
192
- const now = new Date();
193
- const lessons = this.forDate(now, opts);
194
- const timeMinutes = now.getHours() * 60 + now.getMinutes();
195
- for (const lesson of lessons) {
196
- const start = lesson.start.hours * 60 + lesson.start.minutes;
197
- const end = lesson.end.hours * 60 + lesson.end.minutes;
198
- if (timeMinutes >= start && timeMinutes <= end) {
199
- return lesson;
200
- }
201
- }
202
- return null;
203
- }
204
- getWeekNumber(date) {
205
- const period = isSessionPeriod(this.period)
206
- ? getAdjacentSemester(this.period)
207
- : this.period;
208
- return getWeekNumber({
209
- period,
210
- year: this.getSemesterYear(period),
211
- date,
212
- });
213
- }
214
- getSemesterWeeks(weekCount) {
215
- const period = isSessionPeriod(this.period)
216
- ? getAdjacentSemester(this.period)
217
- : this.period;
218
- return getSemesterWeeks({
219
- period,
220
- year: this.getSemesterYear(period),
221
- weekCount,
222
- });
223
- }
224
- getSemesterStart() {
225
- const period = isSessionPeriod(this.period)
226
- ? getAdjacentSemester(this.period)
227
- : this.period;
228
- return getSemesterStart({
229
- period,
230
- year: this.getSemesterYear(period),
231
- });
232
- }
233
- }
@@ -1,16 +0,0 @@
1
- import type { FullScheduleDay, FullScheduleSlot, Lesson, ScheduleEntry, Webinar } from "../types.js";
2
- export declare function sortLessons(a: Lesson, b: Lesson): number;
3
- export declare function filterSlots(slots: FullScheduleSlot[], opts?: {
4
- subgroup?: number;
5
- week?: number;
6
- date?: Date;
7
- }): FullScheduleSlot[];
8
- export declare function slotsToLessons(slots: FullScheduleSlot[], date: Date, opts?: {
9
- isTeacherSchedule?: boolean;
10
- }): Lesson[];
11
- export declare function matchWebinarToLesson(lesson: Lesson, webinars: Webinar[]): Webinar | undefined;
12
- export declare function attachWebinarsToLessons(lessons: Lesson[], webinars: Webinar[]): Lesson[];
13
- /** All transfer entries from the given schedule days. */
14
- export declare function collectTransfers(days: FullScheduleDay[]): ScheduleEntry[];
15
- /** Remove lessons whose source date/slot match a transfer. */
16
- export declare function suppressTransferredLessons(lessons: Lesson[], transfers: ScheduleEntry[], date: Date): Lesson[];
@@ -1,173 +0,0 @@
1
- import { isSameDay } from "./date.js";
2
- export function sortLessons(a, b) {
3
- return a.start.date.getTime() - b.start.date.getTime();
4
- }
5
- function filterEntries(entries, opts) {
6
- return entries.filter((e) => {
7
- // Subgroup filter applies to all entry types
8
- if (opts?.subgroup && e.subgroup && e.subgroup !== opts.subgroup) {
9
- return false;
10
- }
11
- // Transfer entries: only include when the query date matches the target date
12
- if (e.transfer) {
13
- if (!opts?.date)
14
- return false;
15
- return isSameDay(e.transfer.targetDate, opts.date);
16
- }
17
- // Substitute-for entries: only include when the query date matches
18
- if (e.substituteFor) {
19
- if (!opts?.date)
20
- return false;
21
- return isSameDay(e.substituteFor.date, opts.date);
22
- }
23
- if (opts?.week != null) {
24
- if (e.weeks.from > 0 &&
25
- (opts.week < e.weeks.from || opts.week > e.weeks.to)) {
26
- return false;
27
- }
28
- if (e.weekParity) {
29
- const isEven = opts.week % 2 === 0;
30
- if (e.weekParity === "even" && !isEven)
31
- return false;
32
- if (e.weekParity === "odd" && isEven)
33
- return false;
34
- }
35
- }
36
- return true;
37
- });
38
- }
39
- export function filterSlots(slots, opts) {
40
- if (opts?.subgroup == null && opts?.week == null && opts?.date == null)
41
- return slots;
42
- return slots
43
- .map((slot) => ({
44
- ...slot,
45
- entries: filterEntries(slot.entries, opts),
46
- }))
47
- .filter((slot) => slot.entries.length > 0);
48
- }
49
- function makeLessonTime(date, time) {
50
- const d = new Date(date);
51
- d.setHours(time.hours, time.minutes, 0, 0);
52
- return { date: d, hours: time.hours, minutes: time.minutes };
53
- }
54
- function isDistanceRoom(room) {
55
- return /дистанционно|ДОТ/i.test(room ?? "");
56
- }
57
- export function slotsToLessons(slots, date, opts) {
58
- const lessons = [];
59
- for (const slot of slots) {
60
- for (const entry of slot.entries) {
61
- let room = entry.room;
62
- let teacher = entry.teacher;
63
- let isDistance = entry.isDistance || isDistanceRoom(room);
64
- let originalRoom;
65
- let originalTeacher;
66
- // Apply date-specific substitutions
67
- if (entry.substitutions) {
68
- const sub = entry.substitutions.find((s) => isSameDay(s.date, date));
69
- if (sub) {
70
- if (sub.room) {
71
- originalRoom = room;
72
- room = sub.room;
73
- isDistance = sub.isDistance || isDistanceRoom(room);
74
- }
75
- if (sub.teacher) {
76
- // On teacher schedules, a teacher substitution means another teacher
77
- // is taking over — exclude the lesson entirely.
78
- if (opts?.isTeacherSchedule)
79
- continue;
80
- originalTeacher = teacher;
81
- teacher = sub.teacher;
82
- }
83
- }
84
- }
85
- lessons.push({
86
- number: slot.number,
87
- start: makeLessonTime(date, slot.timeStart),
88
- end: makeLessonTime(date, slot.timeEnd),
89
- subject: entry.subject,
90
- type: entry.type,
91
- room,
92
- teacher,
93
- groups: entry.groups,
94
- weeks: entry.weeks,
95
- subgroup: entry.subgroup,
96
- weekParity: entry.weekParity,
97
- isDistance,
98
- originalRoom,
99
- originalTeacher,
100
- transfer: entry.transfer,
101
- substituteFor: entry.substituteFor,
102
- possibleChanges: entry.possibleChanges,
103
- });
104
- }
105
- }
106
- return lessons;
107
- }
108
- function normalizeText(value) {
109
- return value
110
- .toLowerCase()
111
- .replace(/ё/g, "е")
112
- .replace(/\s+/g, " ")
113
- .trim();
114
- }
115
- function sameTime(lesson, webinar) {
116
- return (lesson.start.hours === webinar.timeStart.hours &&
117
- lesson.start.minutes === webinar.timeStart.minutes &&
118
- lesson.end.hours === webinar.timeEnd.hours &&
119
- lesson.end.minutes === webinar.timeEnd.minutes);
120
- }
121
- export function matchWebinarToLesson(lesson, webinars) {
122
- return webinars.find((webinar) => {
123
- if (!webinar.scheduled)
124
- return false;
125
- if (webinar.date && !isSameDay(webinar.date, lesson.start.date))
126
- return false;
127
- if (webinar.slotNumber != null && webinar.slotNumber !== lesson.number) {
128
- return false;
129
- }
130
- if (!sameTime(lesson, webinar))
131
- return false;
132
- if (normalizeText(webinar.subject) !== normalizeText(lesson.subject)) {
133
- return false;
134
- }
135
- if (webinar.type && lesson.type && webinar.type !== lesson.type) {
136
- return false;
137
- }
138
- if (webinar.subgroup != null &&
139
- lesson.subgroup != null &&
140
- webinar.subgroup !== lesson.subgroup) {
141
- return false;
142
- }
143
- return true;
144
- });
145
- }
146
- export function attachWebinarsToLessons(lessons, webinars) {
147
- return lessons.map((lesson) => {
148
- const webinar = matchWebinarToLesson(lesson, webinars);
149
- return webinar ? { ...lesson, webinar } : lesson;
150
- });
151
- }
152
- /** All transfer entries from the given schedule days. */
153
- export function collectTransfers(days) {
154
- const transfers = [];
155
- for (const day of days) {
156
- for (const slot of day.slots) {
157
- for (const entry of slot.entries) {
158
- if (entry.transfer)
159
- transfers.push(entry);
160
- }
161
- }
162
- }
163
- return transfers;
164
- }
165
- /** Remove lessons whose source date/slot match a transfer. */
166
- export function suppressTransferredLessons(lessons, transfers, date) {
167
- return lessons.filter((lesson) => {
168
- return !transfers.some((t) => t.transfer &&
169
- isSameDay(t.transfer.fromDate, date) &&
170
- t.transfer.fromSlot === lesson.number &&
171
- t.transfer.subject === lesson.subject);
172
- });
173
- }