diff --git a/src/app/app-calendar/calendar/calendar.component.html b/src/app/app-calendar/calendar/calendar.component.html index 83abb7f..91762a2 100644 --- a/src/app/app-calendar/calendar/calendar.component.html +++ b/src/app/app-calendar/calendar/calendar.component.html @@ -29,10 +29,11 @@
{{ i + 1 }}
diff --git a/src/app/app-calendar/calendar/calendar.component.ts b/src/app/app-calendar/calendar/calendar.component.ts index 9af60da..ea8c6b7 100644 --- a/src/app/app-calendar/calendar/calendar.component.ts +++ b/src/app/app-calendar/calendar/calendar.component.ts @@ -2,6 +2,14 @@ import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; import { CalendarData, CalendarTableContent } from '../../../types/calendar'; +import { + END_AFTERNOON_SESSION, + END_EVENING_SESSION, + END_MORNING_SESSION, + START_AFTERNOON_SESSION, + START_EVENING_SESSION, + START_MORNING_SESSION, +} from '../../../constants/calendar'; @Component({ selector: 'app-calendar', @@ -12,6 +20,13 @@ import { CalendarData, CalendarTableContent } from '../../../types/calendar'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class CalendarComponent { + START_MORNING_SESSION = START_MORNING_SESSION; + END_MORNING_SESSION = END_MORNING_SESSION; + START_AFTERNOON_SESSION = START_AFTERNOON_SESSION; + END_AFTERNOON_SESSION = END_AFTERNOON_SESSION; + START_EVENING_SESSION = START_EVENING_SESSION; + END_EVENING_SESSION = END_EVENING_SESSION; + @Input('calendar$') calendar$: BehaviorSubject; @Input('calendarTableContent$') calendarTableContent$: BehaviorSubject; @@ -46,17 +61,6 @@ export class CalendarComponent { return 'evening'; } - calendarBackgroundClass(session: number): string { - switch (this.checkSession(session)) { - case 'afternoon': - return 'bg-secondary'; - case 'evening': - return 'bg-neutral'; - default: - return 'bg-accent'; - } - } - processCalendarInDate( date: number ): { start: number; end: number; defaultName: string }[][] { diff --git a/src/app/app-calendar/class-info/class-info.component.html b/src/app/app-calendar/class-info/class-info.component.html index 5ebb334..60e7843 100644 --- a/src/app/app-calendar/class-info/class-info.component.html +++ b/src/app/app-calendar/class-info/class-info.component.html @@ -2,6 +2,59 @@ id="class-info" class="overflow-scroll overflow-x-hidden overflow-y-overlay max-h-[calc(100vh-18rem)]" > +
+ + + + +
+
- -
- - - - -
diff --git a/src/types/calendar.ts b/src/types/calendar.ts index c2bd1a0..b25e9f9 100644 --- a/src/types/calendar.ts +++ b/src/types/calendar.ts @@ -94,8 +94,4 @@ export type AutoMode = | 'refer-non-overlap-afternoon' | 'refer-non-overlap-evening'; -export type CombinationCache = { - [key: string]: ClassCombination[]; -}; - export type ClassCombination = CalendarGroupByClassDetail[]; diff --git a/src/utils/calendar_overlap.ts b/src/utils/calendar_overlap.ts index 96c6da3..2a320fc 100644 --- a/src/utils/calendar_overlap.ts +++ b/src/utils/calendar_overlap.ts @@ -2,7 +2,6 @@ import { CalendarGroupByClassDetail, CalendarGroupBySubjectName, ClassCombination, - CombinationCache, } from '../types/calendar'; import { countSpecificDayOfWeek } from './date'; @@ -12,84 +11,33 @@ import { countSpecificDayOfWeek } from './date'; * @param selectedSubjects - Đối tượng chứa các môn học đã chọn, được nhóm theo tên môn học. * @returns Một mảng các tổ hợp, mỗi tổ hợp là một mảng chứa các chi tiết lớp học. * - * Hàm này sử dụng phương pháp đệ quy để tạo ra tất cả các tổ hợp có thể có từ các môn học đã chọn. - * - `subjectKeys` là mảng chứa các khóa của các môn học. - * - `combinations` là mảng chứa các tổ hợp kết quả. - * - Hàm `backtrack` được sử dụng để duyệt qua tất cả các tổ hợp có thể có. - * - `index` là chỉ số hiện tại trong mảng `subjectKeys`. - * - `currentCombination` là tổ hợp hiện tại đang được xây dựng. - * - Nếu `index` bằng độ dài của `subjectKeys`, tổ hợp hiện tại đã hoàn thành và được thêm vào `combinations`. - * - `subjectKey` là khóa của môn học hiện tại. - * - `subjectData` là dữ liệu của môn học hiện tại. - * - `classKeys` là mảng chứa các khóa của các lớp học trong môn học hiện tại. - * - Với mỗi `classKey`, lớp học tương ứng được thêm vào `currentCombination` và hàm `backtrack` được gọi đệ quy với `index + 1`. - * - Sau khi gọi đệ quy, lớp học được loại bỏ khỏi `currentCombination` để thử các tổ hợp khác. */ export function generateCombinations( - selectedSubjects: CalendarGroupBySubjectName, - cache?: CombinationCache + selectedSubjects: CalendarGroupBySubjectName ): ClassCombination[] { const subjectKeys = Object.keys(selectedSubjects); const combinations: ClassCombination[] = []; + const currentCombination: ClassCombination = []; - function backtrack( - index: number, - currentCombination: ClassCombination - ): ClassCombination[] { - const subjectKey = subjectKeys[index]; + function backtrack(index: number) { + if (index === subjectKeys.length) { + combinations.push([...currentCombination]); + return; + } + const subjectKey = subjectKeys[index]; const subjectData = selectedSubjects[subjectKey]; - if (!subjectData) return []; - - const subjectCache: ClassCombination[] = []; - let remainSubjectsCache: ClassCombination[] = []; - let needToCalculateRemainSubjectsCache = true; - const remainSubjectKeys = subjectKeys.slice(index + 1); + if (!subjectData) return; - if (!remainSubjectKeys.length) needToCalculateRemainSubjectsCache = false; - else if (cache && cache[remainSubjectKeys.join('|')] != undefined) { - remainSubjectsCache = cache[remainSubjectKeys.join('|')]; - needToCalculateRemainSubjectsCache = false; - } - - Object.keys(subjectData.classes).forEach((classKey) => { + const classKeys = Object.keys(subjectData.classes); + for (const classKey of classKeys) { currentCombination.push(subjectData.classes[classKey]); - - if (index === subjectKeys.length - 1) { - const completedCombination = [...currentCombination]; - combinations.push(completedCombination); // clone currentCombination completed result to avoid .pop() - subjectCache.push(completedCombination.slice(index)); - } else { - if (needToCalculateRemainSubjectsCache) { - remainSubjectsCache = backtrack(index + 1, currentCombination); - needToCalculateRemainSubjectsCache = false; - } - - // merge remain subject cache with current subject class - for (const slicedClassCombination of remainSubjectsCache) { - subjectCache.push([ - subjectData.classes[classKey], - ...slicedClassCombination, - ]); - combinations.push([...currentCombination, ...slicedClassCombination]); // clone currentCombination completed result to avoid .pop() - } - } - + backtrack(index + 1); currentCombination.pop(); - }); - - // cache - if (cache && Object.keys(subjectCache).length) { - const combinedSubjectKeys = [subjectKey, ...remainSubjectKeys].join('|'); - if (!cache[combinedSubjectKeys]) cache[combinedSubjectKeys] = []; - cache[combinedSubjectKeys].push(...subjectCache); } - - return subjectCache; } - backtrack(0, []); - + backtrack(0); return combinations; } @@ -113,52 +61,79 @@ export function generateCombinations( * Kết quả cuối cùng là tổng số tiết học bị trùng lặp giữa tất cả các lớp học trong nhóm. */ export function calculateOverlap( - combination: CalendarGroupByClassDetail[] + combination: CalendarGroupByClassDetail[], + cache?: { + [key: string]: number; + } ): number { let overlap = 0; + let combinationCacheKey = combination + .map((cd) => [cd.majors[0], cd.subjectName, cd.subjectClassCode].join('-')) + .join('|'); + + if (cache && cache.hasOwnProperty(combinationCacheKey)) + return cache[combinationCacheKey]; + for (let i = 0; i < combination.length; i++) for (let j = i + 1; j < combination.length; j++) { const classDetail1 = combination[i]; const classDetail2 = combination[j]; - for (let session1 of classDetail1.details) { - for (let session2 of classDetail2.details) { - if ( - session1.startDate <= session2.endDate && - session1.endDate >= session2.startDate && - session1.startSession <= session2.endSession && - session1.endSession >= session2.startSession && - session1.dayOfWeek === session2.dayOfWeek - ) { - const conflictStartDate = Math.max( - session1.startDate, - session2.startDate - ); // Ngày bắt đầu trùng - const conflictEndDate = Math.min( - session1.endDate, - session2.endDate - ); // Ngày kết thúc trùng - - const conflictStartSession = Math.max( - session1.startSession, - session2.startSession - ); // Tiết bắt đầu trùng - const conflictEndSession = Math.min( - session1.endSession, - session2.endSession - ); // Tiết kết thúc trùng - - overlap += - countSpecificDayOfWeek( - conflictStartDate, - conflictEndDate, - session1.dayOfWeek - ) * - (conflictEndSession - conflictStartSession + 1); - } - } + + const pairCacheKey = [ + classDetail1.majors[0], + classDetail1.subjectName, + classDetail1.subjectClassCode, + classDetail2.majors[0], + classDetail2.subjectName, + classDetail2.subjectClassCode, + ].join('|'); + + if (cache && cache.hasOwnProperty(pairCacheKey)) { + overlap += cache[pairCacheKey]; + } else { + let classPairTotalOverlap = 0; + for (let session1 of classDetail1.details) + for (let session2 of classDetail2.details) + if ( + session1.startDate <= session2.endDate && + session1.endDate >= session2.startDate && + session1.startSession <= session2.endSession && + session1.endSession >= session2.startSession && + session1.dayOfWeek === session2.dayOfWeek + ) { + const conflictStartDate = Math.max( + session1.startDate, + session2.startDate + ); // Ngày bắt đầu trùng + const conflictEndDate = Math.min( + session1.endDate, + session2.endDate + ); // Ngày kết thúc trùng + + const conflictStartSession = Math.max( + session1.startSession, + session2.startSession + ); // Tiết bắt đầu trùng + const conflictEndSession = Math.min( + session1.endSession, + session2.endSession + ); // Tiết kết thúc trùng + + classPairTotalOverlap += + countSpecificDayOfWeek( + conflictStartDate, + conflictEndDate, + session1.dayOfWeek + ) * + (conflictEndSession - conflictStartSession + 1); + } + cache && (cache[pairCacheKey] = classPairTotalOverlap); + overlap += classPairTotalOverlap; } } + cache && (cache[combinationCacheKey] = overlap); + return overlap; } @@ -181,27 +156,58 @@ export function getOverlapRange( return null; } -export function calculateTotalSessionsInSessionRangeOfCombination( +export function calculateTotalSessionsInSessionRange( combination: CalendarGroupByClassDetail[], startShiftSession: number, - endShiftSession: number + endShiftSession: number, + cache?: { + [key: string]: number; + } ): number { - return combination.reduce((acc, classData) => { - for (const sessionData of classData.details) { - const overlapMorningSessionRange = getOverlapRange( - [sessionData.startSession, sessionData.endSession], - [startShiftSession, endShiftSession] - ); - acc += - (overlapMorningSessionRange - ? overlapMorningSessionRange[1] - overlapMorningSessionRange[0] + 1 - : 0) * - countSpecificDayOfWeek( - sessionData.startDate, - sessionData.endDate, - sessionData.dayOfWeek + const cacheKeyPrefix = `totalSessionsInSessionRange-${startShiftSession}-${endShiftSession}`; + const combinationCacheKey = [ + cacheKeyPrefix, + ...combination.map((cd) => + [cd.majors[0], cd.subjectName, cd.subjectClassCode].join('-') + ), + ].join('|'); + + if (cache && cache.hasOwnProperty(combinationCacheKey)) + return cache[combinationCacheKey]; + + const result = combination.reduce((acc, classData) => { + const cacheKey = [ + cacheKeyPrefix, + classData.majors[0], + classData.subjectName, + classData.subjectClassCode, + ].join('|'); + + if (cache && cache.hasOwnProperty(cacheKey)) acc += cache[cacheKey]; + else { + let localAcc = 0; + for (const sessionData of classData.details) { + const overlapRange = getOverlapRange( + [sessionData.startSession, sessionData.endSession], + [startShiftSession, endShiftSession] ); + localAcc += + (overlapRange ? overlapRange[1] - overlapRange[0] + 1 : 0) * + countSpecificDayOfWeek( + sessionData.startDate, + sessionData.endDate, + sessionData.dayOfWeek + ); + } + + cache && (cache[cacheKey] = localAcc); + acc += localAcc; } + return acc; }, 0); + + cache && (cache[combinationCacheKey] = result); + + return result; } diff --git a/src/workers/calendar.worker.ts b/src/workers/calendar.worker.ts index 9b9772b..b2c3e66 100644 --- a/src/workers/calendar.worker.ts +++ b/src/workers/calendar.worker.ts @@ -9,20 +9,23 @@ import { import { AutoMode, CalendarData, - CalendarGroupByClassDetail, CalendarGroupByMajor, CalendarGroupBySessionDetail, CalendarGroupBySubjectName, CalendarTableContent, - CombinationCache, } from '../types/calendar'; import { calculateOverlap, - calculateTotalSessionsInSessionRangeOfCombination, + calculateTotalSessionsInSessionRange, generateCombinations, } from '../utils/calendar_overlap'; -const conbinationCache: CombinationCache = {}; +const overlapCache: { + [key: string]: number; +} = {}; +const overlapSessionCache: { + [key: string]: number; +} = {}; function workerCalculateCalendarTableContent( calendarTableContent: CalendarTableContent, @@ -142,29 +145,36 @@ export function workerAutoCalculateCalendarTableContent( {} ); - const combinations = generateCombinations(selectedSubjects, conbinationCache); + const start = performance.now(); + const combinations = generateCombinations(selectedSubjects); + console.log('Generate combinations:', performance.now() - start); + + const start2 = performance.now(); const combinationsOrderByOverlap = combinations .map((combination) => ({ - overlap: calculateOverlap(combination), + overlap: calculateOverlap(combination, overlapCache), totalSessionsInSessionRangeOfCombination: ((combination): number => { switch (auto) { case 'refer-non-overlap-morning': - return calculateTotalSessionsInSessionRangeOfCombination( + return calculateTotalSessionsInSessionRange( combination, START_MORNING_SESSION, - END_MORNING_SESSION + END_MORNING_SESSION, + overlapSessionCache ); case 'refer-non-overlap-afternoon': - return calculateTotalSessionsInSessionRangeOfCombination( + return calculateTotalSessionsInSessionRange( combination, START_AFTERNOON_SESSION, - END_AFTERNOON_SESSION + END_AFTERNOON_SESSION, + overlapSessionCache ); case 'refer-non-overlap-evening': - return calculateTotalSessionsInSessionRangeOfCombination( + return calculateTotalSessionsInSessionRange( combination, START_EVENING_SESSION, - END_EVENING_SESSION + END_EVENING_SESSION, + overlapSessionCache ); } return 0; @@ -180,11 +190,13 @@ export function workerAutoCalculateCalendarTableContent( ); return diff; }); + console.log('Calculate overlap:', performance.now() - start2); const bestCombination = combinationsOrderByOverlap.length ? combinationsOrderByOverlap[autoTh % combinationsOrderByOverlap.length] : undefined; + const start3 = performance.now(); const updatedCalendarGroupByMajor = bestCombination ? (() => { const clonedCalendarGroupByMajor = @@ -206,6 +218,7 @@ export function workerAutoCalculateCalendarTableContent( sessions ).updatedCalendarTableContent : calendarTableContent; + console.log('Update calendar:', performance.now() - start3); return { updatedCalendarTableContent,