chuvsu-js 4.2.0 → 5.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 +165 -193
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +1 -1
- package/dist/common/cache.js +30 -9
- package/dist/common/http.d.ts +6 -0
- package/dist/common/http.js +8 -1
- package/dist/common/parse.d.ts +1 -1
- package/dist/common/parse.js +2 -2
- package/dist/common/types.d.ts +8 -2
- package/dist/common/types.js +12 -0
- package/dist/core.d.ts +10 -0
- package/dist/core.js +5 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -4
- package/dist/lk/client.d.ts +8 -6
- package/dist/lk/client.js +51 -23
- package/dist/lk/types.d.ts +7 -7
- package/dist/node.d.ts +1 -0
- package/dist/node.js +1 -0
- package/dist/parsers.d.ts +3 -0
- package/dist/parsers.js +2 -0
- package/dist/tt/client.d.ts +55 -63
- package/dist/tt/client.js +477 -226
- package/dist/tt/domain/directory.d.ts +14 -0
- package/dist/tt/domain/directory.js +116 -0
- package/dist/tt/domain/ids.d.ts +7 -0
- package/dist/tt/domain/ids.js +20 -0
- package/dist/tt/domain/index.d.ts +7 -0
- package/dist/tt/domain/index.js +5 -0
- package/dist/tt/domain/normalize.d.ts +7 -0
- package/dist/tt/domain/normalize.js +65 -0
- package/dist/tt/domain/repository.d.ts +51 -0
- package/dist/tt/domain/repository.js +805 -0
- package/dist/tt/domain/schedule.d.ts +37 -0
- package/dist/tt/domain/schedule.js +292 -0
- package/dist/tt/domain/types.d.ts +175 -0
- package/dist/tt/domain/types.js +1 -0
- package/dist/tt/observations.d.ts +11 -0
- package/dist/tt/observations.js +131 -0
- package/dist/tt/parse/audience.d.ts +3 -3
- package/dist/tt/parse/audience.js +47 -15
- package/dist/tt/parse/entry-parts.d.ts +1 -0
- package/dist/tt/parse/entry-parts.js +5 -2
- package/dist/tt/parse/full-schedule.d.ts +9 -4
- package/dist/tt/parse/full-schedule.js +32 -41
- package/dist/tt/parse/groups.d.ts +1 -1
- package/dist/tt/parse/groups.js +1 -1
- package/dist/tt/parse/index.d.ts +4 -4
- package/dist/tt/parse/index.js +4 -4
- package/dist/tt/parse/lists.d.ts +6 -5
- package/dist/tt/parse/lists.js +7 -2
- package/dist/tt/parse/overlays.d.ts +5 -4
- package/dist/tt/parse/overlays.js +13 -14
- package/dist/tt/parse/teacher.d.ts +2 -3
- package/dist/tt/parse/teacher.js +14 -56
- package/dist/tt/parse/webinars.js +3 -4
- package/dist/tt/types.d.ts +54 -69
- package/dist/tt/utils/date.d.ts +4 -0
- package/dist/tt/utils/date.js +30 -1
- package/dist/tt/utils/index.d.ts +2 -3
- package/dist/tt/utils/index.js +2 -3
- package/dist/tt/utils/period.d.ts +4 -4
- package/dist/tt/utils/period.js +9 -8
- package/dist/tt/utils/semester.d.ts +4 -4
- package/dist/tt/utils/semester.js +25 -5
- package/dist/tt/utils/time-slots.d.ts +3 -5
- package/dist/tt/utils/time-slots.js +19 -34
- package/dist/tt/webinars.d.ts +7 -0
- package/dist/tt/webinars.js +38 -0
- package/docs/fixture-review.md +56 -0
- package/docs/testing.md +23 -0
- package/docs/v5-architecture.md +144 -0
- package/docs/v5-migration.md +451 -0
- package/package.json +17 -7
- package/dist/shared.d.ts +0 -9
- package/dist/shared.js +0 -7
- package/dist/tt/schedule.d.ts +0 -55
- package/dist/tt/schedule.js +0 -233
- package/dist/tt/utils/lessons.d.ts +0 -16
- package/dist/tt/utils/lessons.js +0 -173
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { GroupRef, RoomRef, TeacherRef, TimetableDirectorySnapshot } from "./types.js";
|
|
2
|
+
export declare class TimetableDirectory {
|
|
3
|
+
private groups;
|
|
4
|
+
private teachers;
|
|
5
|
+
private rooms;
|
|
6
|
+
constructor(snapshot?: TimetableDirectorySnapshot);
|
|
7
|
+
rememberGroups(values: GroupRef[]): boolean;
|
|
8
|
+
rememberTeachers(values: TeacherRef[]): boolean;
|
|
9
|
+
rememberRooms(values: RoomRef[]): boolean;
|
|
10
|
+
resolveGroup(value: GroupRef): GroupRef;
|
|
11
|
+
resolveTeacher(value: TeacherRef): TeacherRef;
|
|
12
|
+
resolveRoom(value: RoomRef): RoomRef;
|
|
13
|
+
export(): TimetableDirectorySnapshot;
|
|
14
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { mergeEntityRefs, normalizeScheduleText, } from "./normalize.js";
|
|
2
|
+
function personKey(name) {
|
|
3
|
+
const words = normalizeScheduleText(name)
|
|
4
|
+
.replace(/[.,]/gu, " ")
|
|
5
|
+
.split(/\s+/gu)
|
|
6
|
+
.filter(Boolean);
|
|
7
|
+
const surname = words[0] ?? "";
|
|
8
|
+
const initials = words
|
|
9
|
+
.slice(1)
|
|
10
|
+
.map((word) => word[0] ?? "")
|
|
11
|
+
.join("");
|
|
12
|
+
return `${surname}|${initials}`;
|
|
13
|
+
}
|
|
14
|
+
function exactKey(name) {
|
|
15
|
+
return normalizeScheduleText(name);
|
|
16
|
+
}
|
|
17
|
+
function validateRefs(values, label) {
|
|
18
|
+
for (const value of values) {
|
|
19
|
+
if (!value.name.trim())
|
|
20
|
+
throw new TypeError(`${label} name is empty`);
|
|
21
|
+
if (value.id != null &&
|
|
22
|
+
(!Number.isInteger(value.id) || value.id < 1)) {
|
|
23
|
+
throw new RangeError(`${label} ID must be a positive integer`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function mergeByIdAndName(current, incoming) {
|
|
28
|
+
const values = mergeEntityRefs([...current, ...incoming]);
|
|
29
|
+
const before = JSON.stringify(current);
|
|
30
|
+
const after = JSON.stringify(values);
|
|
31
|
+
return { values, changed: before !== after };
|
|
32
|
+
}
|
|
33
|
+
function mergeTeachersByIdentity(current, incoming) {
|
|
34
|
+
const merged = mergeEntityRefs([...current, ...incoming]);
|
|
35
|
+
const identified = merged.filter((value) => value.id != null);
|
|
36
|
+
const unresolved = [];
|
|
37
|
+
for (const alias of merged.filter((value) => value.id == null)) {
|
|
38
|
+
const matches = identified
|
|
39
|
+
.map((value, index) => ({ value, index }))
|
|
40
|
+
.filter(({ value }) => personKey(value.name) === personKey(alias.name));
|
|
41
|
+
const ids = new Set(matches.map(({ value }) => value.id));
|
|
42
|
+
if (ids.size === 1) {
|
|
43
|
+
const match = matches[0];
|
|
44
|
+
identified[match.index] = { ...alias, ...match.value };
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
unresolved.push(alias);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const values = mergeEntityRefs([...identified, ...unresolved]);
|
|
51
|
+
return {
|
|
52
|
+
values,
|
|
53
|
+
changed: JSON.stringify(current) !== JSON.stringify(values),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function resolveUnique(value, known, key) {
|
|
57
|
+
if (value.id != null) {
|
|
58
|
+
const byId = known.find((candidate) => candidate.id === value.id);
|
|
59
|
+
return byId ? mergeEntityRefs([value, byId])[0] : value;
|
|
60
|
+
}
|
|
61
|
+
const lookup = key(value.name);
|
|
62
|
+
const candidates = known.filter((candidate) => candidate.id != null && key(candidate.name) === lookup);
|
|
63
|
+
const ids = new Set(candidates.map((candidate) => candidate.id));
|
|
64
|
+
if (ids.size !== 1)
|
|
65
|
+
return value;
|
|
66
|
+
return mergeEntityRefs([value, candidates[0]])[0];
|
|
67
|
+
}
|
|
68
|
+
export class TimetableDirectory {
|
|
69
|
+
groups = [];
|
|
70
|
+
teachers = [];
|
|
71
|
+
rooms = [];
|
|
72
|
+
constructor(snapshot) {
|
|
73
|
+
if (snapshot) {
|
|
74
|
+
validateRefs(snapshot.groups, "Group");
|
|
75
|
+
validateRefs(snapshot.teachers, "Teacher");
|
|
76
|
+
validateRefs(snapshot.rooms, "Room");
|
|
77
|
+
this.groups = mergeEntityRefs(structuredClone(snapshot.groups));
|
|
78
|
+
this.teachers = mergeTeachersByIdentity([], structuredClone(snapshot.teachers)).values;
|
|
79
|
+
this.rooms = mergeEntityRefs(structuredClone(snapshot.rooms));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
rememberGroups(values) {
|
|
83
|
+
validateRefs(values, "Group");
|
|
84
|
+
const result = mergeByIdAndName(this.groups, values);
|
|
85
|
+
this.groups = result.values;
|
|
86
|
+
return result.changed;
|
|
87
|
+
}
|
|
88
|
+
rememberTeachers(values) {
|
|
89
|
+
validateRefs(values, "Teacher");
|
|
90
|
+
const result = mergeTeachersByIdentity(this.teachers, values);
|
|
91
|
+
this.teachers = result.values;
|
|
92
|
+
return result.changed;
|
|
93
|
+
}
|
|
94
|
+
rememberRooms(values) {
|
|
95
|
+
validateRefs(values, "Room");
|
|
96
|
+
const result = mergeByIdAndName(this.rooms, values);
|
|
97
|
+
this.rooms = result.values;
|
|
98
|
+
return result.changed;
|
|
99
|
+
}
|
|
100
|
+
resolveGroup(value) {
|
|
101
|
+
return resolveUnique(value, this.groups, exactKey);
|
|
102
|
+
}
|
|
103
|
+
resolveTeacher(value) {
|
|
104
|
+
return resolveUnique(value, this.teachers, personKey);
|
|
105
|
+
}
|
|
106
|
+
resolveRoom(value) {
|
|
107
|
+
return resolveUnique(value, this.rooms, exactKey);
|
|
108
|
+
}
|
|
109
|
+
export() {
|
|
110
|
+
return {
|
|
111
|
+
groups: structuredClone(this.groups),
|
|
112
|
+
teachers: structuredClone(this.teachers),
|
|
113
|
+
rooms: structuredClone(this.rooms),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { LessonId, LessonIdGenerator, LessonSeriesId } from "./types.js";
|
|
2
|
+
export declare class RandomLessonIdGenerator implements LessonIdGenerator {
|
|
3
|
+
seriesId(): LessonSeriesId;
|
|
4
|
+
lessonId(): LessonId;
|
|
5
|
+
}
|
|
6
|
+
/** Stable occurrence identity inside a persisted series. */
|
|
7
|
+
export declare function occurrenceIdForSeries(seriesId: LessonSeriesId, academicWeek: number, ordinal?: number): LessonId;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
function randomId(prefix) {
|
|
2
|
+
const cryptoApi = globalThis.crypto;
|
|
3
|
+
if (cryptoApi?.randomUUID)
|
|
4
|
+
return `${prefix}_${cryptoApi.randomUUID()}`;
|
|
5
|
+
const random = Math.random().toString(36).slice(2);
|
|
6
|
+
const time = Date.now().toString(36);
|
|
7
|
+
return `${prefix}_${time}_${random}`;
|
|
8
|
+
}
|
|
9
|
+
export class RandomLessonIdGenerator {
|
|
10
|
+
seriesId() {
|
|
11
|
+
return randomId("ser");
|
|
12
|
+
}
|
|
13
|
+
lessonId() {
|
|
14
|
+
return randomId("les");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Stable occurrence identity inside a persisted series. */
|
|
18
|
+
export function occurrenceIdForSeries(seriesId, academicWeek, ordinal = 0) {
|
|
19
|
+
return `les_${seriesId}_${academicWeek}_${ordinal}`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { RandomLessonIdGenerator, occurrenceIdForSeries } from "./ids.js";
|
|
2
|
+
export { TimetableDirectory } from "./directory.js";
|
|
3
|
+
export { entityKey, mergeEntityRefs, mergeGroups, mergeRooms, mergeTeachers, normalizeScheduleText, } from "./normalize.js";
|
|
4
|
+
export { MemoryTimetableRepositoryAdapter, TimetableRepository, } from "./repository.js";
|
|
5
|
+
export { Schedule } from "./schedule.js";
|
|
6
|
+
export type { ScheduleQueryOptions, ScheduleOptions, ScheduleWeekdayOptions, } from "./schedule.js";
|
|
7
|
+
export type { GroupAttendance, GroupRef, IngestResult, LessonId, LessonIdGenerator, LessonOccurrence, LessonRecurrence, LessonSeries, LessonSeriesId, RelationCompleteness, RelationSet, LessonSourceRef, LessonStatus, LessonSubstitution, LessonTransfer, NamedEntityRef, OccurrenceObservation, RoomRef, ScheduleObservation, ScheduleOwner, ScheduleSourceSnapshot, SeriesObservation, TeacherRef, TimetableDirectorySnapshot, TimetableRepositoryAdapter, TimetableRepositorySnapshot, } from "./types.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { RandomLessonIdGenerator, occurrenceIdForSeries } from "./ids.js";
|
|
2
|
+
export { TimetableDirectory } from "./directory.js";
|
|
3
|
+
export { entityKey, mergeEntityRefs, mergeGroups, mergeRooms, mergeTeachers, normalizeScheduleText, } from "./normalize.js";
|
|
4
|
+
export { MemoryTimetableRepositoryAdapter, TimetableRepository, } from "./repository.js";
|
|
5
|
+
export { Schedule } from "./schedule.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { GroupAttendance, NamedEntityRef, RoomRef, TeacherRef } from "./types.js";
|
|
2
|
+
export declare function normalizeScheduleText(value: string): string;
|
|
3
|
+
export declare function entityKey(value: NamedEntityRef): string;
|
|
4
|
+
export declare function mergeEntityRefs<T extends NamedEntityRef>(values: T[]): T[];
|
|
5
|
+
export declare function mergeGroups(values: GroupAttendance[]): GroupAttendance[];
|
|
6
|
+
export declare function mergeTeachers(values: TeacherRef[]): TeacherRef[];
|
|
7
|
+
export declare function mergeRooms(values: RoomRef[]): RoomRef[];
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export function normalizeScheduleText(value) {
|
|
2
|
+
return value
|
|
3
|
+
.normalize("NFKC")
|
|
4
|
+
.toLocaleLowerCase("ru-RU")
|
|
5
|
+
.replace(/ё/gu, "е")
|
|
6
|
+
.replace(/\s+/gu, " ")
|
|
7
|
+
.trim();
|
|
8
|
+
}
|
|
9
|
+
export function entityKey(value) {
|
|
10
|
+
return value.id != null
|
|
11
|
+
? `id:${value.id}`
|
|
12
|
+
: `name:${normalizeScheduleText(value.name)}`;
|
|
13
|
+
}
|
|
14
|
+
function mergeNamed(current, next) {
|
|
15
|
+
const merged = { ...current, ...next };
|
|
16
|
+
if (!next.name && current.name)
|
|
17
|
+
merged.name = current.name;
|
|
18
|
+
return merged;
|
|
19
|
+
}
|
|
20
|
+
export function mergeEntityRefs(values) {
|
|
21
|
+
const byId = new Map();
|
|
22
|
+
const byName = new Map();
|
|
23
|
+
for (const value of values) {
|
|
24
|
+
const nameKey = normalizeScheduleText(value.name);
|
|
25
|
+
const idMatch = value.id == null ? undefined : byId.get(value.id);
|
|
26
|
+
const nameMatch = nameKey ? byName.get(nameKey) : undefined;
|
|
27
|
+
const existing = idMatch ?? nameMatch;
|
|
28
|
+
const merged = existing ? mergeNamed(existing, value) : { ...value };
|
|
29
|
+
if (merged.id != null)
|
|
30
|
+
byId.set(merged.id, merged);
|
|
31
|
+
if (nameKey)
|
|
32
|
+
byName.set(nameKey, merged);
|
|
33
|
+
if (existing && existing !== merged) {
|
|
34
|
+
if (existing.id != null)
|
|
35
|
+
byId.set(existing.id, merged);
|
|
36
|
+
const oldName = normalizeScheduleText(existing.name);
|
|
37
|
+
if (oldName)
|
|
38
|
+
byName.set(oldName, merged);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return [...new Set([...byId.values(), ...byName.values()])].sort((a, b) => entityKey(a).localeCompare(entityKey(b)));
|
|
42
|
+
}
|
|
43
|
+
export function mergeGroups(values) {
|
|
44
|
+
const merged = new Map();
|
|
45
|
+
for (const value of values) {
|
|
46
|
+
const key = `${entityKey(value.group)}:${value.subgroup ?? 0}`;
|
|
47
|
+
const current = merged.get(key);
|
|
48
|
+
merged.set(key, {
|
|
49
|
+
group: current
|
|
50
|
+
? mergeEntityRefs([current.group, value.group])[0]
|
|
51
|
+
: { ...value.group },
|
|
52
|
+
subgroup: value.subgroup,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return [...merged.values()].sort((a, b) => {
|
|
56
|
+
const group = entityKey(a.group).localeCompare(entityKey(b.group));
|
|
57
|
+
return group || (a.subgroup ?? 0) - (b.subgroup ?? 0);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export function mergeTeachers(values) {
|
|
61
|
+
return mergeEntityRefs(values);
|
|
62
|
+
}
|
|
63
|
+
export function mergeRooms(values) {
|
|
64
|
+
return mergeEntityRefs(values);
|
|
65
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { AcademicPeriod } from "../../common/types.js";
|
|
2
|
+
import { TimetableDirectory } from "./directory.js";
|
|
3
|
+
import type { IngestResult, LessonIdGenerator, LessonOccurrence, LessonSeries, ScheduleOwner, ScheduleSourceSnapshot, TimetableRepositoryAdapter, TimetableRepositorySnapshot } from "./types.js";
|
|
4
|
+
export declare class TimetableRepository {
|
|
5
|
+
private readonly idGenerator;
|
|
6
|
+
private _directory;
|
|
7
|
+
private readonly sources;
|
|
8
|
+
private readonly links;
|
|
9
|
+
private readonly seriesRecords;
|
|
10
|
+
private readonly occurrenceRecords;
|
|
11
|
+
private _revision;
|
|
12
|
+
constructor(opts?: {
|
|
13
|
+
idGenerator?: LessonIdGenerator;
|
|
14
|
+
snapshot?: TimetableRepositorySnapshot;
|
|
15
|
+
});
|
|
16
|
+
get directory(): TimetableDirectory;
|
|
17
|
+
get revision(): number;
|
|
18
|
+
rememberGroups(values: Parameters<TimetableDirectory["rememberGroups"]>[0]): void;
|
|
19
|
+
rememberTeachers(values: Parameters<TimetableDirectory["rememberTeachers"]>[0]): void;
|
|
20
|
+
rememberRooms(values: Parameters<TimetableDirectory["rememberRooms"]>[0]): void;
|
|
21
|
+
ingest(snapshot: ScheduleSourceSnapshot): IngestResult;
|
|
22
|
+
getSeries(opts?: {
|
|
23
|
+
owner?: ScheduleOwner;
|
|
24
|
+
academicYearStartYear?: number;
|
|
25
|
+
periods?: readonly AcademicPeriod[];
|
|
26
|
+
}): LessonSeries[];
|
|
27
|
+
getDirectOccurrences(opts?: {
|
|
28
|
+
owner?: ScheduleOwner;
|
|
29
|
+
academicYearStartYear?: number;
|
|
30
|
+
periods?: readonly AcademicPeriod[];
|
|
31
|
+
}): LessonOccurrence[];
|
|
32
|
+
export(): TimetableRepositorySnapshot;
|
|
33
|
+
replaceSnapshot(snapshot: TimetableRepositorySnapshot): void;
|
|
34
|
+
private restore;
|
|
35
|
+
private validateSource;
|
|
36
|
+
private rememberSourceEntities;
|
|
37
|
+
private pruneEmptyRecords;
|
|
38
|
+
private removeClaim;
|
|
39
|
+
private addClaim;
|
|
40
|
+
private findPriorTarget;
|
|
41
|
+
private findCanonicalTarget;
|
|
42
|
+
private aggregateSubstitutions;
|
|
43
|
+
private aggregateSeries;
|
|
44
|
+
private aggregateOccurrence;
|
|
45
|
+
}
|
|
46
|
+
export declare class MemoryTimetableRepositoryAdapter implements TimetableRepositoryAdapter {
|
|
47
|
+
private snapshot;
|
|
48
|
+
load(): Promise<TimetableRepositorySnapshot | null>;
|
|
49
|
+
compareAndSet(expectedRevision: number, snapshot: TimetableRepositorySnapshot): Promise<boolean>;
|
|
50
|
+
clear(): Promise<void>;
|
|
51
|
+
}
|