chuvsu-js 2.8.2 → 3.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 +3 -2
- package/dist/browser.d.ts +1 -5
- package/dist/browser.js +1 -2
- package/dist/index.d.ts +1 -8
- package/dist/index.js +2 -3
- package/dist/shared.d.ts +9 -0
- package/dist/shared.js +7 -0
- package/dist/tt/client.js +1 -1
- package/dist/tt/parse/audience.d.ts +3 -0
- package/dist/tt/parse/audience.js +150 -0
- package/dist/tt/parse/full-schedule.d.ts +4 -0
- package/dist/tt/parse/full-schedule.js +190 -0
- package/dist/tt/parse/groups.d.ts +15 -0
- package/dist/tt/parse/groups.js +28 -0
- package/dist/tt/parse/index.d.ts +5 -0
- package/dist/tt/parse/index.js +5 -0
- package/dist/tt/parse/lists.d.ts +11 -0
- package/dist/tt/parse/lists.js +80 -0
- package/dist/tt/parse/overlays.d.ts +10 -0
- package/dist/tt/parse/overlays.js +104 -0
- package/dist/tt/parse/teacher.d.ts +4 -0
- package/dist/tt/parse/teacher.js +166 -0
- package/dist/tt/schedule.d.ts +1 -2
- package/dist/tt/schedule.js +2 -8
- package/dist/tt/types.d.ts +9 -4
- package/dist/tt/utils/date.d.ts +5 -0
- package/dist/tt/utils/date.js +27 -0
- package/dist/tt/{utils.d.ts → utils/holidays.d.ts} +5 -55
- package/dist/tt/utils/holidays.js +197 -0
- package/dist/tt/utils/index.d.ts +7 -0
- package/dist/tt/utils/index.js +6 -0
- package/dist/tt/utils/lessons.d.ts +14 -0
- package/dist/tt/utils/lessons.js +123 -0
- package/dist/tt/utils/period.d.ts +6 -0
- package/dist/tt/utils/period.js +24 -0
- package/dist/tt/utils/semester.d.ts +25 -0
- package/dist/tt/utils/semester.js +47 -0
- package/dist/tt/utils/time-slots.d.ts +5 -0
- package/dist/tt/utils/time-slots.js +41 -0
- package/package.json +1 -1
- package/dist/tt/parse.d.ts +0 -16
- package/dist/tt/parse.js +0 -671
- package/dist/tt/utils.js +0 -521
package/dist/tt/utils.js
DELETED
|
@@ -1,521 +0,0 @@
|
|
|
1
|
-
export function getCurrentPeriod(opts) {
|
|
2
|
-
const date = opts?.date ?? new Date();
|
|
3
|
-
const month = date.getMonth();
|
|
4
|
-
const day = date.getDate();
|
|
5
|
-
// Dec 25+ and Jan → Winter session (зимняя сессия)
|
|
6
|
-
if (month === 0 || (month === 11 && day >= 25))
|
|
7
|
-
return 2 /* Period.WinterSession */;
|
|
8
|
-
// Feb–May → Spring semester (весенний семестр)
|
|
9
|
-
if (month >= 1 && month <= 4)
|
|
10
|
-
return 3 /* Period.SpringSemester */;
|
|
11
|
-
// Jun–Aug → Summer session (летняя сессия)
|
|
12
|
-
if (month >= 5 && month <= 7)
|
|
13
|
-
return 4 /* Period.SummerSession */;
|
|
14
|
-
// Sep – Dec 24 → Fall semester (осенний семестр)
|
|
15
|
-
return 1 /* Period.FallSemester */;
|
|
16
|
-
}
|
|
17
|
-
export function isSessionPeriod(period) {
|
|
18
|
-
return period === 2 /* Period.WinterSession */ || period === 4 /* Period.SummerSession */;
|
|
19
|
-
}
|
|
20
|
-
export function sortLessons(a, b) {
|
|
21
|
-
return a.start.date.getTime() - b.start.date.getTime();
|
|
22
|
-
}
|
|
23
|
-
const WEEKDAY_NAMES = [
|
|
24
|
-
"Воскресенье",
|
|
25
|
-
"Понедельник",
|
|
26
|
-
"Вторник",
|
|
27
|
-
"Среда",
|
|
28
|
-
"Четверг",
|
|
29
|
-
"Пятница",
|
|
30
|
-
"Суббота",
|
|
31
|
-
];
|
|
32
|
-
export function getWeekdayName(weekday) {
|
|
33
|
-
return WEEKDAY_NAMES[weekday] ?? "";
|
|
34
|
-
}
|
|
35
|
-
export function getMonday(date) {
|
|
36
|
-
const d = new Date(date);
|
|
37
|
-
const day = d.getDay();
|
|
38
|
-
const diff = day === 0 ? -6 : 1 - day;
|
|
39
|
-
d.setDate(d.getDate() + diff);
|
|
40
|
-
d.setHours(0, 0, 0, 0);
|
|
41
|
-
return d;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Get the start date of a semester.
|
|
45
|
-
* Fall: September 1 of the given year.
|
|
46
|
-
* Spring: first Monday of February of the given year.
|
|
47
|
-
*/
|
|
48
|
-
export function getSemesterStart(opts) {
|
|
49
|
-
const year = opts.year ?? new Date().getFullYear();
|
|
50
|
-
if (opts.period === 1 /* Period.FallSemester */) {
|
|
51
|
-
return new Date(year, 8, 1); // September 1
|
|
52
|
-
}
|
|
53
|
-
// Spring: first Monday of February
|
|
54
|
-
const feb1 = new Date(year, 1, 1);
|
|
55
|
-
const day = feb1.getDay();
|
|
56
|
-
const daysToAdd = day === 1 ? 0 : day === 0 ? 1 : 8 - day;
|
|
57
|
-
const firstMonday = new Date(year, 1, 1 + daysToAdd);
|
|
58
|
-
firstMonday.setHours(0, 0, 0, 0);
|
|
59
|
-
return firstMonday;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Get all weeks in a semester with their start/end dates.
|
|
63
|
-
* Week 0 starts from the semester start date.
|
|
64
|
-
*/
|
|
65
|
-
export function getSemesterWeeks(opts) {
|
|
66
|
-
const weekCount = opts.weekCount ?? 17;
|
|
67
|
-
const semesterStart = getSemesterStart(opts);
|
|
68
|
-
const startMonday = getMonday(semesterStart);
|
|
69
|
-
const weeks = [];
|
|
70
|
-
for (let i = 0; i <= weekCount; i++) {
|
|
71
|
-
const start = new Date(startMonday);
|
|
72
|
-
start.setDate(startMonday.getDate() + i * 7);
|
|
73
|
-
const end = new Date(start);
|
|
74
|
-
end.setDate(start.getDate() + 6);
|
|
75
|
-
end.setHours(23, 59, 59, 999);
|
|
76
|
-
weeks.push({ week: i, start, end });
|
|
77
|
-
}
|
|
78
|
-
return weeks;
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Get the current week number within a semester.
|
|
82
|
-
*/
|
|
83
|
-
export function getWeekNumber(opts) {
|
|
84
|
-
const date = opts.date ?? new Date();
|
|
85
|
-
const semesterStart = getSemesterStart(opts);
|
|
86
|
-
const startMonday = getMonday(semesterStart);
|
|
87
|
-
const targetMonday = getMonday(date);
|
|
88
|
-
const diff = targetMonday.getTime() - startMonday.getTime();
|
|
89
|
-
return Math.floor(diff / (7 * 24 * 60 * 60 * 1000));
|
|
90
|
-
}
|
|
91
|
-
function isSameDay(a, b) {
|
|
92
|
-
return (a.getFullYear() === b.getFullYear() &&
|
|
93
|
-
a.getMonth() === b.getMonth() &&
|
|
94
|
-
a.getDate() === b.getDate());
|
|
95
|
-
}
|
|
96
|
-
function filterEntries(entries, opts) {
|
|
97
|
-
return entries.filter((e) => {
|
|
98
|
-
// Subgroup filter applies to all entry types
|
|
99
|
-
if (opts?.subgroup && e.subgroup && e.subgroup !== opts.subgroup) {
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
// Transfer entries: only include when the query date matches the target date
|
|
103
|
-
if (e.transfer) {
|
|
104
|
-
if (!opts?.date)
|
|
105
|
-
return false;
|
|
106
|
-
return isSameDay(e.transfer.targetDate, opts.date);
|
|
107
|
-
}
|
|
108
|
-
// Substitute-for entries: only include when the query date matches
|
|
109
|
-
if (e.substituteFor) {
|
|
110
|
-
if (!opts?.date)
|
|
111
|
-
return false;
|
|
112
|
-
return isSameDay(e.substituteFor.date, opts.date);
|
|
113
|
-
}
|
|
114
|
-
if (opts?.week != null) {
|
|
115
|
-
if (e.weeks.from > 0 &&
|
|
116
|
-
(opts.week < e.weeks.from || opts.week > e.weeks.to)) {
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
|
-
if (e.weekParity) {
|
|
120
|
-
const isEven = opts.week % 2 === 0;
|
|
121
|
-
if (e.weekParity === "even" && !isEven)
|
|
122
|
-
return false;
|
|
123
|
-
if (e.weekParity === "odd" && isEven)
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return true;
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
export function filterSlots(slots, opts) {
|
|
131
|
-
if (opts?.subgroup == null && opts?.week == null && opts?.date == null)
|
|
132
|
-
return slots;
|
|
133
|
-
return slots
|
|
134
|
-
.map((slot) => ({
|
|
135
|
-
...slot,
|
|
136
|
-
entries: filterEntries(slot.entries, opts),
|
|
137
|
-
}))
|
|
138
|
-
.filter((slot) => slot.entries.length > 0);
|
|
139
|
-
}
|
|
140
|
-
function makeLessonTime(date, time) {
|
|
141
|
-
const d = new Date(date);
|
|
142
|
-
d.setHours(time.hours, time.minutes, 0, 0);
|
|
143
|
-
return { date: d, hours: time.hours, minutes: time.minutes };
|
|
144
|
-
}
|
|
145
|
-
export function slotsToLessons(slots, date, opts) {
|
|
146
|
-
const lessons = [];
|
|
147
|
-
for (const slot of slots) {
|
|
148
|
-
for (const entry of slot.entries) {
|
|
149
|
-
let room = entry.room;
|
|
150
|
-
let teacher = entry.teacher;
|
|
151
|
-
let originalRoom;
|
|
152
|
-
let originalTeacher;
|
|
153
|
-
// Apply date-specific substitutions
|
|
154
|
-
if (entry.substitutions) {
|
|
155
|
-
const sub = entry.substitutions.find((s) => isSameDay(s.date, date));
|
|
156
|
-
if (sub) {
|
|
157
|
-
if (sub.room) {
|
|
158
|
-
originalRoom = room;
|
|
159
|
-
room = sub.room;
|
|
160
|
-
}
|
|
161
|
-
if (sub.teacher) {
|
|
162
|
-
// On teacher schedules, a teacher substitution means another teacher
|
|
163
|
-
// is taking over — exclude the lesson entirely.
|
|
164
|
-
if (opts?.isTeacherSchedule)
|
|
165
|
-
continue;
|
|
166
|
-
originalTeacher = teacher;
|
|
167
|
-
teacher = sub.teacher;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
lessons.push({
|
|
172
|
-
number: slot.number,
|
|
173
|
-
start: makeLessonTime(date, slot.timeStart),
|
|
174
|
-
end: makeLessonTime(date, slot.timeEnd),
|
|
175
|
-
subject: entry.subject,
|
|
176
|
-
type: entry.type,
|
|
177
|
-
room,
|
|
178
|
-
teacher,
|
|
179
|
-
groups: entry.groups,
|
|
180
|
-
weeks: entry.weeks,
|
|
181
|
-
subgroup: entry.subgroup,
|
|
182
|
-
weekParity: entry.weekParity,
|
|
183
|
-
originalRoom,
|
|
184
|
-
originalTeacher,
|
|
185
|
-
transfer: entry.transfer,
|
|
186
|
-
substituteFor: entry.substituteFor,
|
|
187
|
-
possibleChanges: entry.possibleChanges,
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
return lessons;
|
|
192
|
-
}
|
|
193
|
-
/** Collect all transfer entries from schedule days. */
|
|
194
|
-
export function collectTransfers(days) {
|
|
195
|
-
const transfers = [];
|
|
196
|
-
for (const day of days) {
|
|
197
|
-
for (const slot of day.slots) {
|
|
198
|
-
for (const entry of slot.entries) {
|
|
199
|
-
if (entry.transfer)
|
|
200
|
-
transfers.push(entry);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return transfers;
|
|
205
|
-
}
|
|
206
|
-
/** Remove lessons whose source date/slot match a transfer. */
|
|
207
|
-
export function suppressTransferredLessons(lessons, transfers, date) {
|
|
208
|
-
return lessons.filter((lesson) => {
|
|
209
|
-
return !transfers.some((t) => t.transfer &&
|
|
210
|
-
isSameDay(t.transfer.fromDate, date) &&
|
|
211
|
-
t.transfer.fromSlot === lesson.number &&
|
|
212
|
-
t.transfer.subject === lesson.subject);
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
// --- Lesson time slots ---
|
|
216
|
-
const VO_TIME_SLOTS = [
|
|
217
|
-
{
|
|
218
|
-
number: 1,
|
|
219
|
-
start: { hours: 8, minutes: 20 },
|
|
220
|
-
end: { hours: 9, minutes: 40 },
|
|
221
|
-
},
|
|
222
|
-
{
|
|
223
|
-
number: 2,
|
|
224
|
-
start: { hours: 9, minutes: 50 },
|
|
225
|
-
end: { hours: 11, minutes: 10 },
|
|
226
|
-
},
|
|
227
|
-
{
|
|
228
|
-
number: 3,
|
|
229
|
-
start: { hours: 11, minutes: 40 },
|
|
230
|
-
end: { hours: 13, minutes: 0 },
|
|
231
|
-
},
|
|
232
|
-
{
|
|
233
|
-
number: 4,
|
|
234
|
-
start: { hours: 13, minutes: 30 },
|
|
235
|
-
end: { hours: 14, minutes: 50 },
|
|
236
|
-
},
|
|
237
|
-
{
|
|
238
|
-
number: 5,
|
|
239
|
-
start: { hours: 15, minutes: 0 },
|
|
240
|
-
end: { hours: 16, minutes: 20 },
|
|
241
|
-
},
|
|
242
|
-
{
|
|
243
|
-
number: 6,
|
|
244
|
-
start: { hours: 16, minutes: 40 },
|
|
245
|
-
end: { hours: 18, minutes: 0 },
|
|
246
|
-
},
|
|
247
|
-
{
|
|
248
|
-
number: 7,
|
|
249
|
-
start: { hours: 18, minutes: 10 },
|
|
250
|
-
end: { hours: 19, minutes: 30 },
|
|
251
|
-
},
|
|
252
|
-
{
|
|
253
|
-
number: 8,
|
|
254
|
-
start: { hours: 19, minutes: 40 },
|
|
255
|
-
end: { hours: 21, minutes: 0 },
|
|
256
|
-
},
|
|
257
|
-
];
|
|
258
|
-
const SPO_TIME_SLOTS = [
|
|
259
|
-
{
|
|
260
|
-
number: 1,
|
|
261
|
-
start: { hours: 8, minutes: 10 },
|
|
262
|
-
end: { hours: 9, minutes: 40 },
|
|
263
|
-
},
|
|
264
|
-
{
|
|
265
|
-
number: 2,
|
|
266
|
-
start: { hours: 9, minutes: 55 },
|
|
267
|
-
end: { hours: 11, minutes: 25 },
|
|
268
|
-
},
|
|
269
|
-
{
|
|
270
|
-
number: 3,
|
|
271
|
-
start: { hours: 11, minutes: 55 },
|
|
272
|
-
end: { hours: 13, minutes: 25 },
|
|
273
|
-
},
|
|
274
|
-
{
|
|
275
|
-
number: 4,
|
|
276
|
-
start: { hours: 13, minutes: 40 },
|
|
277
|
-
end: { hours: 15, minutes: 10 },
|
|
278
|
-
},
|
|
279
|
-
{
|
|
280
|
-
number: 5,
|
|
281
|
-
start: { hours: 15, minutes: 25 },
|
|
282
|
-
end: { hours: 16, minutes: 55 },
|
|
283
|
-
},
|
|
284
|
-
{
|
|
285
|
-
number: 6,
|
|
286
|
-
start: { hours: 17, minutes: 10 },
|
|
287
|
-
end: { hours: 18, minutes: 40 },
|
|
288
|
-
},
|
|
289
|
-
{
|
|
290
|
-
number: 7,
|
|
291
|
-
start: { hours: 18, minutes: 55 },
|
|
292
|
-
end: { hours: 20, minutes: 25 },
|
|
293
|
-
},
|
|
294
|
-
];
|
|
295
|
-
export function getTimeSlots(educationType) {
|
|
296
|
-
return educationType === 2 /* EducationType.VocationalEducation */
|
|
297
|
-
? SPO_TIME_SLOTS
|
|
298
|
-
: VO_TIME_SLOTS;
|
|
299
|
-
}
|
|
300
|
-
function timeToMinutes(t) {
|
|
301
|
-
return t.hours * 60 + t.minutes;
|
|
302
|
-
}
|
|
303
|
-
export function getLessonNumber(time, educationType) {
|
|
304
|
-
const slots = getTimeSlots(educationType);
|
|
305
|
-
const target = timeToMinutes(time);
|
|
306
|
-
let closest = slots[0];
|
|
307
|
-
let minDiff = Math.abs(timeToMinutes(closest.start) - target);
|
|
308
|
-
for (let i = 1; i < slots.length; i++) {
|
|
309
|
-
const diff = Math.abs(timeToMinutes(slots[i].start) - target);
|
|
310
|
-
if (diff < minDiff) {
|
|
311
|
-
minDiff = diff;
|
|
312
|
-
closest = slots[i];
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return closest.number;
|
|
316
|
-
}
|
|
317
|
-
export function getAdjacentSemester(session) {
|
|
318
|
-
return session === 2 /* Period.WinterSession */
|
|
319
|
-
? 1 /* Period.FallSemester */
|
|
320
|
-
: 3 /* Period.SpringSemester */;
|
|
321
|
-
}
|
|
322
|
-
/** Russian non-working public holidays (Статья 112 ТК РФ). */
|
|
323
|
-
export const RUSSIAN_HOLIDAYS = [
|
|
324
|
-
{ month: 1, day: 1, name: "Новый год" },
|
|
325
|
-
{ month: 1, day: 2, name: "Новогодние каникулы" },
|
|
326
|
-
{ month: 1, day: 3, name: "Новогодние каникулы" },
|
|
327
|
-
{ month: 1, day: 4, name: "Новогодние каникулы" },
|
|
328
|
-
{ month: 1, day: 5, name: "Новогодние каникулы" },
|
|
329
|
-
{ month: 1, day: 6, name: "Новогодние каникулы" },
|
|
330
|
-
{ month: 1, day: 7, name: "Рождество Христово" },
|
|
331
|
-
{ month: 1, day: 8, name: "Новогодние каникулы" },
|
|
332
|
-
{ month: 2, day: 23, name: "День защитника Отечества" },
|
|
333
|
-
{ month: 3, day: 8, name: "Международный женский день" },
|
|
334
|
-
{ month: 5, day: 1, name: "Праздник Весны и Труда" },
|
|
335
|
-
{ month: 5, day: 9, name: "День Победы" },
|
|
336
|
-
{ month: 6, day: 12, name: "День России" },
|
|
337
|
-
{ month: 11, day: 4, name: "День народного единства" },
|
|
338
|
-
];
|
|
339
|
-
/**
|
|
340
|
-
* January holiday dates (1–8) are excluded from automatic weekend transfer
|
|
341
|
-
* per Art. 112 ТК РФ. Their transfers are decided by government decree.
|
|
342
|
-
*/
|
|
343
|
-
function isJanuaryHoliday(h) {
|
|
344
|
-
return h.month === 1 && h.day >= 1 && h.day <= 8;
|
|
345
|
-
}
|
|
346
|
-
/**
|
|
347
|
-
* Compute bridge-day transfers for non-January holidays.
|
|
348
|
-
*
|
|
349
|
-
* Pattern (consistent across government decrees):
|
|
350
|
-
* - Holiday on Tuesday → Monday becomes day off, preceding Saturday is work day
|
|
351
|
-
* - Holiday on Thursday → Friday becomes day off, following Saturday is work day
|
|
352
|
-
* (only for 5-day week; 6-day week has Saturday classes, so no gap to bridge)
|
|
353
|
-
*
|
|
354
|
-
* January holidays are excluded (their 2 transfers are unpredictable).
|
|
355
|
-
*/
|
|
356
|
-
function computeBridgeDays(year, holidays, effectiveDays, sixDayWeek) {
|
|
357
|
-
const bridges = [];
|
|
358
|
-
for (const h of holidays) {
|
|
359
|
-
if (isJanuaryHoliday(h))
|
|
360
|
-
continue;
|
|
361
|
-
const date = new Date(year, h.month - 1, h.day);
|
|
362
|
-
const dow = date.getDay();
|
|
363
|
-
if (dow === 2) {
|
|
364
|
-
// Tuesday → Monday off, preceding Saturday works
|
|
365
|
-
const monday = new Date(date);
|
|
366
|
-
monday.setDate(date.getDate() - 1);
|
|
367
|
-
const saturday = new Date(date);
|
|
368
|
-
saturday.setDate(date.getDate() - 3);
|
|
369
|
-
if (!effectiveDays.some((d) => isSameDay(d, monday))) {
|
|
370
|
-
bridges.push({
|
|
371
|
-
dayOff: monday,
|
|
372
|
-
workDay: sixDayWeek ? null : saturday,
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
else if (dow === 4 && !sixDayWeek) {
|
|
377
|
-
// Thursday → Friday off, following Saturday works
|
|
378
|
-
// Only for 5-day week: 6-day week has no gap (Saturday is a work day)
|
|
379
|
-
const friday = new Date(date);
|
|
380
|
-
friday.setDate(date.getDate() + 1);
|
|
381
|
-
const saturday = new Date(date);
|
|
382
|
-
saturday.setDate(date.getDate() + 2);
|
|
383
|
-
if (!effectiveDays.some((d) => isSameDay(d, friday))) {
|
|
384
|
-
bridges.push({ dayOff: friday, workDay: saturday });
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
return bridges;
|
|
389
|
-
}
|
|
390
|
-
/**
|
|
391
|
-
* Compute effective non-working holiday dates for a given year.
|
|
392
|
-
*
|
|
393
|
-
* Rules (Art. 112 ТК РФ):
|
|
394
|
-
* 1. All holidays in the list are non-working days.
|
|
395
|
-
* 2. For non-January holidays: if a holiday falls on Sat/Sun, the day off
|
|
396
|
-
* automatically transfers to the next working day.
|
|
397
|
-
* 3. For January holidays (1–8): weekend transfers are NOT automatic —
|
|
398
|
-
* they are decided by annual government decree. Pass them via `transfers`.
|
|
399
|
-
* 4. Bridge days: if a non-January holiday falls on Tue, Mon is day off
|
|
400
|
-
* (preceding Sat works); if on Thu, Fri is day off (following Sat works).
|
|
401
|
-
* Computed automatically, can be overridden via `transfers`.
|
|
402
|
-
* 5. Government decree transfers (`transfers`) add extra days off and
|
|
403
|
-
* override auto-computed bridge days.
|
|
404
|
-
* 6. For 6-day week (`sixDayWeek`): Saturday is a work day, so
|
|
405
|
-
* Thursday bridges don't apply and Saturday holidays don't auto-transfer.
|
|
406
|
-
*/
|
|
407
|
-
export function getEffectiveHolidays(year, holidays = RUSSIAN_HOLIDAYS, transfers = [], sixDayWeek = true) {
|
|
408
|
-
const originalDates = holidays.map((h) => new Date(year, h.month - 1, h.day));
|
|
409
|
-
const isOriginalHoliday = (d) => originalDates.some((od) => isSameDay(od, d));
|
|
410
|
-
const effectiveDays = [...originalDates];
|
|
411
|
-
// Auto-transfer: only non-January holidays that fall on weekends
|
|
412
|
-
const nonJanuaryOnWeekend = holidays
|
|
413
|
-
.filter((h) => !isJanuaryHoliday(h))
|
|
414
|
-
.map((h) => new Date(year, h.month - 1, h.day))
|
|
415
|
-
.filter((d) => d.getDay() === 0 || d.getDay() === 6)
|
|
416
|
-
.sort((a, b) => a.getTime() - b.getTime());
|
|
417
|
-
for (const holiday of nonJanuaryOnWeekend) {
|
|
418
|
-
let candidate = new Date(holiday);
|
|
419
|
-
if (candidate.getDay() === 6) {
|
|
420
|
-
candidate.setDate(candidate.getDate() + 2);
|
|
421
|
-
}
|
|
422
|
-
else {
|
|
423
|
-
candidate.setDate(candidate.getDate() + 1);
|
|
424
|
-
}
|
|
425
|
-
while (candidate.getDay() === 0 ||
|
|
426
|
-
candidate.getDay() === 6 ||
|
|
427
|
-
isOriginalHoliday(candidate) ||
|
|
428
|
-
effectiveDays.some((ed) => isSameDay(ed, candidate))) {
|
|
429
|
-
candidate.setDate(candidate.getDate() + 1);
|
|
430
|
-
}
|
|
431
|
-
effectiveDays.push(new Date(candidate));
|
|
432
|
-
}
|
|
433
|
-
// Bridge days (auto-computed, can be overridden)
|
|
434
|
-
const autoBridges = computeBridgeDays(year, holidays, effectiveDays, sixDayWeek);
|
|
435
|
-
// Merge: explicit transfers override auto-computed bridges
|
|
436
|
-
const allTransfers = [...autoBridges];
|
|
437
|
-
for (const t of transfers) {
|
|
438
|
-
const idx = allTransfers.findIndex((b) => isSameDay(b.dayOff, t.dayOff));
|
|
439
|
-
if (idx !== -1) {
|
|
440
|
-
allTransfers[idx] = t;
|
|
441
|
-
}
|
|
442
|
-
else {
|
|
443
|
-
allTransfers.push(t);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
const compensatingWorkDays = [];
|
|
447
|
-
for (const t of allTransfers) {
|
|
448
|
-
if (!effectiveDays.some((d) => isSameDay(d, t.dayOff))) {
|
|
449
|
-
effectiveDays.push(t.dayOff);
|
|
450
|
-
}
|
|
451
|
-
if (t.workDay) {
|
|
452
|
-
compensatingWorkDays.push(t.workDay);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
return effectiveDays.sort((a, b) => a.getTime() - b.getTime());
|
|
456
|
-
}
|
|
457
|
-
/**
|
|
458
|
-
* Compute all transfers (auto bridges + explicit) for a given year.
|
|
459
|
-
* Useful for getting compensating work days (Saturdays that become working).
|
|
460
|
-
*/
|
|
461
|
-
export function getHolidayTransfers(year, holidays = RUSSIAN_HOLIDAYS, transfers = [], sixDayWeek) {
|
|
462
|
-
const originalDates = holidays.map((h) => new Date(year, h.month - 1, h.day));
|
|
463
|
-
const effectiveDays = [...originalDates];
|
|
464
|
-
// Replay weekend transfers to build effectiveDays for bridge computation
|
|
465
|
-
const nonJanuaryOnWeekend = holidays
|
|
466
|
-
.filter((h) => !isJanuaryHoliday(h))
|
|
467
|
-
.map((h) => new Date(year, h.month - 1, h.day))
|
|
468
|
-
.filter((d) => d.getDay() === 0 || d.getDay() === 6)
|
|
469
|
-
.sort((a, b) => a.getTime() - b.getTime());
|
|
470
|
-
const isOriginalHoliday = (d) => originalDates.some((od) => isSameDay(od, d));
|
|
471
|
-
for (const holiday of nonJanuaryOnWeekend) {
|
|
472
|
-
let candidate = new Date(holiday);
|
|
473
|
-
if (candidate.getDay() === 6) {
|
|
474
|
-
candidate.setDate(candidate.getDate() + 2);
|
|
475
|
-
}
|
|
476
|
-
else {
|
|
477
|
-
candidate.setDate(candidate.getDate() + 1);
|
|
478
|
-
}
|
|
479
|
-
while (candidate.getDay() === 0 ||
|
|
480
|
-
candidate.getDay() === 6 ||
|
|
481
|
-
isOriginalHoliday(candidate) ||
|
|
482
|
-
effectiveDays.some((ed) => isSameDay(ed, candidate))) {
|
|
483
|
-
candidate.setDate(candidate.getDate() + 1);
|
|
484
|
-
}
|
|
485
|
-
effectiveDays.push(new Date(candidate));
|
|
486
|
-
}
|
|
487
|
-
const autoBridges = computeBridgeDays(year, holidays, effectiveDays, sixDayWeek);
|
|
488
|
-
const allTransfers = [...autoBridges];
|
|
489
|
-
for (const t of transfers) {
|
|
490
|
-
const idx = allTransfers.findIndex((b) => isSameDay(b.dayOff, t.dayOff));
|
|
491
|
-
if (idx !== -1) {
|
|
492
|
-
allTransfers[idx] = t;
|
|
493
|
-
}
|
|
494
|
-
else {
|
|
495
|
-
allTransfers.push(t);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
return allTransfers;
|
|
499
|
-
}
|
|
500
|
-
/**
|
|
501
|
-
* Returns the list of compensating work days (e.g. Saturdays that become working).
|
|
502
|
-
* Includes both auto-computed bridge compensations and explicit transfers.
|
|
503
|
-
*/
|
|
504
|
-
export function getCompensatingWorkDays(year, holidays = RUSSIAN_HOLIDAYS, transfers = [], sixDayWeek) {
|
|
505
|
-
return getHolidayTransfers(year, holidays, transfers, sixDayWeek)
|
|
506
|
-
.filter((t) => t.workDay != null)
|
|
507
|
-
.map((t) => t.workDay);
|
|
508
|
-
}
|
|
509
|
-
/**
|
|
510
|
-
* Returns true if the given date is a non-working holiday,
|
|
511
|
-
* including transferred holidays when they fall on weekends (Art. 112 ТК РФ)
|
|
512
|
-
* and auto-computed bridge days.
|
|
513
|
-
* Pass an empty array for `holidays` to disable holiday checking.
|
|
514
|
-
*/
|
|
515
|
-
export function isHoliday(date, holidays = RUSSIAN_HOLIDAYS, transfers = [], sixDayWeek = true) {
|
|
516
|
-
if (holidays.length === 0 && transfers.length === 0)
|
|
517
|
-
return false;
|
|
518
|
-
const year = date.getFullYear();
|
|
519
|
-
const effectiveDays = getEffectiveHolidays(year, holidays, transfers, sixDayWeek);
|
|
520
|
-
return effectiveDays.some((d) => isSameDay(d, date));
|
|
521
|
-
}
|