chuvsu-js 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,8 +5,6 @@ Node.js библиотека для работы с порталами ЧувГ
5
5
  - **tt.chuvsu.ru** — расписание занятий (факультеты, группы, преподаватели)
6
6
  - **lk.chuvsu.ru** — личный кабинет студента (персональные данные)
7
7
 
8
- Пока что очень сырая, много что можно оптимизировать.
9
-
10
8
  ## Установка
11
9
 
12
10
  ```bash
@@ -27,19 +25,30 @@ await tt.loginAsGuest();
27
25
 
28
26
  // Найти группу по названию
29
27
  const groups = await tt.searchGroup({ name: "КТ-41-24" });
30
- console.log(groups); // [{ id: 123, name: "КТ-41-24", specialty: "...", profile: "..." }]
28
+ console.log(groups); // [{ id: 8919, name: "КТ-41-24", specialty: "...", profile: "..." }]
31
29
 
32
- // Получить расписание на сегодня
33
- const lessons = await tt.getScheduleForDate({
34
- groupId: groups[0].id,
35
- date: new Date(),
36
- });
30
+ // Получить расписание группы
31
+ const schedule = await tt.getSchedule({ groupId: groups[0].id });
37
32
 
38
- for (const lesson of lessons) {
33
+ // Расписание на сегодня
34
+ const today = schedule.today();
35
+ for (const lesson of today) {
39
36
  console.log(
40
37
  `${lesson.start.hours}:${lesson.start.minutes} — ${lesson.subject} (${lesson.type})`,
41
38
  );
42
39
  }
40
+
41
+ // С фильтром по подгруппе
42
+ schedule.today({ subgroup: 1 });
43
+
44
+ // На завтра
45
+ schedule.tomorrow();
46
+
47
+ // На текущую неделю
48
+ schedule.thisWeek();
49
+
50
+ // Текущая пара
51
+ schedule.currentLesson();
43
52
  ```
44
53
 
45
54
  ### Личный кабинет (LkClient)
@@ -84,30 +93,13 @@ await tt.login({ email: "...", password: "..." });
84
93
  await tt.loginAsGuest();
85
94
  ```
86
95
 
87
- #### Расписание
96
+ #### Получение расписания
88
97
 
89
98
  ```ts
90
- // Полное расписание группы (все дни, все слоты)
91
- const schedule = await tt.getGroupSchedule({ groupId, period? });
92
-
93
- // Расписание на конкретную дату
94
- const lessons = await tt.getScheduleForDate({ groupId, date, filter?, period? });
95
-
96
- // Расписание на день недели (0 = воскресенье, 1 = понедельник, ...)
97
- const lessons = await tt.getScheduleForDay({ groupId, weekday, filter?, period? });
98
-
99
- // Расписание на неделю
100
- const week = await tt.getScheduleForWeek({ groupId, week?, filter?, period? });
101
-
102
- // Текущая пара
103
- const lesson = await tt.getCurrentLesson({ groupId, filter? });
99
+ const schedule = await tt.getSchedule({ groupId, period? });
104
100
  ```
105
101
 
106
- **ScheduleFilter** фильтрация по подгруппе и/или неделе:
107
-
108
- ```ts
109
- { subgroup?: number; week?: number }
110
- ```
102
+ Возвращает объект `Schedule`, который позволяет получать расписание локально, без дополнительных запросов к серверу.
111
103
 
112
104
  #### Поиск
113
105
 
@@ -125,6 +117,13 @@ const groups = await tt.searchGroup({ name: "ЗИ" });
125
117
  const teachers = await tt.searchTeacher({ name: "Иванов" });
126
118
  ```
127
119
 
120
+ #### Период
121
+
122
+ ```ts
123
+ // Текущий учебный период
124
+ const period = tt.getCurrentPeriod();
125
+ ```
126
+
128
127
  #### Кеш
129
128
 
130
129
  ```ts
@@ -137,10 +136,71 @@ const data = tt.exportCache();
137
136
  tt.importCache(data);
138
137
  ```
139
138
 
140
- Категории кеша: `schedule`, `faculties`, `groups`, `currentPeriod`.
139
+ Категории кеша: `schedule`, `faculties`, `groups`.
140
+
141
+ ### Schedule
142
+
143
+ Объект расписания группы. Все методы синхронные — данные уже загружены.
144
+
145
+ #### Свойства
146
+
147
+ ```ts
148
+ schedule.groupId; // ID группы
149
+ schedule.period; // Учебный период
150
+ schedule.days; // Сырые данные (FullScheduleDay[])
151
+ ```
152
+
153
+ #### Расписание по дате
154
+
155
+ ```ts
156
+ // На сегодня
157
+ schedule.today({ subgroup?: number });
158
+
159
+ // На завтра
160
+ schedule.tomorrow({ subgroup?: number });
161
+
162
+ // На конкретную дату
163
+ schedule.forDate(date: Date, { subgroup?: number });
164
+ ```
165
+
166
+ #### Расписание по неделе
167
+
168
+ ```ts
169
+ // На текущую неделю
170
+ schedule.thisWeek({ subgroup?: number });
171
+
172
+ // На конкретную неделю
173
+ schedule.forWeek(week?: number, { subgroup?: number });
174
+ ```
175
+
176
+ #### Расписание по дню недели
177
+
178
+ ```ts
179
+ // По дню недели (0 = воскресенье, 1 = понедельник, ...)
180
+ schedule.forDay(weekday: number, { subgroup?: number, week?: number });
181
+ ```
182
+
183
+ #### Текущая пара
184
+
185
+ ```ts
186
+ const lesson = schedule.currentLesson({ subgroup?: number });
187
+ ```
141
188
 
142
189
  #### Утилиты семестра
143
190
 
191
+ ```ts
192
+ // Номер текущей недели
193
+ schedule.getWeekNumber(date?: Date);
194
+
195
+ // Все недели семестра
196
+ schedule.getSemesterWeeks(weekCount?: number);
197
+
198
+ // Начало семестра
199
+ schedule.getSemesterStart();
200
+ ```
201
+
202
+ Утилиты также доступны как standalone функции:
203
+
144
204
  ```ts
145
205
  import {
146
206
  getSemesterStart,
@@ -149,13 +209,8 @@ import {
149
209
  Period,
150
210
  } from "chuvsu-js";
151
211
 
152
- // Начало семестра
153
212
  getSemesterStart({ period: Period.FallSemester, year: 2025 });
154
-
155
- // Все недели семестра
156
213
  getSemesterWeeks({ period: Period.SpringSemester });
157
-
158
- // Номер текущей недели
159
214
  getWeekNumber({ period: Period.SpringSemester });
160
215
  ```
161
216
 
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  export { LkClient } from "./lk/client.js";
2
2
  export { TtClient } from "./tt/client.js";
3
+ export { Schedule } from "./tt/schedule.js";
3
4
  export type { CacheEntry } from "./common/cache.js";
4
- export { getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, filterSlots, slotsToLessons, } from "./tt/schedule.js";
5
+ export { getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, } from "./tt/utils.js";
5
6
  export { Period, EducationType, AuthError, ParseError } from "./common/types.js";
6
7
  export type { Time, WeekRange, Teacher, } from "./common/types.js";
7
8
  export type { PersonalData, } from "./lk/types.js";
8
- export type { Faculty, Group, ScheduleEntry, FullScheduleSlot, FullScheduleDay, LessonTimeSlot, Lesson, LessonTime, ScheduleWeekDay, ScheduleFilter, SemesterWeek, TtClientOptions, CacheConfig, } from "./tt/types.js";
9
+ export type { Faculty, Group, ScheduleEntry, FullScheduleSlot, FullScheduleDay, LessonTimeSlot, Lesson, LessonTime, SemesterWeek, TtClientOptions, CacheConfig, } from "./tt/types.js";
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { LkClient } from "./lk/client.js";
2
2
  export { TtClient } from "./tt/client.js";
3
- export { getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, filterSlots, slotsToLessons, } from "./tt/schedule.js";
3
+ export { Schedule } from "./tt/schedule.js";
4
+ export { getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, } from "./tt/utils.js";
4
5
  export { AuthError, ParseError } from "./common/types.js";
@@ -1,10 +1,13 @@
1
1
  import type { PersonalData } from "./types.js";
2
2
  export declare class LkClient {
3
3
  private http;
4
+ private credentials;
4
5
  login(opts: {
5
6
  email: string;
6
7
  password: string;
7
8
  }): Promise<void>;
9
+ private isSessionExpired;
10
+ private authGet;
8
11
  getPersonalData(): Promise<PersonalData>;
9
12
  getGroupId(): Promise<number | null>;
10
13
  }
package/dist/lk/client.js CHANGED
@@ -6,14 +6,27 @@ const LOGIN_URL = `${BASE}/info/login.php`;
6
6
  const STUDENT_BASE = `${BASE}/student`;
7
7
  export class LkClient {
8
8
  http = new HttpClient();
9
+ credentials = null;
9
10
  async login(opts) {
10
11
  const res = await this.http.post(LOGIN_URL, { email: opts.email, password: opts.password, role: "1", enter: "" }, false);
11
12
  if (!(res.status === 302 && res.location?.includes("student"))) {
12
13
  throw new AuthError("LK login failed");
13
14
  }
15
+ this.credentials = opts;
16
+ }
17
+ isSessionExpired(body) {
18
+ return body.includes("login.php");
19
+ }
20
+ async authGet(url) {
21
+ const res = await this.http.get(url);
22
+ if (this.credentials && this.isSessionExpired(res.body)) {
23
+ await this.login(this.credentials);
24
+ return this.http.get(url);
25
+ }
26
+ return res;
14
27
  }
15
28
  async getPersonalData() {
16
- const { body } = await this.http.get(`${STUDENT_BASE}/personal_data.php`);
29
+ const { body } = await this.authGet(`${STUDENT_BASE}/personal_data.php`);
17
30
  const vals = extractScriptValues(body, "form_personal_data");
18
31
  return {
19
32
  lastName: vals.fam ?? "",
@@ -32,7 +45,7 @@ export class LkClient {
32
45
  };
33
46
  }
34
47
  async getGroupId() {
35
- const { body } = await this.http.get(`${STUDENT_BASE}/tt.php`);
48
+ const { body } = await this.authGet(`${STUDENT_BASE}/tt.php`);
36
49
  const match = body.match(/tt\.chuvsu\.ru\/index\/grouptt\/gr\/(\d+)/);
37
50
  return match ? parseInt(match[1]) : null;
38
51
  }
@@ -1,10 +1,12 @@
1
1
  import type { CacheEntry } from "../common/cache.js";
2
2
  import { Period } from "../common/types.js";
3
- import type { Faculty, Group, FullScheduleDay, ScheduleFilter, Lesson, ScheduleWeekDay, TtClientOptions, CacheConfig } from "./types.js";
3
+ import { Schedule } from "./schedule.js";
4
+ import type { Faculty, Group, TtClientOptions, CacheConfig } from "./types.js";
4
5
  export declare class TtClient {
5
6
  private http;
6
7
  private educationType;
7
8
  private cache;
9
+ private loginMode;
8
10
  constructor(opts?: TtClientOptions);
9
11
  private get pertt();
10
12
  clearCache(category?: keyof CacheConfig): void;
@@ -15,34 +17,14 @@ export declare class TtClient {
15
17
  password: string;
16
18
  }): Promise<void>;
17
19
  loginAsGuest(): Promise<void>;
18
- getGroupSchedule(opts: {
20
+ private isSessionExpired;
21
+ private relogin;
22
+ private authGet;
23
+ private authPost;
24
+ getSchedule(opts: {
19
25
  groupId: number;
20
26
  period?: Period;
21
- }): Promise<FullScheduleDay[]>;
22
- private getFilteredSlots;
23
- private getDateForWeekday;
24
- getScheduleForDay(opts: {
25
- groupId: number;
26
- weekday: number;
27
- filter?: ScheduleFilter;
28
- period?: Period;
29
- }): Promise<Lesson[]>;
30
- getScheduleForDate(opts: {
31
- groupId: number;
32
- date: Date;
33
- subgroup?: number;
34
- period?: Period;
35
- }): Promise<Lesson[]>;
36
- getScheduleForWeek(opts: {
37
- groupId: number;
38
- week?: number;
39
- filter?: ScheduleFilter;
40
- period?: Period;
41
- }): Promise<ScheduleWeekDay[]>;
42
- getCurrentLesson(opts: {
43
- groupId: number;
44
- subgroup?: number;
45
- }): Promise<Lesson | null>;
27
+ }): Promise<Schedule>;
46
28
  getCurrentPeriod(opts?: {
47
29
  date?: Date;
48
30
  }): Period;
package/dist/tt/client.js CHANGED
@@ -2,13 +2,14 @@ import { HttpClient } from "../common/http.js";
2
2
  import { Cache } from "../common/cache.js";
3
3
  import { AuthError } from "../common/types.js";
4
4
  import { parseGroupButtons, parseFacultyButtons, parseTeacherButtons, parseFullSchedule, } from "./parse.js";
5
- import { filterSlots, getMonday, getWeekdayName, getWeekNumber, getSemesterStart, getSemesterWeeks, slotsToLessons, } from "./schedule.js";
5
+ import { Schedule } from "./schedule.js";
6
6
  const BASE = "https://tt.chuvsu.ru";
7
7
  const AUTH_URL = `${BASE}/auth`;
8
8
  export class TtClient {
9
9
  http = new HttpClient();
10
10
  educationType;
11
11
  cache;
12
+ loginMode = null;
12
13
  constructor(opts) {
13
14
  this.educationType = opts?.educationType ?? 1 /* EducationType.HigherEducation */;
14
15
  if (opts?.cache == null) {
@@ -51,125 +52,66 @@ export class TtClient {
51
52
  if (res.status !== 302) {
52
53
  throw new AuthError("TT login failed");
53
54
  }
55
+ this.loginMode = { type: "credentials", ...opts };
54
56
  }
55
57
  async loginAsGuest() {
56
58
  const res = await this.http.post(AUTH_URL, { guest: "Войти гостем", hfac: "0", pertt: this.pertt }, false);
57
59
  if (res.status !== 302) {
58
60
  throw new AuthError("TT guest login failed");
59
61
  }
62
+ this.loginMode = { type: "guest" };
60
63
  }
61
- // --- Schedule ---
62
- async getGroupSchedule(opts) {
63
- const cacheKey = `${opts.groupId}:${opts.period ?? 0}`;
64
- const cached = this.cache?.get("schedule", cacheKey);
65
- if (cached)
66
- return cached;
67
- const url = `${BASE}/index/grouptt/gr/${opts.groupId}`;
68
- let body;
69
- if (opts.period !== undefined) {
70
- ({ body } = await this.http.post(url, { htype: String(opts.period) }));
64
+ isSessionExpired(body) {
65
+ return body.includes('name="wname"');
66
+ }
67
+ async relogin() {
68
+ if (!this.loginMode)
69
+ return;
70
+ if (this.loginMode.type === "credentials") {
71
+ await this.login(this.loginMode);
71
72
  }
72
73
  else {
73
- ({ body } = await this.http.get(url));
74
+ await this.loginAsGuest();
74
75
  }
75
- const data = parseFullSchedule(body);
76
- this.cache?.set("schedule", cacheKey, data);
77
- return data;
78
76
  }
79
- async getFilteredSlots(opts) {
80
- const schedule = await this.getGroupSchedule({
81
- groupId: opts.groupId,
82
- period: opts.period,
83
- });
84
- const dayName = getWeekdayName(opts.weekday);
85
- const day = schedule.find((d) => d.weekday.toLowerCase() === dayName.toLowerCase());
86
- if (!day)
87
- return [];
88
- return filterSlots(day.slots, opts.filter);
89
- }
90
- getDateForWeekday(weekday, period, week) {
91
- if (week != null) {
92
- const semesterStart = getSemesterStart({ period });
93
- const startMonday = getMonday(semesterStart);
94
- const date = new Date(startMonday);
95
- date.setDate(startMonday.getDate() +
96
- (week - 1) * 7 +
97
- (weekday === 0 ? 6 : weekday - 1));
98
- return date;
77
+ async authGet(url) {
78
+ const res = await this.http.get(url);
79
+ if (this.loginMode && this.isSessionExpired(res.body)) {
80
+ await this.relogin();
81
+ return this.http.get(url);
99
82
  }
100
- const now = new Date();
101
- const currentDay = now.getDay();
102
- const diff = weekday - currentDay;
103
- const date = new Date(now);
104
- date.setDate(now.getDate() + diff);
105
- date.setHours(0, 0, 0, 0);
106
- return date;
107
- }
108
- async getScheduleForDay(opts) {
109
- const period = opts.period ?? this.getCurrentPeriod();
110
- const slots = await this.getFilteredSlots({
111
- groupId: opts.groupId,
112
- weekday: opts.weekday,
113
- filter: opts.filter,
114
- period,
115
- });
116
- const date = this.getDateForWeekday(opts.weekday, period, opts.filter?.week);
117
- return slotsToLessons(slots, date);
118
- }
119
- async getScheduleForDate(opts) {
120
- const weekday = opts.date.getDay();
121
- const period = opts.period ?? this.getCurrentPeriod({ date: opts.date });
122
- const effectiveFilter = { subgroup: opts.subgroup };
123
- if (effectiveFilter.week == null) {
124
- effectiveFilter.week = getWeekNumber({ period, date: opts.date });
83
+ return res;
84
+ }
85
+ async authPost(url, data) {
86
+ const res = await this.http.post(url, data);
87
+ if (this.loginMode && this.isSessionExpired(res.body)) {
88
+ await this.relogin();
89
+ return this.http.post(url, data);
125
90
  }
126
- const slots = await this.getFilteredSlots({
127
- groupId: opts.groupId,
128
- weekday,
129
- filter: effectiveFilter,
130
- period,
131
- });
132
- return slotsToLessons(slots, opts.date);
91
+ return res;
133
92
  }
134
- async getScheduleForWeek(opts) {
93
+ // --- Schedule ---
94
+ async getSchedule(opts) {
135
95
  const period = opts.period ?? this.getCurrentPeriod();
136
- const week = opts.week ?? getWeekNumber({ period });
137
- const effectiveFilter = { ...opts.filter, week };
138
- const semesterWeeks = getSemesterWeeks({ period, weekCount: week });
139
- const weekData = semesterWeeks.find((w) => w.week === week);
140
- const mondayDate = weekData ? weekData.start : new Date();
141
- const result = [];
142
- for (let i = 0; i < 7; i++) {
143
- const date = new Date(mondayDate);
144
- date.setDate(mondayDate.getDate() + i);
145
- date.setHours(0, 0, 0, 0);
146
- const weekday = i === 6 ? 0 : i + 1;
147
- const slots = await this.getFilteredSlots({
148
- groupId: opts.groupId,
149
- weekday,
150
- filter: effectiveFilter,
151
- period,
152
- });
153
- result.push({ date, lessons: slotsToLessons(slots, date) });
96
+ const cacheKey = `${opts.groupId}:${period}`;
97
+ const cached = this.cache?.get("schedule", cacheKey);
98
+ let days;
99
+ if (cached) {
100
+ days = cached;
154
101
  }
155
- return result;
156
- }
157
- async getCurrentLesson(opts) {
158
- const now = new Date();
159
- const lessons = await this.getScheduleForDate({
160
- groupId: opts.groupId,
161
- date: now,
162
- subgroup: opts.subgroup,
163
- });
164
- const timeMinutes = now.getHours() * 60 + now.getMinutes();
165
- for (const lesson of lessons) {
166
- const start = lesson.start.hours * 60 + lesson.start.minutes;
167
- const end = lesson.end.hours * 60 + lesson.end.minutes;
168
- if (timeMinutes >= start && timeMinutes <= end) {
169
- return lesson;
102
+ else {
103
+ const url = `${BASE}/index/grouptt/gr/${opts.groupId}`;
104
+ let body;
105
+ if (opts.period !== undefined) {
106
+ ({ body } = await this.authPost(url, { htype: String(opts.period) }));
107
+ }
108
+ else {
109
+ ({ body } = await this.authGet(url));
170
110
  }
111
+ days = parseFullSchedule(body);
112
+ this.cache?.set("schedule", cacheKey, days);
171
113
  }
172
- return null;
114
+ return new Schedule(opts.groupId, period, days);
173
115
  }
174
116
  // --- Period ---
175
117
  getCurrentPeriod(opts) {
@@ -193,7 +135,7 @@ export class TtClient {
193
135
  const cached = this.cache?.get("faculties", "all");
194
136
  if (cached)
195
137
  return cached;
196
- const { body } = await this.http.get(`${BASE}/`);
138
+ const { body } = await this.authGet(`${BASE}/`);
197
139
  const data = parseFacultyButtons(body);
198
140
  this.cache?.set("faculties", "all", data);
199
141
  return data;
@@ -203,7 +145,7 @@ export class TtClient {
203
145
  const cached = this.cache?.get("groups", cacheKey);
204
146
  if (cached)
205
147
  return cached;
206
- const { body } = await this.http.post(`${BASE}/`, {
148
+ const { body } = await this.authPost(`${BASE}/`, {
207
149
  hfac: String(opts.facultyId),
208
150
  pertt: this.pertt,
209
151
  });
@@ -212,7 +154,7 @@ export class TtClient {
212
154
  return data;
213
155
  }
214
156
  async searchGroup(opts) {
215
- const { body } = await this.http.post(`${BASE}/`, {
157
+ const { body } = await this.authPost(`${BASE}/`, {
216
158
  grname: opts.name,
217
159
  findgr: "найти",
218
160
  hfac: "0",
@@ -221,7 +163,7 @@ export class TtClient {
221
163
  return parseGroupButtons(body);
222
164
  }
223
165
  async searchTeacher(opts) {
224
- const { body } = await this.http.post(`${BASE}/`, {
166
+ const { body } = await this.authPost(`${BASE}/`, {
225
167
  techname: opts.name,
226
168
  findtech: "найти",
227
169
  hfac: "0",
@@ -1,31 +1,35 @@
1
- import type { FullScheduleSlot, ScheduleFilter, SemesterWeek, Lesson } from "./types.js";
1
+ import type { FullScheduleDay, SemesterWeek, Lesson } from "./types.js";
2
2
  import { Period } from "../common/types.js";
3
- export declare function getWeekdayName(weekday: number): string;
4
- export declare function filterSlots(slots: FullScheduleSlot[], filter?: ScheduleFilter): FullScheduleSlot[];
5
- export declare function getMonday(date: Date): Date;
6
- /**
7
- * Get the start date of a semester.
8
- * Fall: September 1 of the given year.
9
- * Spring: first Monday of February of the given year.
10
- */
11
- export declare function getSemesterStart(opts: {
12
- period: Period;
13
- year?: number;
14
- }): Date;
15
- /**
16
- * Get all weeks in a semester with their start/end dates.
17
- * Week 0 starts from the semester start date.
18
- */
19
- export declare function getSemesterWeeks(opts: {
20
- period: Period;
21
- year?: number;
22
- weekCount?: number;
23
- }): SemesterWeek[];
24
- /**
25
- * Get the current week number within a semester.
26
- */
27
- export declare function getWeekNumber(opts: {
28
- period: Period;
29
- date?: Date;
30
- }): number;
31
- export declare function slotsToLessons(slots: FullScheduleSlot[], date: Date): Lesson[];
3
+ export declare class Schedule {
4
+ readonly groupId: number;
5
+ readonly period: Period;
6
+ readonly days: FullScheduleDay[];
7
+ constructor(groupId: number, period: Period, days: FullScheduleDay[]);
8
+ private getSlotsForWeekday;
9
+ private getDateForWeekday;
10
+ forDay(weekday: number, opts?: {
11
+ subgroup?: number;
12
+ week?: number;
13
+ }): Lesson[];
14
+ forDate(date: Date, opts?: {
15
+ subgroup?: number;
16
+ }): Lesson[];
17
+ forWeek(week?: number, opts?: {
18
+ subgroup?: number;
19
+ }): Lesson[];
20
+ today(opts?: {
21
+ subgroup?: number;
22
+ }): Lesson[];
23
+ tomorrow(opts?: {
24
+ subgroup?: number;
25
+ }): Lesson[];
26
+ thisWeek(opts?: {
27
+ subgroup?: number;
28
+ }): Lesson[];
29
+ currentLesson(opts?: {
30
+ subgroup?: number;
31
+ }): Lesson | null;
32
+ getWeekNumber(date?: Date): number;
33
+ getSemesterWeeks(weekCount?: number): SemesterWeek[];
34
+ getSemesterStart(): Date;
35
+ }
@@ -1,124 +1,104 @@
1
- const WEEKDAY_NAMES = [
2
- "Воскресенье",
3
- "Понедельник",
4
- "Вторник",
5
- "Среда",
6
- "Четверг",
7
- "Пятница",
8
- "Суббота",
9
- ];
10
- export function getWeekdayName(weekday) {
11
- return WEEKDAY_NAMES[weekday] ?? "";
12
- }
13
- export function filterSlots(slots, filter) {
14
- if (filter?.subgroup == null && filter?.week == null)
15
- return slots;
16
- return slots
17
- .map((slot) => ({
18
- ...slot,
19
- entries: filterEntries(slot.entries, filter),
20
- }))
21
- .filter((slot) => slot.entries.length > 0);
22
- }
23
- function filterEntries(entries, filter) {
24
- return entries.filter((e) => {
25
- if (filter?.subgroup && e.subgroup && e.subgroup !== filter.subgroup) {
26
- return false;
27
- }
28
- if (filter?.week != null) {
29
- if (e.weeks.from > 0 &&
30
- (filter.week < e.weeks.from || filter.week > e.weeks.to)) {
31
- return false;
32
- }
33
- if (e.weekParity) {
34
- const isEven = filter.week % 2 === 0;
35
- if (e.weekParity === "even" && !isEven)
36
- return false;
37
- if (e.weekParity === "odd" && isEven)
38
- return false;
39
- }
1
+ import { getWeekdayName, getMonday, getSemesterStart, getSemesterWeeks, getWeekNumber, filterSlots, slotsToLessons, } from "./utils.js";
2
+ export class Schedule {
3
+ groupId;
4
+ period;
5
+ days;
6
+ constructor(groupId, period, days) {
7
+ this.groupId = groupId;
8
+ this.period = period;
9
+ this.days = days;
10
+ }
11
+ getSlotsForWeekday(weekday, opts) {
12
+ const dayName = getWeekdayName(weekday);
13
+ const day = this.days.find((d) => d.weekday.toLowerCase() === dayName.toLowerCase());
14
+ if (!day)
15
+ return [];
16
+ return filterSlots(day.slots, opts);
17
+ }
18
+ getDateForWeekday(weekday, week) {
19
+ if (week != null) {
20
+ const startMonday = getMonday(getSemesterStart({ period: this.period }));
21
+ const date = new Date(startMonday);
22
+ date.setDate(startMonday.getDate() +
23
+ (week - 1) * 7 +
24
+ (weekday === 0 ? 6 : weekday - 1));
25
+ return date;
40
26
  }
41
- return true;
42
- });
43
- }
44
- export function getMonday(date) {
45
- const d = new Date(date);
46
- const day = d.getDay();
47
- const diff = day === 0 ? -6 : 1 - day;
48
- d.setDate(d.getDate() + diff);
49
- d.setHours(0, 0, 0, 0);
50
- return d;
51
- }
52
- /**
53
- * Get the start date of a semester.
54
- * Fall: September 1 of the given year.
55
- * Spring: first Monday of February of the given year.
56
- */
57
- export function getSemesterStart(opts) {
58
- const year = opts.year ?? new Date().getFullYear();
59
- if (opts.period === 1 /* Period.FallSemester */) {
60
- return new Date(year, 8, 1); // September 1
27
+ const now = new Date();
28
+ const currentDay = now.getDay();
29
+ const diff = weekday - currentDay;
30
+ const date = new Date(now);
31
+ date.setDate(now.getDate() + diff);
32
+ date.setHours(0, 0, 0, 0);
33
+ return date;
61
34
  }
62
- // Spring: first Monday of February
63
- const feb1 = new Date(year, 1, 1);
64
- const day = feb1.getDay();
65
- const daysToAdd = day === 1 ? 0 : day === 0 ? 1 : 8 - day;
66
- const firstMonday = new Date(year, 1, 1 + daysToAdd);
67
- firstMonday.setHours(0, 0, 0, 0);
68
- return firstMonday;
69
- }
70
- /**
71
- * Get all weeks in a semester with their start/end dates.
72
- * Week 0 starts from the semester start date.
73
- */
74
- export function getSemesterWeeks(opts) {
75
- const weekCount = opts.weekCount ?? 17;
76
- const semesterStart = getSemesterStart(opts);
77
- const startMonday = getMonday(semesterStart);
78
- const weeks = [];
79
- for (let i = 0; i <= weekCount; i++) {
80
- const start = new Date(startMonday);
81
- start.setDate(startMonday.getDate() + i * 7);
82
- const end = new Date(start);
83
- end.setDate(start.getDate() + 6);
84
- end.setHours(23, 59, 59, 999);
85
- weeks.push({ week: i, start, end });
35
+ forDay(weekday, opts) {
36
+ const slots = this.getSlotsForWeekday(weekday, opts);
37
+ const date = this.getDateForWeekday(weekday, opts?.week);
38
+ return slotsToLessons(slots, date);
86
39
  }
87
- return weeks;
88
- }
89
- /**
90
- * Get the current week number within a semester.
91
- */
92
- export function getWeekNumber(opts) {
93
- const date = opts.date ?? new Date();
94
- const semesterStart = getSemesterStart(opts);
95
- const startMonday = getMonday(semesterStart);
96
- const targetMonday = getMonday(date);
97
- const diff = targetMonday.getTime() - startMonday.getTime();
98
- return Math.floor(diff / (7 * 24 * 60 * 60 * 1000));
99
- }
100
- function makeLessonTime(date, time) {
101
- const d = new Date(date);
102
- d.setHours(time.hours, time.minutes, 0, 0);
103
- return { date: d, hours: time.hours, minutes: time.minutes };
104
- }
105
- export function slotsToLessons(slots, date) {
106
- const lessons = [];
107
- for (const slot of slots) {
108
- for (const entry of slot.entries) {
109
- lessons.push({
110
- number: slot.number,
111
- start: makeLessonTime(date, slot.timeStart),
112
- end: makeLessonTime(date, slot.timeEnd),
113
- subject: entry.subject,
114
- type: entry.type,
115
- room: entry.room,
116
- teacher: entry.teacher,
117
- weeks: entry.weeks,
118
- subgroup: entry.subgroup,
119
- weekParity: entry.weekParity,
40
+ forDate(date, opts) {
41
+ const weekday = date.getDay();
42
+ const week = getWeekNumber({ period: this.period, date });
43
+ const slots = this.getSlotsForWeekday(weekday, {
44
+ subgroup: opts?.subgroup,
45
+ week,
46
+ });
47
+ return slotsToLessons(slots, date);
48
+ }
49
+ forWeek(week, opts) {
50
+ const effectiveWeek = week ?? getWeekNumber({ period: this.period });
51
+ const semesterWeeks = getSemesterWeeks({
52
+ period: this.period,
53
+ weekCount: effectiveWeek,
54
+ });
55
+ const weekData = semesterWeeks.find((w) => w.week === effectiveWeek);
56
+ const mondayDate = weekData ? weekData.start : new Date();
57
+ const lessons = [];
58
+ for (let i = 0; i < 7; i++) {
59
+ const date = new Date(mondayDate);
60
+ date.setDate(mondayDate.getDate() + i);
61
+ date.setHours(0, 0, 0, 0);
62
+ const weekday = i === 6 ? 0 : i + 1;
63
+ const slots = this.getSlotsForWeekday(weekday, {
64
+ subgroup: opts?.subgroup,
65
+ week: effectiveWeek,
120
66
  });
67
+ lessons.push(...slotsToLessons(slots, date));
121
68
  }
69
+ return lessons;
70
+ }
71
+ today(opts) {
72
+ return this.forDate(new Date(), opts);
73
+ }
74
+ tomorrow(opts) {
75
+ const date = new Date();
76
+ date.setDate(date.getDate() + 1);
77
+ return this.forDate(date, opts);
78
+ }
79
+ thisWeek(opts) {
80
+ return this.forWeek(undefined, opts);
81
+ }
82
+ currentLesson(opts) {
83
+ const now = new Date();
84
+ const lessons = this.forDate(now, opts);
85
+ const timeMinutes = now.getHours() * 60 + now.getMinutes();
86
+ for (const lesson of lessons) {
87
+ const start = lesson.start.hours * 60 + lesson.start.minutes;
88
+ const end = lesson.end.hours * 60 + lesson.end.minutes;
89
+ if (timeMinutes >= start && timeMinutes <= end) {
90
+ return lesson;
91
+ }
92
+ }
93
+ return null;
94
+ }
95
+ getWeekNumber(date) {
96
+ return getWeekNumber({ period: this.period, date });
97
+ }
98
+ getSemesterWeeks(weekCount) {
99
+ return getSemesterWeeks({ period: this.period, weekCount });
100
+ }
101
+ getSemesterStart() {
102
+ return getSemesterStart({ period: this.period });
122
103
  }
123
- return lessons;
124
104
  }
@@ -50,14 +50,6 @@ export interface Lesson {
50
50
  subgroup?: number;
51
51
  weekParity?: "even" | "odd";
52
52
  }
53
- export interface ScheduleWeekDay {
54
- date: Date;
55
- lessons: Lesson[];
56
- }
57
- export interface ScheduleFilter {
58
- subgroup?: number;
59
- week?: number;
60
- }
61
53
  export interface SemesterWeek {
62
54
  week: number;
63
55
  start: Date;
@@ -0,0 +1,34 @@
1
+ import type { FullScheduleSlot, SemesterWeek, Lesson } from "./types.js";
2
+ import { Period } from "../common/types.js";
3
+ export declare function getWeekdayName(weekday: number): string;
4
+ export declare function getMonday(date: Date): Date;
5
+ /**
6
+ * Get the start date of a semester.
7
+ * Fall: September 1 of the given year.
8
+ * Spring: first Monday of February of the given year.
9
+ */
10
+ export declare function getSemesterStart(opts: {
11
+ period: Period;
12
+ year?: number;
13
+ }): Date;
14
+ /**
15
+ * Get all weeks in a semester with their start/end dates.
16
+ * Week 0 starts from the semester start date.
17
+ */
18
+ export declare function getSemesterWeeks(opts: {
19
+ period: Period;
20
+ year?: number;
21
+ weekCount?: number;
22
+ }): SemesterWeek[];
23
+ /**
24
+ * Get the current week number within a semester.
25
+ */
26
+ export declare function getWeekNumber(opts: {
27
+ period: Period;
28
+ date?: Date;
29
+ }): number;
30
+ export declare function filterSlots(slots: FullScheduleSlot[], opts?: {
31
+ subgroup?: number;
32
+ week?: number;
33
+ }): FullScheduleSlot[];
34
+ export declare function slotsToLessons(slots: FullScheduleSlot[], date: Date): Lesson[];
@@ -0,0 +1,124 @@
1
+ const WEEKDAY_NAMES = [
2
+ "Воскресенье",
3
+ "Понедельник",
4
+ "Вторник",
5
+ "Среда",
6
+ "Четверг",
7
+ "Пятница",
8
+ "Суббота",
9
+ ];
10
+ export function getWeekdayName(weekday) {
11
+ return WEEKDAY_NAMES[weekday] ?? "";
12
+ }
13
+ export function getMonday(date) {
14
+ const d = new Date(date);
15
+ const day = d.getDay();
16
+ const diff = day === 0 ? -6 : 1 - day;
17
+ d.setDate(d.getDate() + diff);
18
+ d.setHours(0, 0, 0, 0);
19
+ return d;
20
+ }
21
+ /**
22
+ * Get the start date of a semester.
23
+ * Fall: September 1 of the given year.
24
+ * Spring: first Monday of February of the given year.
25
+ */
26
+ export function getSemesterStart(opts) {
27
+ const year = opts.year ?? new Date().getFullYear();
28
+ if (opts.period === 1 /* Period.FallSemester */) {
29
+ return new Date(year, 8, 1); // September 1
30
+ }
31
+ // Spring: first Monday of February
32
+ const feb1 = new Date(year, 1, 1);
33
+ const day = feb1.getDay();
34
+ const daysToAdd = day === 1 ? 0 : day === 0 ? 1 : 8 - day;
35
+ const firstMonday = new Date(year, 1, 1 + daysToAdd);
36
+ firstMonday.setHours(0, 0, 0, 0);
37
+ return firstMonday;
38
+ }
39
+ /**
40
+ * Get all weeks in a semester with their start/end dates.
41
+ * Week 0 starts from the semester start date.
42
+ */
43
+ export function getSemesterWeeks(opts) {
44
+ const weekCount = opts.weekCount ?? 17;
45
+ const semesterStart = getSemesterStart(opts);
46
+ const startMonday = getMonday(semesterStart);
47
+ const weeks = [];
48
+ for (let i = 0; i <= weekCount; i++) {
49
+ const start = new Date(startMonday);
50
+ start.setDate(startMonday.getDate() + i * 7);
51
+ const end = new Date(start);
52
+ end.setDate(start.getDate() + 6);
53
+ end.setHours(23, 59, 59, 999);
54
+ weeks.push({ week: i, start, end });
55
+ }
56
+ return weeks;
57
+ }
58
+ /**
59
+ * Get the current week number within a semester.
60
+ */
61
+ export function getWeekNumber(opts) {
62
+ const date = opts.date ?? new Date();
63
+ const semesterStart = getSemesterStart(opts);
64
+ const startMonday = getMonday(semesterStart);
65
+ const targetMonday = getMonday(date);
66
+ const diff = targetMonday.getTime() - startMonday.getTime();
67
+ return Math.floor(diff / (7 * 24 * 60 * 60 * 1000));
68
+ }
69
+ function filterEntries(entries, opts) {
70
+ return entries.filter((e) => {
71
+ if (opts?.subgroup && e.subgroup && e.subgroup !== opts.subgroup) {
72
+ return false;
73
+ }
74
+ if (opts?.week != null) {
75
+ if (e.weeks.from > 0 &&
76
+ (opts.week < e.weeks.from || opts.week > e.weeks.to)) {
77
+ return false;
78
+ }
79
+ if (e.weekParity) {
80
+ const isEven = opts.week % 2 === 0;
81
+ if (e.weekParity === "even" && !isEven)
82
+ return false;
83
+ if (e.weekParity === "odd" && isEven)
84
+ return false;
85
+ }
86
+ }
87
+ return true;
88
+ });
89
+ }
90
+ export function filterSlots(slots, opts) {
91
+ if (opts?.subgroup == null && opts?.week == null)
92
+ return slots;
93
+ return slots
94
+ .map((slot) => ({
95
+ ...slot,
96
+ entries: filterEntries(slot.entries, opts),
97
+ }))
98
+ .filter((slot) => slot.entries.length > 0);
99
+ }
100
+ function makeLessonTime(date, time) {
101
+ const d = new Date(date);
102
+ d.setHours(time.hours, time.minutes, 0, 0);
103
+ return { date: d, hours: time.hours, minutes: time.minutes };
104
+ }
105
+ export function slotsToLessons(slots, date) {
106
+ const lessons = [];
107
+ for (const slot of slots) {
108
+ for (const entry of slot.entries) {
109
+ lessons.push({
110
+ number: slot.number,
111
+ start: makeLessonTime(date, slot.timeStart),
112
+ end: makeLessonTime(date, slot.timeEnd),
113
+ subject: entry.subject,
114
+ type: entry.type,
115
+ room: entry.room,
116
+ teacher: entry.teacher,
117
+ weeks: entry.weeks,
118
+ subgroup: entry.subgroup,
119
+ weekParity: entry.weekParity,
120
+ });
121
+ }
122
+ }
123
+ return lessons;
124
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chuvsu-js",
3
- "version": "0.2.0",
3
+ "version": "1.0.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",