chuvsu-js 2.4.1 → 2.5.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/dist/browser.d.ts +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/tt/client.d.ts +12 -1
- package/dist/tt/client.js +44 -1
- package/dist/tt/parse.d.ts +3 -1
- package/dist/tt/parse.js +151 -2
- package/dist/tt/schedule.d.ts +4 -2
- package/dist/tt/schedule.js +5 -2
- package/dist/tt/types.d.ts +11 -0
- package/dist/tt/utils.d.ts +43 -3
- package/dist/tt/utils.js +181 -6
- package/package.json +1 -1
package/dist/browser.d.ts
CHANGED
|
@@ -2,4 +2,4 @@ export { Schedule } from "./tt/schedule.js";
|
|
|
2
2
|
export { getCurrentPeriod, isSessionPeriod, getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, getTimeSlots, getLessonNumber, getAdjacentSemester, } from "./tt/utils.js";
|
|
3
3
|
export { Period, EducationType } from "./common/types.js";
|
|
4
4
|
export type { Time, WeekRange, Teacher } from "./common/types.js";
|
|
5
|
-
export type { Faculty, Group, ScheduleEntry, FullScheduleSlot, FullScheduleDay, LessonTimeSlot, Lesson, LessonTime, SemesterWeek, Substitution, TransferInfo, TtClientOptions, CacheConfig, } from "./tt/types.js";
|
|
5
|
+
export type { Faculty, Group, ScheduleEntry, FullScheduleSlot, FullScheduleDay, LessonTimeSlot, Lesson, LessonTime, SemesterWeek, Substitution, TransferInfo, TeacherInfo, TtClientOptions, CacheConfig, } from "./tt/types.js";
|
package/dist/index.d.ts
CHANGED
|
@@ -2,9 +2,9 @@ export { LkClient } from "./lk/client.js";
|
|
|
2
2
|
export { TtClient } from "./tt/client.js";
|
|
3
3
|
export { Schedule } from "./tt/schedule.js";
|
|
4
4
|
export type { CacheEntry } from "./common/cache.js";
|
|
5
|
-
export { getCurrentPeriod, isSessionPeriod, getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, getTimeSlots, getLessonNumber, getAdjacentSemester, isHoliday, RUSSIAN_HOLIDAYS, } from "./tt/utils.js";
|
|
6
|
-
export type { Holiday } from "./tt/utils.js";
|
|
5
|
+
export { getCurrentPeriod, isSessionPeriod, getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, getTimeSlots, getLessonNumber, getAdjacentSemester, isHoliday, getEffectiveHolidays, getHolidayTransfers, getCompensatingWorkDays, RUSSIAN_HOLIDAYS, } from "./tt/utils.js";
|
|
6
|
+
export type { Holiday, HolidayTransfer } from "./tt/utils.js";
|
|
7
7
|
export { Period, EducationType, AuthError, ParseError, } from "./common/types.js";
|
|
8
8
|
export type { Time, WeekRange, Teacher } from "./common/types.js";
|
|
9
9
|
export type { PersonalData } from "./lk/types.js";
|
|
10
|
-
export type { Faculty, Group, ScheduleEntry, FullScheduleSlot, FullScheduleDay, LessonTimeSlot, Lesson, LessonTime, SemesterWeek, Substitution, TransferInfo, TtClientOptions, CacheConfig, } from "./tt/types.js";
|
|
10
|
+
export type { Faculty, Group, ScheduleEntry, FullScheduleSlot, FullScheduleDay, LessonTimeSlot, Lesson, LessonTime, SemesterWeek, Substitution, TransferInfo, TeacherInfo, TtClientOptions, CacheConfig, } from "./tt/types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { LkClient } from "./lk/client.js";
|
|
2
2
|
export { TtClient } from "./tt/client.js";
|
|
3
3
|
export { Schedule } from "./tt/schedule.js";
|
|
4
|
-
export { getCurrentPeriod, isSessionPeriod, getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, getTimeSlots, getLessonNumber, getAdjacentSemester, isHoliday, RUSSIAN_HOLIDAYS, } from "./tt/utils.js";
|
|
4
|
+
export { getCurrentPeriod, isSessionPeriod, getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, getTimeSlots, getLessonNumber, getAdjacentSemester, isHoliday, getEffectiveHolidays, getHolidayTransfers, getCompensatingWorkDays, RUSSIAN_HOLIDAYS, } from "./tt/utils.js";
|
|
5
5
|
export { AuthError, ParseError, } from "./common/types.js";
|
package/dist/tt/client.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CacheEntry } from "../common/cache.js";
|
|
2
2
|
import { Period } from "../common/types.js";
|
|
3
3
|
import { Schedule } from "./schedule.js";
|
|
4
|
-
import type { Faculty, Group, TtClientOptions, CacheConfig } from "./types.js";
|
|
4
|
+
import type { Faculty, Group, TeacherInfo, TtClientOptions, CacheConfig } from "./types.js";
|
|
5
5
|
export declare class TtClient {
|
|
6
6
|
private http;
|
|
7
7
|
private educationType;
|
|
@@ -47,4 +47,15 @@ export declare class TtClient {
|
|
|
47
47
|
id: number;
|
|
48
48
|
name: string;
|
|
49
49
|
}[]>;
|
|
50
|
+
getTeachers(): Promise<{
|
|
51
|
+
id: number;
|
|
52
|
+
name: string;
|
|
53
|
+
}[]>;
|
|
54
|
+
private fetchTeacherSchedule;
|
|
55
|
+
getTeacherSchedule(teacherId: number): Promise<Schedule>;
|
|
56
|
+
getTeacherScheduleForPeriod(opts: {
|
|
57
|
+
teacherId: number;
|
|
58
|
+
period: Period;
|
|
59
|
+
}): Promise<Schedule>;
|
|
60
|
+
getTeacherInfo(teacherId: number): Promise<TeacherInfo | null>;
|
|
50
61
|
}
|
package/dist/tt/client.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { HttpClient } from "../common/http.js";
|
|
2
2
|
import { Cache } from "../common/cache.js";
|
|
3
3
|
import { AuthError } from "../common/types.js";
|
|
4
|
-
import { parseGroupButtons, parseFacultyButtons, parseTeacherButtons, parseFullSchedule, } from "./parse.js";
|
|
4
|
+
import { parseGroupButtons, parseFacultyButtons, parseTeacherButtons, parseFullSchedule, parseTeacherFullSchedule, parseTeacherInfo, } from "./parse.js";
|
|
5
5
|
import { Schedule } from "./schedule.js";
|
|
6
6
|
const BASE = "https://tt.chuvsu.ru";
|
|
7
7
|
const AUTH_URL = `${BASE}/auth`;
|
|
@@ -173,4 +173,47 @@ export class TtClient {
|
|
|
173
173
|
});
|
|
174
174
|
return parseTeacherButtons(body);
|
|
175
175
|
}
|
|
176
|
+
// --- Teacher schedule ---
|
|
177
|
+
async getTeachers() {
|
|
178
|
+
const cached = this.cache?.get("teachers", "all");
|
|
179
|
+
if (cached)
|
|
180
|
+
return cached;
|
|
181
|
+
const { body } = await this.authGet(`${BASE}/index/tech`);
|
|
182
|
+
const data = parseTeacherButtons(body);
|
|
183
|
+
this.cache?.set("teachers", "all", data);
|
|
184
|
+
return data;
|
|
185
|
+
}
|
|
186
|
+
async fetchTeacherSchedule(teacherId, period) {
|
|
187
|
+
const cacheKey = `teacher:${teacherId}:${period}`;
|
|
188
|
+
const cached = this.cache?.get("schedule", cacheKey);
|
|
189
|
+
if (cached)
|
|
190
|
+
return cached;
|
|
191
|
+
const url = `${BASE}/index/techtt/tech/${teacherId}`;
|
|
192
|
+
const { body } = await this.authPost(url, { htype: String(period) });
|
|
193
|
+
const days = parseTeacherFullSchedule(body, this.educationType);
|
|
194
|
+
this.cache?.set("schedule", cacheKey, days);
|
|
195
|
+
return days;
|
|
196
|
+
}
|
|
197
|
+
async getTeacherSchedule(teacherId) {
|
|
198
|
+
const schedules = new Map();
|
|
199
|
+
const results = await Promise.all(ALL_PERIODS.map(async (period) => {
|
|
200
|
+
const days = await this.fetchTeacherSchedule(teacherId, period);
|
|
201
|
+
return { period, days };
|
|
202
|
+
}));
|
|
203
|
+
for (const { period, days } of results) {
|
|
204
|
+
schedules.set(period, days);
|
|
205
|
+
}
|
|
206
|
+
return new Schedule(teacherId, schedules, undefined, this.educationType);
|
|
207
|
+
}
|
|
208
|
+
async getTeacherScheduleForPeriod(opts) {
|
|
209
|
+
const days = await this.fetchTeacherSchedule(opts.teacherId, opts.period);
|
|
210
|
+
const schedules = new Map();
|
|
211
|
+
schedules.set(opts.period, days);
|
|
212
|
+
return new Schedule(opts.teacherId, schedules, opts.period, this.educationType);
|
|
213
|
+
}
|
|
214
|
+
async getTeacherInfo(teacherId) {
|
|
215
|
+
const url = `${BASE}/index/techtt/tech/${teacherId}`;
|
|
216
|
+
const { body } = await this.authGet(url);
|
|
217
|
+
return parseTeacherInfo(body);
|
|
218
|
+
}
|
|
176
219
|
}
|
package/dist/tt/parse.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Period, EducationType } from "../common/types.js";
|
|
2
|
-
import type { Faculty, Group, FullScheduleDay } from "./types.js";
|
|
2
|
+
import type { Faculty, Group, FullScheduleDay, TeacherInfo } from "./types.js";
|
|
3
3
|
export declare function parsePeriodFromPage(html: string): Period | null;
|
|
4
4
|
export declare function parseGroupButtons(html: string): Group[];
|
|
5
5
|
export declare function parseFacultyButtons(html: string): Faculty[];
|
|
@@ -8,3 +8,5 @@ export declare function parseTeacherButtons(html: string): {
|
|
|
8
8
|
name: string;
|
|
9
9
|
}[];
|
|
10
10
|
export declare function parseFullSchedule(html: string, educationType?: EducationType): FullScheduleDay[];
|
|
11
|
+
export declare function parseTeacherFullSchedule(html: string, educationType?: EducationType): FullScheduleDay[];
|
|
12
|
+
export declare function parseTeacherInfo(html: string): TeacherInfo | null;
|
package/dist/tt/parse.js
CHANGED
|
@@ -65,7 +65,7 @@ export function parseFullSchedule(html, educationType) {
|
|
|
65
65
|
return parseSemesterSchedule(doc);
|
|
66
66
|
}
|
|
67
67
|
// --- Semester schedule parsing (weekday-based, repeating weekly) ---
|
|
68
|
-
function
|
|
68
|
+
function parseSemesterScheduleWith(doc, entryParser) {
|
|
69
69
|
const days = [];
|
|
70
70
|
const rows = doc.querySelectorAll("tr");
|
|
71
71
|
let currentDay = null;
|
|
@@ -96,7 +96,7 @@ function parseSemesterSchedule(doc) {
|
|
|
96
96
|
continue;
|
|
97
97
|
const entries = [];
|
|
98
98
|
for (const entryRow of dataCell.querySelectorAll("table tr")) {
|
|
99
|
-
const entry =
|
|
99
|
+
const entry = entryParser(entryRow);
|
|
100
100
|
if (entry)
|
|
101
101
|
entries.push(entry);
|
|
102
102
|
}
|
|
@@ -109,6 +109,9 @@ function parseSemesterSchedule(doc) {
|
|
|
109
109
|
}
|
|
110
110
|
return days;
|
|
111
111
|
}
|
|
112
|
+
function parseSemesterSchedule(doc) {
|
|
113
|
+
return parseSemesterScheduleWith(doc, parseSemesterEntry);
|
|
114
|
+
}
|
|
112
115
|
function parseDate(dd, mm, yyyy) {
|
|
113
116
|
return new Date(parseInt(yyyy), parseInt(mm) - 1, parseInt(dd));
|
|
114
117
|
}
|
|
@@ -291,3 +294,149 @@ function parseSessionEntry(td) {
|
|
|
291
294
|
timeEnd: parseTime(timeMatch[2]),
|
|
292
295
|
};
|
|
293
296
|
}
|
|
297
|
+
// --- Teacher schedule parsing ---
|
|
298
|
+
export function parseTeacherFullSchedule(html, educationType) {
|
|
299
|
+
const doc = parseHtml(html);
|
|
300
|
+
const edType = educationType ?? 1 /* EducationType.HigherEducation */;
|
|
301
|
+
if (doc.querySelector('td[id^="trd2"]')) {
|
|
302
|
+
return parseTeacherSessionSchedule(doc, edType);
|
|
303
|
+
}
|
|
304
|
+
return parseSemesterScheduleWith(doc, parseTeacherSemesterEntry);
|
|
305
|
+
}
|
|
306
|
+
function parseTeacherSemesterEntry(el) {
|
|
307
|
+
const td = el.querySelector("td") ?? el;
|
|
308
|
+
const fullHtml = td.innerHTML ?? "";
|
|
309
|
+
const plainText = text(td);
|
|
310
|
+
if (!plainText)
|
|
311
|
+
return null;
|
|
312
|
+
const possibleChanges = (td.getAttribute("class") ?? "").includes("want") || undefined;
|
|
313
|
+
const redDivs = td.querySelectorAll('div[style*="border: 2px solid red"]');
|
|
314
|
+
for (const div of redDivs) {
|
|
315
|
+
const result = parseTransferDiv(div);
|
|
316
|
+
if (result) {
|
|
317
|
+
if (possibleChanges)
|
|
318
|
+
result.entry.possibleChanges = true;
|
|
319
|
+
return result.entry;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const substitutions = [];
|
|
323
|
+
for (const div of redDivs) {
|
|
324
|
+
const sub = parseSubstitutionDiv(div);
|
|
325
|
+
if (sub)
|
|
326
|
+
substitutions.push(sub);
|
|
327
|
+
}
|
|
328
|
+
let cleanHtml = fullHtml;
|
|
329
|
+
let cleanText = plainText;
|
|
330
|
+
for (const div of redDivs) {
|
|
331
|
+
cleanHtml = cleanHtml.replace(div.outerHTML ?? "", "");
|
|
332
|
+
cleanText = cleanText.replace(text(div), "");
|
|
333
|
+
}
|
|
334
|
+
const subjectEl = td.querySelector('span[style*="color: blue"]');
|
|
335
|
+
const subject = subjectEl ? text(subjectEl) : "";
|
|
336
|
+
if (!subject)
|
|
337
|
+
return null;
|
|
338
|
+
const typeMatch = cleanText.match(/\((лк|пр|лб|зач|экз|зчО|кр|конс)\)/);
|
|
339
|
+
const weeksMatch = cleanText.match(/\(([^)]*нед\.?[^)]*)\)/);
|
|
340
|
+
const roomMatch = cleanHtml.match(/(?:<sup>[^<]*<\/sup>)?([А-Яа-яA-Za-z]-\d+)/);
|
|
341
|
+
const groupsMatch = cleanHtml.match(/<br\s*\/?>\s*([^<]+?)(?:<br|<\/td|<div|<i|$)/);
|
|
342
|
+
const subgroupMatch = cleanText.match(/(\d+)\s*подгруппа/);
|
|
343
|
+
const weekParity = parseWeekParity(cleanHtml);
|
|
344
|
+
return {
|
|
345
|
+
room: roomMatch?.[1] ?? "",
|
|
346
|
+
subject,
|
|
347
|
+
type: typeMatch?.[1] ?? "",
|
|
348
|
+
weeks: parseWeeks(weeksMatch?.[1] ?? ""),
|
|
349
|
+
teacher: { name: "" },
|
|
350
|
+
groups: groupsMatch?.[1]?.trim() ?? "",
|
|
351
|
+
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
352
|
+
weekParity,
|
|
353
|
+
substitutions: substitutions.length > 0 ? substitutions : undefined,
|
|
354
|
+
possibleChanges,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function parseTeacherSessionSchedule(doc, educationType) {
|
|
358
|
+
const days = [];
|
|
359
|
+
for (const dateCell of doc.querySelectorAll('td[id^="trd2"]')) {
|
|
360
|
+
const id = dateCell.getAttribute("id") ?? "";
|
|
361
|
+
const dateMatch = id.match(/trd(\d{4})(\d{2})(\d{2})/);
|
|
362
|
+
if (!dateMatch)
|
|
363
|
+
continue;
|
|
364
|
+
const year = parseInt(dateMatch[1]);
|
|
365
|
+
const month = parseInt(dateMatch[2]) - 1;
|
|
366
|
+
const dayNum = parseInt(dateMatch[3]);
|
|
367
|
+
const date = new Date(year, month, dayNum);
|
|
368
|
+
const cellHtml = dateCell.innerHTML ?? "";
|
|
369
|
+
const brMatch = cellHtml.match(/<br\s*\/?>\s*(.+)/i);
|
|
370
|
+
const weekday = brMatch ? brMatch[1].trim() : "";
|
|
371
|
+
const row = dateCell.parentElement;
|
|
372
|
+
if (!row)
|
|
373
|
+
continue;
|
|
374
|
+
const dataCell = row.querySelector("td.trdata:not(.trfd)");
|
|
375
|
+
if (!dataCell)
|
|
376
|
+
continue;
|
|
377
|
+
const slots = [];
|
|
378
|
+
for (const entryRow of dataCell.querySelectorAll("table tr")) {
|
|
379
|
+
const td = entryRow.querySelector("td") ?? entryRow;
|
|
380
|
+
const entry = parseTeacherSessionEntry(td);
|
|
381
|
+
if (!entry)
|
|
382
|
+
continue;
|
|
383
|
+
slots.push({
|
|
384
|
+
number: getLessonNumber(entry.timeStart, educationType),
|
|
385
|
+
timeStart: entry.timeStart,
|
|
386
|
+
timeEnd: entry.timeEnd,
|
|
387
|
+
entries: [entry.entry],
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
if (slots.length > 0) {
|
|
391
|
+
days.push({ weekday, date, slots });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return days;
|
|
395
|
+
}
|
|
396
|
+
function parseTeacherSessionEntry(td) {
|
|
397
|
+
const fullHtml = td.innerHTML ?? "";
|
|
398
|
+
const plainText = text(td);
|
|
399
|
+
if (!plainText)
|
|
400
|
+
return null;
|
|
401
|
+
const subjectEl = td.querySelector('span[style*="color: blue"]');
|
|
402
|
+
const subject = subjectEl ? text(subjectEl) : "";
|
|
403
|
+
if (!subject)
|
|
404
|
+
return null;
|
|
405
|
+
const roomMatch = fullHtml.match(/^([^<]*?)\s*<span/);
|
|
406
|
+
const room = roomMatch ? roomMatch[1].trim() : "";
|
|
407
|
+
const typeMatch = plainText.match(/\((лк|пр|лб|зач|экз|зчО|кр|конс\.?|Экз)\)/i);
|
|
408
|
+
const type = typeMatch ? typeMatch[1].replace(/\.$/, "").toLowerCase() : "";
|
|
409
|
+
// Groups: text between </span> type and <br>time
|
|
410
|
+
const groupsMatch = fullHtml.match(/\((?:лк|пр|лб|зач|экз|зчО|кр|конс\.?|Экз)\)\s*([^<]+?)\s*<br/i);
|
|
411
|
+
const timeMatch = fullHtml.match(/<br\s*\/?>\s*(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/);
|
|
412
|
+
if (!timeMatch)
|
|
413
|
+
return null;
|
|
414
|
+
return {
|
|
415
|
+
entry: {
|
|
416
|
+
room,
|
|
417
|
+
subject,
|
|
418
|
+
type,
|
|
419
|
+
weeks: { from: 0, to: 0 },
|
|
420
|
+
teacher: { name: "" },
|
|
421
|
+
groups: groupsMatch?.[1]?.trim() ?? "",
|
|
422
|
+
},
|
|
423
|
+
timeStart: parseTime(timeMatch[1]),
|
|
424
|
+
timeEnd: parseTime(timeMatch[2]),
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
export function parseTeacherInfo(html) {
|
|
428
|
+
const doc = parseHtml(html);
|
|
429
|
+
const nameEl = doc.querySelector(".htextb");
|
|
430
|
+
if (!nameEl)
|
|
431
|
+
return null;
|
|
432
|
+
const nameHtml = nameEl.innerHTML ?? "";
|
|
433
|
+
const nameMatch = nameHtml.match(/^([^<]+)/);
|
|
434
|
+
const name = nameMatch?.[1]?.trim() ?? "";
|
|
435
|
+
if (!name)
|
|
436
|
+
return null;
|
|
437
|
+
const degreeEl = nameEl.querySelector('span[style*="color: blue"]');
|
|
438
|
+
const degree = degreeEl ? text(degreeEl).trim() : undefined;
|
|
439
|
+
const deptEl = doc.querySelector(".htext");
|
|
440
|
+
const department = deptEl ? text(deptEl).trim() : undefined;
|
|
441
|
+
return { name, degree: degree || undefined, department: department || undefined };
|
|
442
|
+
}
|
package/dist/tt/schedule.d.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import type { FullScheduleDay, SemesterWeek, Lesson } from "./types.js";
|
|
2
2
|
import { Period, EducationType } from "../common/types.js";
|
|
3
|
-
import { type Holiday } from "./utils.js";
|
|
3
|
+
import { type Holiday, type HolidayTransfer } from "./utils.js";
|
|
4
4
|
export declare class Schedule {
|
|
5
5
|
readonly groupId: number;
|
|
6
6
|
readonly scheduleMap: Map<number, FullScheduleDay[]>;
|
|
7
7
|
readonly educationType: EducationType;
|
|
8
8
|
/** List of holidays to exclude from schedule queries. Pass `[]` to disable. */
|
|
9
9
|
readonly holidays: Holiday[];
|
|
10
|
+
/** Government decree day-off transfers (Постановление Правительства). */
|
|
11
|
+
readonly holidayTransfers: HolidayTransfer[];
|
|
10
12
|
private _period?;
|
|
11
|
-
constructor(groupId: number, scheduleMap: Map<number, FullScheduleDay[]>, period?: Period, educationType?: EducationType, holidays?: Holiday[] | null);
|
|
13
|
+
constructor(groupId: number, scheduleMap: Map<number, FullScheduleDay[]>, period?: Period, educationType?: EducationType, holidays?: Holiday[] | null, holidayTransfers?: HolidayTransfer[]);
|
|
12
14
|
/** Current (or fixed) period for this schedule. */
|
|
13
15
|
get period(): Period;
|
|
14
16
|
/** Days for the current period. */
|
package/dist/tt/schedule.js
CHANGED
|
@@ -5,13 +5,16 @@ export class Schedule {
|
|
|
5
5
|
educationType;
|
|
6
6
|
/** List of holidays to exclude from schedule queries. Pass `[]` to disable. */
|
|
7
7
|
holidays;
|
|
8
|
+
/** Government decree day-off transfers (Постановление Правительства). */
|
|
9
|
+
holidayTransfers;
|
|
8
10
|
_period;
|
|
9
|
-
constructor(groupId, scheduleMap, period, educationType, holidays) {
|
|
11
|
+
constructor(groupId, scheduleMap, period, educationType, holidays, holidayTransfers) {
|
|
10
12
|
this.groupId = groupId;
|
|
11
13
|
this.scheduleMap = scheduleMap;
|
|
12
14
|
this.educationType = educationType ?? 1 /* EducationType.HigherEducation */;
|
|
13
15
|
this._period = period;
|
|
14
16
|
this.holidays = holidays ?? RUSSIAN_HOLIDAYS;
|
|
17
|
+
this.holidayTransfers = holidayTransfers ?? [];
|
|
15
18
|
}
|
|
16
19
|
/** Current (or fixed) period for this schedule. */
|
|
17
20
|
get period() {
|
|
@@ -80,7 +83,7 @@ export class Schedule {
|
|
|
80
83
|
return slotsToLessons(slots, date);
|
|
81
84
|
}
|
|
82
85
|
forDate(date, opts) {
|
|
83
|
-
if (isHoliday(date, this.holidays))
|
|
86
|
+
if (isHoliday(date, this.holidays, this.holidayTransfers))
|
|
84
87
|
return [];
|
|
85
88
|
const period = getCurrentPeriod({ date });
|
|
86
89
|
const lessons = [];
|
package/dist/tt/types.d.ts
CHANGED
|
@@ -35,6 +35,8 @@ export interface ScheduleEntry {
|
|
|
35
35
|
type: string;
|
|
36
36
|
weeks: WeekRange;
|
|
37
37
|
teacher: Teacher;
|
|
38
|
+
/** For teacher schedules: group names (e.g. "КТ-42-25 (1 подгруппа)"). */
|
|
39
|
+
groups?: string;
|
|
38
40
|
subgroup?: number;
|
|
39
41
|
weekParity?: "even" | "odd";
|
|
40
42
|
/** Date-specific substitutions (замена на). */
|
|
@@ -73,6 +75,8 @@ export interface Lesson {
|
|
|
73
75
|
type: string;
|
|
74
76
|
room: string;
|
|
75
77
|
teacher: Teacher;
|
|
78
|
+
/** For teacher schedules: group names. */
|
|
79
|
+
groups?: string;
|
|
76
80
|
weeks: WeekRange;
|
|
77
81
|
subgroup?: number;
|
|
78
82
|
weekParity?: "even" | "odd";
|
|
@@ -85,6 +89,12 @@ export interface Lesson {
|
|
|
85
89
|
/** Whether this lesson is marked as potentially changing. */
|
|
86
90
|
possibleChanges?: boolean;
|
|
87
91
|
}
|
|
92
|
+
/** Teacher info from the schedule page header. */
|
|
93
|
+
export interface TeacherInfo {
|
|
94
|
+
name: string;
|
|
95
|
+
degree?: string;
|
|
96
|
+
department?: string;
|
|
97
|
+
}
|
|
88
98
|
export interface SemesterWeek {
|
|
89
99
|
week: number;
|
|
90
100
|
start: Date;
|
|
@@ -94,6 +104,7 @@ export interface CacheConfig {
|
|
|
94
104
|
schedule?: number;
|
|
95
105
|
faculties?: number;
|
|
96
106
|
groups?: number;
|
|
107
|
+
teachers?: number;
|
|
97
108
|
}
|
|
98
109
|
export interface TtClientOptions {
|
|
99
110
|
educationType?: EducationType;
|
package/dist/tt/utils.d.ts
CHANGED
|
@@ -54,10 +54,50 @@ export interface Holiday {
|
|
|
54
54
|
/** Human-readable name. */
|
|
55
55
|
name: string;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* A government-decree day-off transfer (Постановление Правительства).
|
|
59
|
+
* Moves a day off from one date to another.
|
|
60
|
+
*/
|
|
61
|
+
export interface HolidayTransfer {
|
|
62
|
+
/** The date that becomes a day off. */
|
|
63
|
+
dayOff: Date;
|
|
64
|
+
/** The date that becomes a working day (e.g. a Saturday). `null` if no compensating work day. */
|
|
65
|
+
workDay: Date | null;
|
|
66
|
+
}
|
|
57
67
|
/** Russian non-working public holidays (Статья 112 ТК РФ). */
|
|
58
68
|
export declare const RUSSIAN_HOLIDAYS: Holiday[];
|
|
59
69
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
70
|
+
* Compute effective non-working holiday dates for a given year.
|
|
71
|
+
*
|
|
72
|
+
* Rules (Art. 112 ТК РФ):
|
|
73
|
+
* 1. All holidays in the list are non-working days.
|
|
74
|
+
* 2. For non-January holidays: if a holiday falls on Sat/Sun, the day off
|
|
75
|
+
* automatically transfers to the next working day.
|
|
76
|
+
* 3. For January holidays (1–8): weekend transfers are NOT automatic —
|
|
77
|
+
* they are decided by annual government decree. Pass them via `transfers`.
|
|
78
|
+
* 4. Bridge days: if a non-January holiday falls on Tue, Mon is day off
|
|
79
|
+
* (preceding Sat works); if on Thu, Fri is day off (following Sat works).
|
|
80
|
+
* Computed automatically, can be overridden via `transfers`.
|
|
81
|
+
* 5. Government decree transfers (`transfers`) add extra days off and
|
|
82
|
+
* override auto-computed bridge days.
|
|
83
|
+
* 6. For 6-day week (`sixDayWeek`): Saturday is a work day, so
|
|
84
|
+
* Thursday bridges don't apply and Saturday holidays don't auto-transfer.
|
|
85
|
+
*/
|
|
86
|
+
export declare function getEffectiveHolidays(year: number, holidays?: Holiday[], transfers?: HolidayTransfer[], sixDayWeek?: boolean): Date[];
|
|
87
|
+
/**
|
|
88
|
+
* Compute all transfers (auto bridges + explicit) for a given year.
|
|
89
|
+
* Useful for getting compensating work days (Saturdays that become working).
|
|
90
|
+
*/
|
|
91
|
+
export declare function getHolidayTransfers(year: number, holidays: Holiday[] | undefined, transfers: HolidayTransfer[] | undefined, sixDayWeek: boolean): HolidayTransfer[];
|
|
92
|
+
/**
|
|
93
|
+
* Returns the list of compensating work days (e.g. Saturdays that become working).
|
|
94
|
+
* Includes both auto-computed bridge compensations and explicit transfers.
|
|
95
|
+
*/
|
|
96
|
+
export declare function getCompensatingWorkDays(year: number, holidays: Holiday[] | undefined, transfers: HolidayTransfer[] | undefined, sixDayWeek: boolean): Date[];
|
|
97
|
+
/**
|
|
98
|
+
* Returns true if the given date is a non-working holiday,
|
|
99
|
+
* including transferred holidays when they fall on weekends (Art. 112 ТК РФ)
|
|
100
|
+
* and auto-computed bridge days.
|
|
101
|
+
* Pass an empty array for `holidays` to disable holiday checking.
|
|
62
102
|
*/
|
|
63
|
-
export declare function isHoliday(date: Date, holidays?: Holiday[]): boolean;
|
|
103
|
+
export declare function isHoliday(date: Date, holidays?: Holiday[], transfers?: HolidayTransfer[], sixDayWeek?: boolean): boolean;
|
package/dist/tt/utils.js
CHANGED
|
@@ -165,6 +165,7 @@ export function slotsToLessons(slots, date) {
|
|
|
165
165
|
type: entry.type,
|
|
166
166
|
room,
|
|
167
167
|
teacher,
|
|
168
|
+
groups: entry.groups,
|
|
168
169
|
weeks: entry.weeks,
|
|
169
170
|
subgroup: entry.subgroup,
|
|
170
171
|
weekParity: entry.weekParity,
|
|
@@ -324,11 +325,185 @@ export const RUSSIAN_HOLIDAYS = [
|
|
|
324
325
|
{ month: 11, day: 4, name: "День народного единства" },
|
|
325
326
|
];
|
|
326
327
|
/**
|
|
327
|
-
*
|
|
328
|
-
*
|
|
328
|
+
* January holiday dates (1–8) are excluded from automatic weekend transfer
|
|
329
|
+
* per Art. 112 ТК РФ. Their transfers are decided by government decree.
|
|
329
330
|
*/
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
331
|
+
function isJanuaryHoliday(h) {
|
|
332
|
+
return h.month === 1 && h.day >= 1 && h.day <= 8;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Compute bridge-day transfers for non-January holidays.
|
|
336
|
+
*
|
|
337
|
+
* Pattern (consistent across government decrees):
|
|
338
|
+
* - Holiday on Tuesday → Monday becomes day off, preceding Saturday is work day
|
|
339
|
+
* - Holiday on Thursday → Friday becomes day off, following Saturday is work day
|
|
340
|
+
* (only for 5-day week; 6-day week has Saturday classes, so no gap to bridge)
|
|
341
|
+
*
|
|
342
|
+
* January holidays are excluded (their 2 transfers are unpredictable).
|
|
343
|
+
*/
|
|
344
|
+
function computeBridgeDays(year, holidays, effectiveDays, sixDayWeek) {
|
|
345
|
+
const bridges = [];
|
|
346
|
+
for (const h of holidays) {
|
|
347
|
+
if (isJanuaryHoliday(h))
|
|
348
|
+
continue;
|
|
349
|
+
const date = new Date(year, h.month - 1, h.day);
|
|
350
|
+
const dow = date.getDay();
|
|
351
|
+
if (dow === 2) {
|
|
352
|
+
// Tuesday → Monday off, preceding Saturday works
|
|
353
|
+
const monday = new Date(date);
|
|
354
|
+
monday.setDate(date.getDate() - 1);
|
|
355
|
+
const saturday = new Date(date);
|
|
356
|
+
saturday.setDate(date.getDate() - 3);
|
|
357
|
+
if (!effectiveDays.some((d) => isSameDay(d, monday))) {
|
|
358
|
+
bridges.push({
|
|
359
|
+
dayOff: monday,
|
|
360
|
+
workDay: sixDayWeek ? null : saturday,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
else if (dow === 4 && !sixDayWeek) {
|
|
365
|
+
// Thursday → Friday off, following Saturday works
|
|
366
|
+
// Only for 5-day week: 6-day week has no gap (Saturday is a work day)
|
|
367
|
+
const friday = new Date(date);
|
|
368
|
+
friday.setDate(date.getDate() + 1);
|
|
369
|
+
const saturday = new Date(date);
|
|
370
|
+
saturday.setDate(date.getDate() + 2);
|
|
371
|
+
if (!effectiveDays.some((d) => isSameDay(d, friday))) {
|
|
372
|
+
bridges.push({ dayOff: friday, workDay: saturday });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return bridges;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Compute effective non-working holiday dates for a given year.
|
|
380
|
+
*
|
|
381
|
+
* Rules (Art. 112 ТК РФ):
|
|
382
|
+
* 1. All holidays in the list are non-working days.
|
|
383
|
+
* 2. For non-January holidays: if a holiday falls on Sat/Sun, the day off
|
|
384
|
+
* automatically transfers to the next working day.
|
|
385
|
+
* 3. For January holidays (1–8): weekend transfers are NOT automatic —
|
|
386
|
+
* they are decided by annual government decree. Pass them via `transfers`.
|
|
387
|
+
* 4. Bridge days: if a non-January holiday falls on Tue, Mon is day off
|
|
388
|
+
* (preceding Sat works); if on Thu, Fri is day off (following Sat works).
|
|
389
|
+
* Computed automatically, can be overridden via `transfers`.
|
|
390
|
+
* 5. Government decree transfers (`transfers`) add extra days off and
|
|
391
|
+
* override auto-computed bridge days.
|
|
392
|
+
* 6. For 6-day week (`sixDayWeek`): Saturday is a work day, so
|
|
393
|
+
* Thursday bridges don't apply and Saturday holidays don't auto-transfer.
|
|
394
|
+
*/
|
|
395
|
+
export function getEffectiveHolidays(year, holidays = RUSSIAN_HOLIDAYS, transfers = [], sixDayWeek = true) {
|
|
396
|
+
const originalDates = holidays.map((h) => new Date(year, h.month - 1, h.day));
|
|
397
|
+
const isOriginalHoliday = (d) => originalDates.some((od) => isSameDay(od, d));
|
|
398
|
+
const effectiveDays = [...originalDates];
|
|
399
|
+
// Auto-transfer: only non-January holidays that fall on weekends
|
|
400
|
+
const nonJanuaryOnWeekend = holidays
|
|
401
|
+
.filter((h) => !isJanuaryHoliday(h))
|
|
402
|
+
.map((h) => new Date(year, h.month - 1, h.day))
|
|
403
|
+
.filter((d) => d.getDay() === 0 || d.getDay() === 6)
|
|
404
|
+
.sort((a, b) => a.getTime() - b.getTime());
|
|
405
|
+
for (const holiday of nonJanuaryOnWeekend) {
|
|
406
|
+
let candidate = new Date(holiday);
|
|
407
|
+
if (candidate.getDay() === 6) {
|
|
408
|
+
candidate.setDate(candidate.getDate() + 2);
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
candidate.setDate(candidate.getDate() + 1);
|
|
412
|
+
}
|
|
413
|
+
while (candidate.getDay() === 0 ||
|
|
414
|
+
candidate.getDay() === 6 ||
|
|
415
|
+
isOriginalHoliday(candidate) ||
|
|
416
|
+
effectiveDays.some((ed) => isSameDay(ed, candidate))) {
|
|
417
|
+
candidate.setDate(candidate.getDate() + 1);
|
|
418
|
+
}
|
|
419
|
+
effectiveDays.push(new Date(candidate));
|
|
420
|
+
}
|
|
421
|
+
// Bridge days (auto-computed, can be overridden)
|
|
422
|
+
const autoBridges = computeBridgeDays(year, holidays, effectiveDays, sixDayWeek);
|
|
423
|
+
// Merge: explicit transfers override auto-computed bridges
|
|
424
|
+
const allTransfers = [...autoBridges];
|
|
425
|
+
for (const t of transfers) {
|
|
426
|
+
const idx = allTransfers.findIndex((b) => isSameDay(b.dayOff, t.dayOff));
|
|
427
|
+
if (idx !== -1) {
|
|
428
|
+
allTransfers[idx] = t;
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
allTransfers.push(t);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const compensatingWorkDays = [];
|
|
435
|
+
for (const t of allTransfers) {
|
|
436
|
+
if (!effectiveDays.some((d) => isSameDay(d, t.dayOff))) {
|
|
437
|
+
effectiveDays.push(t.dayOff);
|
|
438
|
+
}
|
|
439
|
+
if (t.workDay) {
|
|
440
|
+
compensatingWorkDays.push(t.workDay);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return effectiveDays.sort((a, b) => a.getTime() - b.getTime());
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Compute all transfers (auto bridges + explicit) for a given year.
|
|
447
|
+
* Useful for getting compensating work days (Saturdays that become working).
|
|
448
|
+
*/
|
|
449
|
+
export function getHolidayTransfers(year, holidays = RUSSIAN_HOLIDAYS, transfers = [], sixDayWeek) {
|
|
450
|
+
const originalDates = holidays.map((h) => new Date(year, h.month - 1, h.day));
|
|
451
|
+
const effectiveDays = [...originalDates];
|
|
452
|
+
// Replay weekend transfers to build effectiveDays for bridge computation
|
|
453
|
+
const nonJanuaryOnWeekend = holidays
|
|
454
|
+
.filter((h) => !isJanuaryHoliday(h))
|
|
455
|
+
.map((h) => new Date(year, h.month - 1, h.day))
|
|
456
|
+
.filter((d) => d.getDay() === 0 || d.getDay() === 6)
|
|
457
|
+
.sort((a, b) => a.getTime() - b.getTime());
|
|
458
|
+
const isOriginalHoliday = (d) => originalDates.some((od) => isSameDay(od, d));
|
|
459
|
+
for (const holiday of nonJanuaryOnWeekend) {
|
|
460
|
+
let candidate = new Date(holiday);
|
|
461
|
+
if (candidate.getDay() === 6) {
|
|
462
|
+
candidate.setDate(candidate.getDate() + 2);
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
candidate.setDate(candidate.getDate() + 1);
|
|
466
|
+
}
|
|
467
|
+
while (candidate.getDay() === 0 ||
|
|
468
|
+
candidate.getDay() === 6 ||
|
|
469
|
+
isOriginalHoliday(candidate) ||
|
|
470
|
+
effectiveDays.some((ed) => isSameDay(ed, candidate))) {
|
|
471
|
+
candidate.setDate(candidate.getDate() + 1);
|
|
472
|
+
}
|
|
473
|
+
effectiveDays.push(new Date(candidate));
|
|
474
|
+
}
|
|
475
|
+
const autoBridges = computeBridgeDays(year, holidays, effectiveDays, sixDayWeek);
|
|
476
|
+
const allTransfers = [...autoBridges];
|
|
477
|
+
for (const t of transfers) {
|
|
478
|
+
const idx = allTransfers.findIndex((b) => isSameDay(b.dayOff, t.dayOff));
|
|
479
|
+
if (idx !== -1) {
|
|
480
|
+
allTransfers[idx] = t;
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
allTransfers.push(t);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return allTransfers;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Returns the list of compensating work days (e.g. Saturdays that become working).
|
|
490
|
+
* Includes both auto-computed bridge compensations and explicit transfers.
|
|
491
|
+
*/
|
|
492
|
+
export function getCompensatingWorkDays(year, holidays = RUSSIAN_HOLIDAYS, transfers = [], sixDayWeek) {
|
|
493
|
+
return getHolidayTransfers(year, holidays, transfers, sixDayWeek)
|
|
494
|
+
.filter((t) => t.workDay != null)
|
|
495
|
+
.map((t) => t.workDay);
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Returns true if the given date is a non-working holiday,
|
|
499
|
+
* including transferred holidays when they fall on weekends (Art. 112 ТК РФ)
|
|
500
|
+
* and auto-computed bridge days.
|
|
501
|
+
* Pass an empty array for `holidays` to disable holiday checking.
|
|
502
|
+
*/
|
|
503
|
+
export function isHoliday(date, holidays = RUSSIAN_HOLIDAYS, transfers = [], sixDayWeek = true) {
|
|
504
|
+
if (holidays.length === 0 && transfers.length === 0)
|
|
505
|
+
return false;
|
|
506
|
+
const year = date.getFullYear();
|
|
507
|
+
const effectiveDays = getEffectiveHolidays(year, holidays, transfers, sixDayWeek);
|
|
508
|
+
return effectiveDays.some((d) => isSameDay(d, date));
|
|
334
509
|
}
|