chuvsu-js 2.3.0 → 2.4.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 CHANGED
@@ -6,8 +6,8 @@ Node.js библиотека для работы с порталами ЧувГ
6
6
  - **lk.chuvsu.ru** — личный кабинет студента (персональные данные)
7
7
 
8
8
  > [!WARNING]
9
- > Пока не доработана, код говно, замены занятий пока не парсит.
10
- > Не надейтесь на правильный вывод расписания.
9
+ > Пока не доработана, код и архитектура говно и надо бы его 10 раз переписать.
10
+ > Не надейтесь на правильный вывод расписания, но впринципе я не замечал пока расхождений.
11
11
 
12
12
  ## Установка
13
13
 
package/dist/browser.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  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
- export type { Time, WeekRange, Teacher, } from "./common/types.js";
5
- export type { Faculty, Group, ScheduleEntry, FullScheduleSlot, FullScheduleDay, LessonTimeSlot, Lesson, LessonTime, SemesterWeek, } from "./tt/types.js";
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";
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export { Schedule } from "./tt/schedule.js";
4
4
  export type { CacheEntry } from "./common/cache.js";
5
5
  export { getCurrentPeriod, isSessionPeriod, getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, getTimeSlots, getLessonNumber, getAdjacentSemester, isHoliday, RUSSIAN_HOLIDAYS, } from "./tt/utils.js";
6
6
  export type { Holiday } from "./tt/utils.js";
7
- export { Period, EducationType, AuthError, ParseError } from "./common/types.js";
8
- export type { Time, WeekRange, Teacher, } from "./common/types.js";
9
- export type { PersonalData, } from "./lk/types.js";
10
- export type { Faculty, Group, ScheduleEntry, FullScheduleSlot, FullScheduleDay, LessonTimeSlot, Lesson, LessonTime, SemesterWeek, TtClientOptions, CacheConfig, } from "./tt/types.js";
7
+ export { Period, EducationType, AuthError, ParseError, } from "./common/types.js";
8
+ export type { Time, WeekRange, Teacher } from "./common/types.js";
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";
package/dist/index.js CHANGED
@@ -2,4 +2,4 @@ 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 { getCurrentPeriod, isSessionPeriod, getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, getTimeSlots, getLessonNumber, getAdjacentSemester, isHoliday, RUSSIAN_HOLIDAYS, } from "./tt/utils.js";
5
- export { AuthError, ParseError } from "./common/types.js";
5
+ export { AuthError, ParseError, } from "./common/types.js";
package/dist/tt/parse.js CHANGED
@@ -109,12 +109,83 @@ function parseSemesterSchedule(doc) {
109
109
  }
110
110
  return days;
111
111
  }
112
+ function parseDate(dd, mm, yyyy) {
113
+ return new Date(parseInt(yyyy), parseInt(mm) - 1, parseInt(dd));
114
+ }
115
+ function parseTransferDiv(div) {
116
+ const divText = text(div);
117
+ const divHtml = div.innerHTML ?? "";
118
+ const m = divText.match(/(\d{2})\.(\d{2})\.(\d{4})\s*перенос\s*c\s*(\d{2})\.(\d{2})\.(\d{4})\s*\((\d+)\s*пара\)/);
119
+ if (!m)
120
+ return null;
121
+ const targetDate = parseDate(m[1], m[2], m[3]);
122
+ const fromDate = parseDate(m[4], m[5], m[6]);
123
+ const fromSlot = parseInt(m[7]);
124
+ const subjectEl = div.querySelector('span[style*="color: blue"]');
125
+ const subject = subjectEl ? text(subjectEl) : "";
126
+ if (!subject)
127
+ return null;
128
+ const roomMatch = divHtml.match(/([А-Яа-яA-Za-z]-\d+)/);
129
+ const typeMatch = divText.match(/\((лк|пр|лб|зач|экз|зчО|кр|конс)\)/);
130
+ // Teacher: last text line before closing </div>
131
+ const parts = divHtml.split(/<br\s*\/?>/);
132
+ const lastPart = parts[parts.length - 1]?.replace(/<[^>]*>/g, "").trim();
133
+ const transfer = { targetDate, fromDate, fromSlot, subject };
134
+ return {
135
+ transfer,
136
+ entry: {
137
+ room: roomMatch?.[1] ?? "",
138
+ subject,
139
+ type: typeMatch?.[1] ?? "",
140
+ weeks: { from: 0, to: 0 },
141
+ teacher: parseTeacher(lastPart ?? ""),
142
+ transfer,
143
+ },
144
+ };
145
+ }
146
+ function parseSubstitutionDiv(div) {
147
+ const divText = text(div);
148
+ const divHtml = div.innerHTML ?? "";
149
+ const m = divText.match(/(\d{2})\.(\d{2})\.(\d{4})\s*замена\s*на:/);
150
+ if (!m)
151
+ return null;
152
+ const date = parseDate(m[1], m[2], m[3]);
153
+ let room;
154
+ let teacher;
155
+ const roomMatch = divHtml.match(/Аудитория:\s*<span[^>]*>([^<]+)<\/span>/);
156
+ if (roomMatch)
157
+ room = roomMatch[1].trim();
158
+ const teacherMatch = divHtml.match(/Преподаватель:\s*<span[^>]*>([^<]+)<\/span>/);
159
+ if (teacherMatch)
160
+ teacher = parseTeacher(teacherMatch[1].trim());
161
+ return { date, room, teacher };
162
+ }
112
163
  function parseSemesterEntry(el) {
113
164
  const td = el.querySelector("td") ?? el;
114
165
  const fullHtml = td.innerHTML ?? "";
115
166
  const plainText = text(td);
116
167
  if (!plainText)
117
168
  return null;
169
+ const possibleChanges = (td.getAttribute("class") ?? "").includes("want") || undefined;
170
+ // Detect red-bordered divs (transfers / substitutions)
171
+ const redDivs = td.querySelectorAll('div[style*="border: 2px solid red"]');
172
+ // Check for transfer (перенос) — the whole entry is the transferred lesson
173
+ for (const div of redDivs) {
174
+ const result = parseTransferDiv(div);
175
+ if (result) {
176
+ if (possibleChanges)
177
+ result.entry.possibleChanges = true;
178
+ return result.entry;
179
+ }
180
+ }
181
+ // Collect substitutions (замена на)
182
+ const substitutions = [];
183
+ for (const div of redDivs) {
184
+ const sub = parseSubstitutionDiv(div);
185
+ if (sub)
186
+ substitutions.push(sub);
187
+ }
188
+ // Parse regular entry
118
189
  const subjectEl = td.querySelector('span[style*="color: blue"]');
119
190
  const subject = subjectEl ? text(subjectEl) : "";
120
191
  if (!subject)
@@ -133,6 +204,8 @@ function parseSemesterEntry(el) {
133
204
  teacher: parseTeacher(teacherMatch?.[1] ?? ""),
134
205
  subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
135
206
  weekParity,
207
+ substitutions: substitutions.length > 0 ? substitutions : undefined,
208
+ possibleChanges,
136
209
  };
137
210
  }
138
211
  // --- Session schedule parsing (date-based, specific dates) ---
@@ -1,4 +1,4 @@
1
- import { getCurrentPeriod, isSessionPeriod, getWeekdayName, getMonday, getSemesterStart, getSemesterWeeks, getWeekNumber, getAdjacentSemester, filterSlots, slotsToLessons, sortLessons, isHoliday, RUSSIAN_HOLIDAYS, } from "./utils.js";
1
+ import { getCurrentPeriod, isSessionPeriod, getWeekdayName, getMonday, getSemesterStart, getSemesterWeeks, getWeekNumber, getAdjacentSemester, filterSlots, slotsToLessons, sortLessons, isHoliday, collectTransfers, suppressTransferredLessons, RUSSIAN_HOLIDAYS, } from "./utils.js";
2
2
  export class Schedule {
3
3
  groupId;
4
4
  scheduleMap;
@@ -104,9 +104,15 @@ export class Schedule {
104
104
  const slots = this.getSlotsForWeekday(weekday, semesterDays, {
105
105
  subgroup: opts?.subgroup,
106
106
  week,
107
+ date,
107
108
  });
108
109
  lessons.push(...slotsToLessons(slots, date));
109
110
  }
111
+ // 3. Suppress lessons that were transferred away from this date
112
+ const transfers = collectTransfers(semesterDays);
113
+ if (transfers.length > 0) {
114
+ return suppressTransferredLessons(lessons, transfers, date).sort(sortLessons);
115
+ }
110
116
  }
111
117
  return lessons.sort(sortLessons);
112
118
  }
@@ -9,6 +9,26 @@ export interface Group {
9
9
  specialty?: string;
10
10
  profile?: string;
11
11
  }
12
+ /** A date-specific substitution (room and/or teacher change). */
13
+ export interface Substitution {
14
+ /** The date this substitution applies to. */
15
+ date: Date;
16
+ /** New room, if changed. */
17
+ room?: string;
18
+ /** New teacher, if changed. */
19
+ teacher?: Teacher;
20
+ }
21
+ /** Info about a lesson transferred from another date/slot. */
22
+ export interface TransferInfo {
23
+ /** Date when this lesson takes place (target). */
24
+ targetDate: Date;
25
+ /** Original date the lesson was moved from. */
26
+ fromDate: Date;
27
+ /** Original slot number (пара). */
28
+ fromSlot: number;
29
+ /** Subject name (used to match the source entry). */
30
+ subject: string;
31
+ }
12
32
  export interface ScheduleEntry {
13
33
  room: string;
14
34
  subject: string;
@@ -17,6 +37,12 @@ export interface ScheduleEntry {
17
37
  teacher: Teacher;
18
38
  subgroup?: number;
19
39
  weekParity?: "even" | "odd";
40
+ /** Date-specific substitutions (замена на). */
41
+ substitutions?: Substitution[];
42
+ /** If this entry is a transferred lesson (перенос). */
43
+ transfer?: TransferInfo;
44
+ /** Whether this entry is marked as potentially changing (class="want"). */
45
+ possibleChanges?: boolean;
20
46
  }
21
47
  export interface FullScheduleSlot {
22
48
  number: number;
@@ -50,6 +76,14 @@ export interface Lesson {
50
76
  weeks: WeekRange;
51
77
  subgroup?: number;
52
78
  weekParity?: "even" | "odd";
79
+ /** If a substitution was applied, the original room. */
80
+ originalRoom?: string;
81
+ /** If a substitution was applied, the original teacher. */
82
+ originalTeacher?: Teacher;
83
+ /** Transfer info if this lesson was moved from another date/slot. */
84
+ transfer?: TransferInfo;
85
+ /** Whether this lesson is marked as potentially changing. */
86
+ possibleChanges?: boolean;
53
87
  }
54
88
  export interface SemesterWeek {
55
89
  week: number;
@@ -1,4 +1,4 @@
1
- import type { FullScheduleSlot, SemesterWeek, Lesson, LessonTimeSlot } from "./types.js";
1
+ import type { FullScheduleDay, FullScheduleSlot, ScheduleEntry, SemesterWeek, Lesson, LessonTimeSlot } from "./types.js";
2
2
  import { Period, EducationType } from "../common/types.js";
3
3
  import type { Time } from "../common/types.js";
4
4
  export declare function getCurrentPeriod(opts?: {
@@ -36,8 +36,13 @@ export declare function getWeekNumber(opts: {
36
36
  export declare function filterSlots(slots: FullScheduleSlot[], opts?: {
37
37
  subgroup?: number;
38
38
  week?: number;
39
+ date?: Date;
39
40
  }): FullScheduleSlot[];
40
41
  export declare function slotsToLessons(slots: FullScheduleSlot[], date: Date): Lesson[];
42
+ /** Collect all transfer entries from schedule days. */
43
+ export declare function collectTransfers(days: FullScheduleDay[]): ScheduleEntry[];
44
+ /** Remove lessons whose source date/slot match a transfer. */
45
+ export declare function suppressTransferredLessons(lessons: Lesson[], transfers: ScheduleEntry[], date: Date): Lesson[];
41
46
  export declare function getTimeSlots(educationType: EducationType): LessonTimeSlot[];
42
47
  export declare function getLessonNumber(time: Time, educationType: EducationType): number;
43
48
  export declare function getAdjacentSemester(session: Period): Period;
package/dist/tt/utils.js CHANGED
@@ -88,8 +88,19 @@ export function getWeekNumber(opts) {
88
88
  const diff = targetMonday.getTime() - startMonday.getTime();
89
89
  return Math.floor(diff / (7 * 24 * 60 * 60 * 1000));
90
90
  }
91
+ function isSameDay(a, b) {
92
+ return (a.getFullYear() === b.getFullYear() &&
93
+ a.getMonth() === b.getMonth() &&
94
+ a.getDate() === b.getDate());
95
+ }
91
96
  function filterEntries(entries, opts) {
92
97
  return entries.filter((e) => {
98
+ // Transfer entries: only include when the query date matches the target date
99
+ if (e.transfer) {
100
+ if (!opts?.date)
101
+ return false;
102
+ return isSameDay(e.transfer.targetDate, opts.date);
103
+ }
93
104
  if (opts?.subgroup && e.subgroup && e.subgroup !== opts.subgroup) {
94
105
  return false;
95
106
  }
@@ -110,7 +121,7 @@ function filterEntries(entries, opts) {
110
121
  });
111
122
  }
112
123
  export function filterSlots(slots, opts) {
113
- if (opts?.subgroup == null && opts?.week == null)
124
+ if (opts?.subgroup == null && opts?.week == null && opts?.date == null)
114
125
  return slots;
115
126
  return slots
116
127
  .map((slot) => ({
@@ -128,22 +139,66 @@ export function slotsToLessons(slots, date) {
128
139
  const lessons = [];
129
140
  for (const slot of slots) {
130
141
  for (const entry of slot.entries) {
142
+ let room = entry.room;
143
+ let teacher = entry.teacher;
144
+ let originalRoom;
145
+ let originalTeacher;
146
+ // Apply date-specific substitutions
147
+ if (entry.substitutions) {
148
+ const sub = entry.substitutions.find((s) => isSameDay(s.date, date));
149
+ if (sub) {
150
+ if (sub.room) {
151
+ originalRoom = room;
152
+ room = sub.room;
153
+ }
154
+ if (sub.teacher) {
155
+ originalTeacher = teacher;
156
+ teacher = sub.teacher;
157
+ }
158
+ }
159
+ }
131
160
  lessons.push({
132
161
  number: slot.number,
133
162
  start: makeLessonTime(date, slot.timeStart),
134
163
  end: makeLessonTime(date, slot.timeEnd),
135
164
  subject: entry.subject,
136
165
  type: entry.type,
137
- room: entry.room,
138
- teacher: entry.teacher,
166
+ room,
167
+ teacher,
139
168
  weeks: entry.weeks,
140
169
  subgroup: entry.subgroup,
141
170
  weekParity: entry.weekParity,
171
+ originalRoom,
172
+ originalTeacher,
173
+ transfer: entry.transfer,
174
+ possibleChanges: entry.possibleChanges,
142
175
  });
143
176
  }
144
177
  }
145
178
  return lessons;
146
179
  }
180
+ /** Collect all transfer entries from schedule days. */
181
+ export function collectTransfers(days) {
182
+ const transfers = [];
183
+ for (const day of days) {
184
+ for (const slot of day.slots) {
185
+ for (const entry of slot.entries) {
186
+ if (entry.transfer)
187
+ transfers.push(entry);
188
+ }
189
+ }
190
+ }
191
+ return transfers;
192
+ }
193
+ /** Remove lessons whose source date/slot match a transfer. */
194
+ export function suppressTransferredLessons(lessons, transfers, date) {
195
+ return lessons.filter((lesson) => {
196
+ return !transfers.some((t) => t.transfer &&
197
+ isSameDay(t.transfer.fromDate, date) &&
198
+ t.transfer.fromSlot === lesson.number &&
199
+ t.transfer.subject === lesson.subject);
200
+ });
201
+ }
147
202
  // --- Lesson time slots ---
148
203
  const VO_TIME_SLOTS = [
149
204
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chuvsu-js",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Node.js library for ChuvSU student portal (lk.chuvsu.ru) and schedule (tt.chuvsu.ru)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",