mirror of
https://github.com/n08i40k/schedule-parser-next.git
synced 2025-12-06 09:47:46 +03:00
Много
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
XlsDownloaderBase,
|
||||
XlsDownloaderCacheMode,
|
||||
XlsDownloaderResult,
|
||||
} from "../xls-downloader/xls-downloader.base";
|
||||
|
||||
import * as XLSX from "xlsx";
|
||||
@@ -17,11 +16,11 @@ import { trimAll } from "../../../utility/string.util";
|
||||
type InternalId = { row: number; column: number; name: string };
|
||||
type InternalDay = InternalId & { lessons: Array<InternalId> };
|
||||
|
||||
export type ScheduleParseResult = {
|
||||
export class ScheduleParseResult {
|
||||
etag: string;
|
||||
group: GroupDto;
|
||||
affectedDays: Array<number>;
|
||||
};
|
||||
groups: Array<GroupDto>;
|
||||
affectedDays: Array<Array<number>>;
|
||||
}
|
||||
|
||||
export class ScheduleParser {
|
||||
private lastResult: ScheduleParseResult | null = null;
|
||||
@@ -72,13 +71,13 @@ export class ScheduleParser {
|
||||
}
|
||||
|
||||
parseSkeleton(worksheet: XLSX.Sheet): {
|
||||
groupSkeleton: InternalId;
|
||||
groupSkeletons: Array<InternalId>;
|
||||
daySkeletons: Array<InternalDay>;
|
||||
} {
|
||||
const range = XLSX.utils.decode_range(worksheet["!ref"] || "");
|
||||
let isHeaderParsed: boolean = false;
|
||||
|
||||
let group: InternalId = null;
|
||||
const groups: Array<InternalId> = [];
|
||||
const days: Array<InternalDay> = [];
|
||||
|
||||
for (let row = range.s.r + 1; row <= range.e.r; ++row) {
|
||||
@@ -99,10 +98,9 @@ export class ScheduleParser {
|
||||
row,
|
||||
column,
|
||||
);
|
||||
if (!groupName || this.group !== groupName) continue;
|
||||
if (!groupName) continue;
|
||||
|
||||
group = { row: row, column: column, name: groupName };
|
||||
break;
|
||||
groups.push({ row: row, column: column, name: groupName });
|
||||
}
|
||||
++row;
|
||||
}
|
||||
@@ -116,33 +114,27 @@ export class ScheduleParser {
|
||||
break;
|
||||
}
|
||||
|
||||
return { daySkeletons: days, groupSkeleton: group };
|
||||
return { daySkeletons: days, groupSkeletons: groups };
|
||||
}
|
||||
|
||||
async getSchedule(
|
||||
forceCached: boolean = false,
|
||||
): Promise<ScheduleParseResult> {
|
||||
let downloadData: XlsDownloaderResult;
|
||||
if (forceCached && this.lastResult !== null) return this.lastResult;
|
||||
|
||||
const downloadData = await this.xlsDownloader.downloadXLS();
|
||||
|
||||
if (
|
||||
!forceCached ||
|
||||
(downloadData = await this.xlsDownloader.getCachedXLS()) === null
|
||||
!downloadData.new &&
|
||||
this.lastResult &&
|
||||
this.xlsDownloader.getCacheMode() !== XlsDownloaderCacheMode.NONE
|
||||
) {
|
||||
console.debug("Обновление кеша...");
|
||||
downloadData = await this.xlsDownloader.downloadXLS();
|
||||
console.debug(
|
||||
"Так как скачанный XLS не новый, присутствует уже готовый результат и кеширование не отключено...",
|
||||
);
|
||||
console.debug("будет возвращён предыдущий результат.");
|
||||
|
||||
if (
|
||||
!downloadData.new &&
|
||||
this.lastResult &&
|
||||
this.xlsDownloader.getCacheMode() != XlsDownloaderCacheMode.NONE
|
||||
) {
|
||||
console.debug(
|
||||
"Так как скачанный XLS не новый, присутствует уже готовый результат и кеширование не отключено...",
|
||||
);
|
||||
console.debug("будет возвращён предыдущий результат.");
|
||||
|
||||
return this.lastResult;
|
||||
}
|
||||
return this.lastResult;
|
||||
}
|
||||
|
||||
console.debug("Чтение кешированного XLS документа...");
|
||||
@@ -150,102 +142,115 @@ export class ScheduleParser {
|
||||
const workBook = XLSX.read(downloadData.fileData);
|
||||
const workSheet = workBook.Sheets[workBook.SheetNames[0]];
|
||||
|
||||
const { groupSkeleton, daySkeletons } = this.parseSkeleton(workSheet);
|
||||
const { groupSkeletons, daySkeletons } = this.parseSkeleton(workSheet);
|
||||
|
||||
const group = new GroupDto(groupSkeleton.name);
|
||||
const groups: Array<GroupDto> = [];
|
||||
|
||||
for (let dayIdx = 0; dayIdx < daySkeletons.length - 1; ++dayIdx) {
|
||||
const daySkeleton = daySkeletons[dayIdx];
|
||||
const day = new DayDto(daySkeleton.name);
|
||||
for (const groupSkeleton of groupSkeletons) {
|
||||
const group = new GroupDto(groupSkeleton.name);
|
||||
|
||||
const lessonTimeColumn = daySkeletons[0].column + 1;
|
||||
const rowDistance = daySkeletons[dayIdx + 1].row - daySkeleton.row;
|
||||
for (let dayIdx = 0; dayIdx < daySkeletons.length - 1; ++dayIdx) {
|
||||
const daySkeleton = daySkeletons[dayIdx];
|
||||
const day = new DayDto(daySkeleton.name);
|
||||
|
||||
for (
|
||||
let row = daySkeleton.row;
|
||||
row < daySkeleton.row + rowDistance;
|
||||
++row
|
||||
) {
|
||||
const time = ScheduleParser.getCellName(
|
||||
workSheet,
|
||||
row,
|
||||
lessonTimeColumn,
|
||||
)?.replaceAll(" ", "");
|
||||
if (!time || typeof time !== "string") continue;
|
||||
const lessonTimeColumn = daySkeletons[0].column + 1;
|
||||
const rowDistance =
|
||||
daySkeletons[dayIdx + 1].row - daySkeleton.row;
|
||||
|
||||
const rawName = ScheduleParser.getCellName(
|
||||
workSheet,
|
||||
row,
|
||||
groupSkeleton.column,
|
||||
);
|
||||
const cabinets: Array<string> = [];
|
||||
|
||||
const rawCabinets = String(
|
||||
ScheduleParser.getCellName(
|
||||
for (
|
||||
let row = daySkeleton.row;
|
||||
row < daySkeleton.row + rowDistance;
|
||||
++row
|
||||
) {
|
||||
const time = ScheduleParser.getCellName(
|
||||
workSheet,
|
||||
row,
|
||||
groupSkeleton.column + 1,
|
||||
),
|
||||
);
|
||||
if (rawCabinets !== "null") {
|
||||
const rawLessonCabinetParts = rawCabinets.split(/(\n|\s)/g);
|
||||
lessonTimeColumn,
|
||||
)?.replaceAll(" ", "");
|
||||
if (!time || typeof time !== "string") continue;
|
||||
|
||||
for (const cabinet of rawLessonCabinetParts) {
|
||||
if (
|
||||
cabinet.length === 0 ||
|
||||
cabinet === " " ||
|
||||
cabinet === "\n"
|
||||
)
|
||||
continue;
|
||||
const rawName = ScheduleParser.getCellName(
|
||||
workSheet,
|
||||
row,
|
||||
groupSkeleton.column,
|
||||
);
|
||||
const cabinets: Array<string> = [];
|
||||
|
||||
cabinets.push(cabinet);
|
||||
const rawCabinets = String(
|
||||
ScheduleParser.getCellName(
|
||||
workSheet,
|
||||
row,
|
||||
groupSkeleton.column + 1,
|
||||
),
|
||||
);
|
||||
if (rawCabinets !== "null") {
|
||||
const rawLessonCabinetParts =
|
||||
rawCabinets.split(/(\n|\s)/g);
|
||||
|
||||
for (const cabinet of rawLessonCabinetParts) {
|
||||
if (
|
||||
cabinet.length === 0 ||
|
||||
cabinet === " " ||
|
||||
cabinet === "\n"
|
||||
)
|
||||
continue;
|
||||
|
||||
cabinets.push(cabinet);
|
||||
}
|
||||
}
|
||||
|
||||
if (!rawName || rawName.length === 0) {
|
||||
day.lessons.push(null);
|
||||
continue;
|
||||
}
|
||||
|
||||
const type = time?.includes("пара")
|
||||
? LessonTypeDto.DEFAULT
|
||||
: LessonTypeDto.CUSTOM;
|
||||
|
||||
const { name, teacherFullNames } =
|
||||
this.parseTeacherFullNames(
|
||||
trimAll(rawName?.replace("\n", "") ?? ""),
|
||||
);
|
||||
|
||||
day.lessons.push(
|
||||
new LessonDto(
|
||||
type,
|
||||
LessonTimeDto.fromString(
|
||||
type === LessonTypeDto.DEFAULT
|
||||
? time.substring(5)
|
||||
: time,
|
||||
),
|
||||
name,
|
||||
cabinets,
|
||||
teacherFullNames,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const type =
|
||||
!rawName || rawName.length === 0
|
||||
? LessonTypeDto.NONE
|
||||
: time?.includes("пара")
|
||||
? LessonTypeDto.DEFAULT
|
||||
: LessonTypeDto.CUSTOM;
|
||||
day.fillIndices();
|
||||
|
||||
const { name, teacherFullNames } = this.parseTeacherFullNames(
|
||||
trimAll(rawName?.replace("\n", "") ?? ""),
|
||||
);
|
||||
|
||||
day.lessons.push(
|
||||
new LessonDto(
|
||||
type,
|
||||
LessonTimeDto.fromString(
|
||||
type === LessonTypeDto.DEFAULT
|
||||
? time.substring(5)
|
||||
: time,
|
||||
),
|
||||
name,
|
||||
cabinets,
|
||||
teacherFullNames,
|
||||
),
|
||||
);
|
||||
if (day.nonNullIndices.length == 0) group.days.push(null);
|
||||
else group.days.push(day);
|
||||
}
|
||||
|
||||
day.fillIndices();
|
||||
group.days.push(day);
|
||||
groups[group.name] = group;
|
||||
}
|
||||
|
||||
return (this.lastResult = {
|
||||
etag: downloadData.etag,
|
||||
group: group,
|
||||
affectedDays: this.getAffectedDays(this.lastResult?.group, group),
|
||||
groups: groups,
|
||||
affectedDays: this.getAffectedDays(this.lastResult?.groups, groups),
|
||||
});
|
||||
}
|
||||
|
||||
private getAffectedDays(
|
||||
cachedGroup: GroupDto | null,
|
||||
group: GroupDto,
|
||||
): Array<number> {
|
||||
const affectedDays: Array<number> = [];
|
||||
cachedGroups: Array<GroupDto> | null,
|
||||
groups: Array<GroupDto>,
|
||||
): Array<Array<number>> {
|
||||
const affectedDays: Array<Array<number>> = [];
|
||||
|
||||
if (!cachedGroup) return affectedDays;
|
||||
if (!cachedGroups) return affectedDays;
|
||||
|
||||
// noinspection SpellCheckingInspection
|
||||
const dayEquals = (lday: DayDto | null, rday: DayDto): boolean => {
|
||||
@@ -276,14 +281,23 @@ export class ScheduleParser {
|
||||
return true;
|
||||
};
|
||||
|
||||
for (const dayIdx in group.days) {
|
||||
// noinspection SpellCheckingInspection
|
||||
const lday = group.days[dayIdx];
|
||||
// noinspection SpellCheckingInspection
|
||||
const rday = cachedGroup.days[dayIdx];
|
||||
for (const groupName in cachedGroups) {
|
||||
const cachedGroup = cachedGroups[groupName];
|
||||
const group = groups[groupName];
|
||||
|
||||
if (!dayEquals(lday, rday))
|
||||
affectedDays.push(Number.parseInt(dayIdx));
|
||||
const affectedGroupDays: Array<number> = [];
|
||||
|
||||
for (const dayIdx in group.days) {
|
||||
// noinspection SpellCheckingInspection
|
||||
const lday = group.days[dayIdx];
|
||||
// noinspection SpellCheckingInspection
|
||||
const rday = cachedGroup.days[dayIdx];
|
||||
|
||||
if (!dayEquals(lday, rday))
|
||||
affectedGroupDays.push(Number.parseInt(dayIdx));
|
||||
}
|
||||
|
||||
affectedDays[groupName] = affectedGroupDays;
|
||||
}
|
||||
|
||||
return affectedDays;
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
HttpStatus, Post,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { AuthGuard } from "../auth/auth.guard";
|
||||
import { ScheduleService } from "./schedule.service";
|
||||
import { ScheduleDto } from "../dto/schedule.dto";
|
||||
import {
|
||||
GroupScheduleDto,
|
||||
GroupScheduleRequestDto,
|
||||
ScheduleDto,
|
||||
ScheduleGroupsDto,
|
||||
} from "../dto/schedule.dto";
|
||||
import { ResultDto } from "../utility/validation/class-validator.interceptor";
|
||||
import {
|
||||
ApiExtraModels,
|
||||
ApiNotFoundResponse,
|
||||
ApiOkResponse,
|
||||
ApiOperation,
|
||||
refs,
|
||||
@@ -19,7 +26,7 @@ import {
|
||||
@Controller("api/v1/schedule")
|
||||
@UseGuards(AuthGuard)
|
||||
export class ScheduleController {
|
||||
constructor(private scheduleService: ScheduleService) {}
|
||||
constructor(private readonly scheduleService: ScheduleService) {}
|
||||
|
||||
@ApiExtraModels(ScheduleDto)
|
||||
@ApiOperation({ summary: "Получение расписания", tags: ["schedule"] })
|
||||
@@ -33,4 +40,40 @@ export class ScheduleController {
|
||||
getSchedule(): Promise<ScheduleDto> {
|
||||
return this.scheduleService.getSchedule();
|
||||
}
|
||||
|
||||
@ApiExtraModels(GroupScheduleDto)
|
||||
@ApiOperation({
|
||||
summary: "Получение расписания группы",
|
||||
tags: ["schedule"],
|
||||
})
|
||||
@ApiOkResponse({
|
||||
description: "Расписание получено успешно",
|
||||
schema: refs(GroupScheduleDto)[0],
|
||||
})
|
||||
@ApiNotFoundResponse({ description: "Требуемая группа не найдена" })
|
||||
@ResultDto(GroupScheduleDto)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("getGroup")
|
||||
getGroupSchedule(
|
||||
@Body() groupDto: GroupScheduleRequestDto,
|
||||
): Promise<GroupScheduleDto> {
|
||||
return this.scheduleService.getGroup(groupDto.name);
|
||||
}
|
||||
|
||||
@ApiExtraModels(ScheduleGroupsDto)
|
||||
@ApiOperation({
|
||||
summary: "Получение списка названий всех групп в расписании",
|
||||
tags: ["schedule"],
|
||||
})
|
||||
@ApiOkResponse({
|
||||
description: "Список получен успешно",
|
||||
schema: refs(ScheduleGroupsDto)[0],
|
||||
})
|
||||
@ApiNotFoundResponse({ description: "Требуемая группа не найдена" })
|
||||
@ResultDto(ScheduleGroupsDto)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get("getGroupNames")
|
||||
async getGroupNames(): Promise<ScheduleGroupsDto> {
|
||||
return this.scheduleService.getGroupNames();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { UsersService } from "../users/users.service";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: [ScheduleService, UsersService, PrismaService],
|
||||
controllers: [ScheduleController],
|
||||
exports: [ScheduleService],
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ScheduleParser } from "./internal/schedule-parser/schedule-parser";
|
||||
import { Inject, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import {
|
||||
ScheduleParser,
|
||||
ScheduleParseResult,
|
||||
} from "./internal/schedule-parser/schedule-parser";
|
||||
import { BasicXlsDownloader } from "./internal/xls-downloader/basic-xls-downloader";
|
||||
import { XlsDownloaderCacheMode } from "./internal/xls-downloader/xls-downloader.base";
|
||||
import { ScheduleDto } from "../dto/schedule.dto";
|
||||
import {
|
||||
GroupDto,
|
||||
GroupScheduleDto,
|
||||
ScheduleDto,
|
||||
ScheduleGroupsDto,
|
||||
} from "../dto/schedule.dto";
|
||||
import { Cache, CACHE_MANAGER } from "@nestjs/cache-manager";
|
||||
import { instanceToPlain } from "class-transformer";
|
||||
import { cacheGetOrFill } from "../utility/cache.util";
|
||||
|
||||
@Injectable()
|
||||
export class ScheduleService {
|
||||
@@ -15,26 +26,86 @@ export class ScheduleService {
|
||||
);
|
||||
|
||||
private lastCacheUpdate: Date = new Date(0);
|
||||
private lastChangedDays: Array<number> = [];
|
||||
private lastChangedDays: Array<Array<number>> = [];
|
||||
|
||||
constructor() {}
|
||||
constructor(@Inject(CACHE_MANAGER) private readonly cacheManager: Cache) {}
|
||||
|
||||
private async getSourceSchedule(): Promise<ScheduleParseResult> {
|
||||
return cacheGetOrFill(this.cacheManager, "sourceSchedule", async () => {
|
||||
this.lastCacheUpdate = new Date();
|
||||
|
||||
const schedule = await this.scheduleParser.getSchedule();
|
||||
schedule.groups = ScheduleService.toObject(
|
||||
schedule.groups,
|
||||
) as Array<GroupDto>;
|
||||
|
||||
return schedule;
|
||||
});
|
||||
}
|
||||
|
||||
private static toObject<T>(array: Array<T>): object {
|
||||
const object = {};
|
||||
|
||||
for (const item in array) object[item] = array[item];
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
async getSchedule(): Promise<ScheduleDto> {
|
||||
const now = new Date();
|
||||
const cacheExpired =
|
||||
(this.lastCacheUpdate.valueOf() - now.valueOf()) / 1000 / 60 > 5;
|
||||
return cacheGetOrFill(this.cacheManager, "schedule", async () => {
|
||||
const sourceSchedule = await this.getSourceSchedule();
|
||||
|
||||
if (cacheExpired) this.lastCacheUpdate = now;
|
||||
for (const groupName in sourceSchedule.affectedDays) {
|
||||
const affectedDays = sourceSchedule.affectedDays[groupName];
|
||||
|
||||
const schedule = await this.scheduleParser.getSchedule(!cacheExpired);
|
||||
if (schedule.affectedDays.length !== 0)
|
||||
this.lastChangedDays = schedule.affectedDays;
|
||||
if (affectedDays?.length !== 0)
|
||||
this.lastChangedDays[groupName] = affectedDays;
|
||||
}
|
||||
|
||||
return {
|
||||
updatedAt: this.lastCacheUpdate,
|
||||
groups: ScheduleService.toObject(sourceSchedule.groups),
|
||||
etag: sourceSchedule.etag,
|
||||
lastChangedDays: this.lastChangedDays,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getGroup(group: string): Promise<GroupScheduleDto> {
|
||||
const schedule = await this.getSourceSchedule();
|
||||
console.log(schedule);
|
||||
if ((schedule.groups as object)[group] === undefined) {
|
||||
throw new NotFoundException(
|
||||
"Группы с таким названием не существует!",
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
updatedAt: this.lastCacheUpdate,
|
||||
data: schedule.group,
|
||||
group: schedule.groups[group],
|
||||
etag: schedule.etag,
|
||||
lastChangedDays: this.lastChangedDays,
|
||||
lastChangedDays: this.lastChangedDays[group] ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
async getGroupNames(): Promise<ScheduleGroupsDto> {
|
||||
let groupNames: ScheduleGroupsDto | undefined =
|
||||
await this.cacheManager.get("groupNames");
|
||||
|
||||
if (!groupNames) {
|
||||
const schedule = await this.getSourceSchedule();
|
||||
const names: Array<string> = [];
|
||||
|
||||
for (const groupName in schedule.groups) names.push(groupName);
|
||||
|
||||
groupNames = { names };
|
||||
await this.cacheManager.set(
|
||||
"groupNames",
|
||||
instanceToPlain(groupNames),
|
||||
24 * 60 * 60 * 1000,
|
||||
);
|
||||
}
|
||||
|
||||
return groupNames;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user