Optimize performance: cache dates, reduce lookups, improve algorithms
Co-authored-by: THoguet <61246905+THoguet@users.noreply.github.com>
This commit is contained in:
1 parent
d3caf86028
commit
601e7855aa
3 files changed
+102
-50
No files matched your search
@@ -18,10 +18,15 @@ export class CalendarMonth implements OnInit {
|
|||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
year!: number;
|
year!: number;
|
||||||
|
|
||||||
|
// Cache "now" as a class property to avoid recreating it on every call
|
||||||
|
private readonly now: Date = (() => {
|
||||||
|
const date = new Date();
|
||||||
|
date.setHours(0, 0, 0, 0);
|
||||||
|
return date;
|
||||||
|
})();
|
||||||
|
|
||||||
getNow(): Date {
|
getNow(): Date {
|
||||||
const now = new Date();
|
return this.now;
|
||||||
now.setHours(0, 0, 0, 0);
|
|
||||||
return now;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
currentMonth: Date = new Date();
|
currentMonth: Date = new Date();
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { Calendar } from './calendar';
|
import { CalendarService } from './calendar-service';
|
||||||
|
|
||||||
describe('Calendar', () => {
|
describe('CalendarService', () => {
|
||||||
let service: Calendar;
|
let service: CalendarService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({});
|
TestBed.configureTestingModule({});
|
||||||
service = TestBed.inject(Calendar);
|
service = TestBed.inject(CalendarService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ const allTypesOptions: HolidaysTypes.Options = {
|
|||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class CalendarService {
|
export class CalendarService {
|
||||||
|
// Cache for holiday timestamps
|
||||||
|
private holidayTimestampsCache: Set<number> | null = null;
|
||||||
|
private holidayCacheYear: number | null = null;
|
||||||
|
|
||||||
// Get selected dates from current date to the next year
|
// Get selected dates from current date to the next year
|
||||||
getSelectedDatesFromNow(): SelectedDates {
|
getSelectedDatesFromNow(): SelectedDates {
|
||||||
const holidays = this.getHolidays();
|
const holidays = this.getHolidays();
|
||||||
@@ -31,20 +35,30 @@ export class CalendarService {
|
|||||||
const holidays = this.getHolidays();
|
const holidays = this.getHolidays();
|
||||||
const monthInAYear = CalendarService.monthsInAYearFromNow();
|
const monthInAYear = CalendarService.monthsInAYearFromNow();
|
||||||
let selectedDates: SelectedDates = new SelectedDates();
|
let selectedDates: SelectedDates = new SelectedDates();
|
||||||
|
|
||||||
|
// Cache today's date components to avoid repeated new Date() calls
|
||||||
|
const today = new Date();
|
||||||
|
const todayDate = today.getDate();
|
||||||
|
const todayMonth = today.getMonth();
|
||||||
|
const todayYear = today.getFullYear();
|
||||||
|
|
||||||
|
// Create a Set of holiday timestamps for O(1) lookup instead of O(n)
|
||||||
|
const holidayTimestamps = new Set(holidays.map((h) => h.getTime()));
|
||||||
|
|
||||||
for (const weeksInAMonth of monthInAYear) {
|
for (const weeksInAMonth of monthInAYear) {
|
||||||
for (const daysInAWeek of weeksInAMonth) {
|
for (const daysInAWeek of weeksInAMonth) {
|
||||||
for (const day of daysInAWeek) {
|
for (const day of daysInAWeek) {
|
||||||
if (day) {
|
if (day) {
|
||||||
this.setWeekendSelection(day, selectedDates);
|
this.setWeekendSelection(day, selectedDates);
|
||||||
if (holidays.some((holiday) => holiday.getTime() === day.getTime())) {
|
if (holidayTimestamps.has(day.getTime())) {
|
||||||
selectedDates._datesSelected.push(
|
selectedDates._datesSelected.push(
|
||||||
new SelectedDate(DayType.CLOSED_DAY, new DateRange<Date>(day, day)),
|
new SelectedDate(DayType.CLOSED_DAY, new DateRange<Date>(day, day)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
day.getDate() === new Date().getDate() &&
|
day.getDate() === todayDate &&
|
||||||
day.getMonth() === new Date().getMonth() &&
|
day.getMonth() === todayMonth &&
|
||||||
day.getFullYear() === new Date().getFullYear()
|
day.getFullYear() === todayYear
|
||||||
) {
|
) {
|
||||||
selectedDates._datesSelected.push(
|
selectedDates._datesSelected.push(
|
||||||
new SelectedDate(DayType.TODAY, new DateRange<Date>(day, day)),
|
new SelectedDate(DayType.TODAY, new DateRange<Date>(day, day)),
|
||||||
@@ -84,12 +98,23 @@ export class CalendarService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getHolidays(): Date[] {
|
getHolidays(): Date[] {
|
||||||
|
// Cache holidays to avoid recalculation
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
if (this.holidayCacheYear === currentYear && this.holidayTimestampsCache) {
|
||||||
|
return Array.from(this.holidayTimestampsCache).map((ts) => new Date(ts));
|
||||||
|
}
|
||||||
|
|
||||||
// Logic to return holidays for the given year
|
// Logic to return holidays for the given year
|
||||||
const hd = new Holidays('FR', allTypesOptions);
|
const hd = new Holidays('FR', allTypesOptions);
|
||||||
const holidays = hd.getHolidays(new Date().getFullYear(), lang);
|
const holidays = hd.getHolidays(currentYear, lang);
|
||||||
const holidaysPlusOne = hd.getHolidays(new Date().getFullYear() + 1, lang);
|
const holidaysPlusOne = hd.getHolidays(currentYear + 1, lang);
|
||||||
holidays.push(...holidaysPlusOne);
|
holidays.push(...holidaysPlusOne);
|
||||||
let holidaysDate: Date[] = holidays.map((holiday) => new Date(holiday.date));
|
let holidaysDate: Date[] = holidays.map((holiday) => new Date(holiday.date));
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
this.holidayTimestampsCache = new Set(holidaysDate.map((d) => d.getTime()));
|
||||||
|
this.holidayCacheYear = currentYear;
|
||||||
|
|
||||||
return holidaysDate;
|
return holidaysDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,8 +558,9 @@ export class SelectedDates implements SelectedDateInterface {
|
|||||||
samediMalin(vacationsNumber: VacationsNumber): void {
|
samediMalin(vacationsNumber: VacationsNumber): void {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
now.setHours(0, 0, 0, 0);
|
now.setHours(0, 0, 0, 0);
|
||||||
|
const nowTime = now.getTime();
|
||||||
|
|
||||||
const samedi_ferie = this.datesSelected.filter(
|
const samedi_ferie = this._datesSelected.filter(
|
||||||
(date) => date.type === DayType.CLOSED_DAY && date.range.start?.getDay() === 6,
|
(date) => date.type === DayType.CLOSED_DAY && date.range.start?.getDay() === 6,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -572,17 +598,17 @@ export class SelectedDates implements SelectedDateInterface {
|
|||||||
|
|
||||||
// Check if we can apply each strategy (and not in the past)
|
// Check if we can apply each strategy (and not in the past)
|
||||||
const canApplyTwoBeforeStrategy =
|
const canApplyTwoBeforeStrategy =
|
||||||
twoDaysBefore1 >= now &&
|
twoDaysBefore1.getTime() >= nowTime &&
|
||||||
!this.isSelected(twoDaysBefore1) &&
|
!this.isSelected(twoDaysBefore1) &&
|
||||||
!this.isSelected(twoDaysBefore2);
|
!this.isSelected(twoDaysBefore2);
|
||||||
|
|
||||||
const canApplyTwoAfterStrategy =
|
const canApplyTwoAfterStrategy =
|
||||||
twoDaysAfter1 >= now &&
|
twoDaysAfter1.getTime() >= nowTime &&
|
||||||
!this.isSelected(twoDaysAfter1) &&
|
!this.isSelected(twoDaysAfter1) &&
|
||||||
!this.isSelected(twoDaysAfter2);
|
!this.isSelected(twoDaysAfter2);
|
||||||
|
|
||||||
const canApplyMixedStrategy =
|
const canApplyMixedStrategy =
|
||||||
oneDayBefore >= now &&
|
oneDayBefore.getTime() >= nowTime &&
|
||||||
!this.isSelected(oneDayBefore) &&
|
!this.isSelected(oneDayBefore) &&
|
||||||
!this.isSelected(oneDayAfter);
|
!this.isSelected(oneDayAfter);
|
||||||
|
|
||||||
@@ -597,6 +623,7 @@ export class SelectedDates implements SelectedDateInterface {
|
|||||||
new DateRange<Date>(twoDaysBefore1, twoDaysBefore2),
|
new DateRange<Date>(twoDaysBefore1, twoDaysBefore2),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
const heuristic = this.computeHeuristics();
|
||||||
strategies.push({
|
strategies.push({
|
||||||
apply: () => {
|
apply: () => {
|
||||||
this._datesSelected = [...tempDatesBackup];
|
this._datesSelected = [...tempDatesBackup];
|
||||||
@@ -609,7 +636,7 @@ export class SelectedDates implements SelectedDateInterface {
|
|||||||
vacationsNumber.cp -= 2;
|
vacationsNumber.cp -= 2;
|
||||||
vacationsNumber.other += 1; // Reward: gain 1 other day
|
vacationsNumber.other += 1; // Reward: gain 1 other day
|
||||||
},
|
},
|
||||||
heuristic: this.computeHeuristics(),
|
heuristic: heuristic,
|
||||||
});
|
});
|
||||||
this._datesSelected = [...tempDatesBackup];
|
this._datesSelected = [...tempDatesBackup];
|
||||||
this.datesSelected = this._datesSelected.slice();
|
this.datesSelected = this._datesSelected.slice();
|
||||||
@@ -620,6 +647,7 @@ export class SelectedDates implements SelectedDateInterface {
|
|||||||
this.push(
|
this.push(
|
||||||
new SelectedDate(DayType.CP, new DateRange<Date>(twoDaysAfter1, twoDaysAfter2)),
|
new SelectedDate(DayType.CP, new DateRange<Date>(twoDaysAfter1, twoDaysAfter2)),
|
||||||
);
|
);
|
||||||
|
const heuristic = this.computeHeuristics();
|
||||||
strategies.push({
|
strategies.push({
|
||||||
apply: () => {
|
apply: () => {
|
||||||
this._datesSelected = [...tempDatesBackup];
|
this._datesSelected = [...tempDatesBackup];
|
||||||
@@ -632,7 +660,7 @@ export class SelectedDates implements SelectedDateInterface {
|
|||||||
vacationsNumber.cp -= 2;
|
vacationsNumber.cp -= 2;
|
||||||
vacationsNumber.other += 1; // Reward: gain 1 other day
|
vacationsNumber.other += 1; // Reward: gain 1 other day
|
||||||
},
|
},
|
||||||
heuristic: this.computeHeuristics(),
|
heuristic: heuristic,
|
||||||
});
|
});
|
||||||
this._datesSelected = [...tempDatesBackup];
|
this._datesSelected = [...tempDatesBackup];
|
||||||
this.datesSelected = this._datesSelected.slice();
|
this.datesSelected = this._datesSelected.slice();
|
||||||
@@ -646,6 +674,7 @@ export class SelectedDates implements SelectedDateInterface {
|
|||||||
this.push(
|
this.push(
|
||||||
new SelectedDate(DayType.CP, new DateRange<Date>(oneDayAfter, oneDayAfter)),
|
new SelectedDate(DayType.CP, new DateRange<Date>(oneDayAfter, oneDayAfter)),
|
||||||
);
|
);
|
||||||
|
const heuristic = this.computeHeuristics();
|
||||||
strategies.push({
|
strategies.push({
|
||||||
apply: () => {
|
apply: () => {
|
||||||
this._datesSelected = [...tempDatesBackup];
|
this._datesSelected = [...tempDatesBackup];
|
||||||
@@ -664,7 +693,7 @@ export class SelectedDates implements SelectedDateInterface {
|
|||||||
vacationsNumber.cp -= 2;
|
vacationsNumber.cp -= 2;
|
||||||
vacationsNumber.other += 1; // Reward: gain 1 other day
|
vacationsNumber.other += 1; // Reward: gain 1 other day
|
||||||
},
|
},
|
||||||
heuristic: this.computeHeuristics(),
|
heuristic: heuristic,
|
||||||
});
|
});
|
||||||
this._datesSelected = [...tempDatesBackup];
|
this._datesSelected = [...tempDatesBackup];
|
||||||
this.datesSelected = this._datesSelected.slice();
|
this.datesSelected = this._datesSelected.slice();
|
||||||
@@ -685,61 +714,79 @@ export class SelectedDates implements SelectedDateInterface {
|
|||||||
const daysOfYear = CalendarService.monthsInAYearFromNow();
|
const daysOfYear = CalendarService.monthsInAYearFromNow();
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
now.setHours(0, 0, 0, 0);
|
now.setHours(0, 0, 0, 0);
|
||||||
daysOfYear.forEach((month) => {
|
const nowTime = now.getTime();
|
||||||
month.forEach((week) => {
|
|
||||||
week.forEach((day) => {
|
// Preprocess: filter out past days and already selected days in one pass
|
||||||
if (day) {
|
const candidateDays: Date[] = [];
|
||||||
if (day) day.setHours(0, 0, 0, 0);
|
|
||||||
if (day && day < now) {
|
|
||||||
week[week.indexOf(day)] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const tempDatesBackup = [...this._datesSelected];
|
|
||||||
let strategies: { apply: () => void; heuristic: number; value: Date }[] = [];
|
|
||||||
for (const month of daysOfYear) {
|
for (const month of daysOfYear) {
|
||||||
for (const week of month) {
|
for (const week of month) {
|
||||||
for (const day of week) {
|
for (const day of week) {
|
||||||
if (day && !this.isSelected(day)) {
|
if (day) {
|
||||||
|
day.setHours(0, 0, 0, 0);
|
||||||
|
if (day.getTime() >= nowTime && !this.isSelected(day)) {
|
||||||
|
candidateDays.push(day);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Early exit if no candidates
|
||||||
|
if (candidateDays.length === 0) {
|
||||||
|
return { apply: () => {}, heuristic: -1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempDatesBackup = [...this._datesSelected];
|
||||||
|
let bestStrategy: { apply: () => void; heuristic: number; value: Date } | null = null;
|
||||||
|
let bestHeuristic = -Infinity;
|
||||||
|
|
||||||
|
// Evaluate each candidate day
|
||||||
|
for (const day of candidateDays) {
|
||||||
|
// Reset to backup state
|
||||||
this._datesSelected = [...tempDatesBackup];
|
this._datesSelected = [...tempDatesBackup];
|
||||||
this.datesSelected = this._datesSelected.slice();
|
this.datesSelected = this._datesSelected.slice();
|
||||||
this.grouping();
|
this.grouping();
|
||||||
// mark these days as vacation
|
|
||||||
|
// Mark this day as vacation
|
||||||
const startDay = new Date(day);
|
const startDay = new Date(day);
|
||||||
const endDay = new Date(day);
|
const endDay = new Date(day);
|
||||||
this.datesSelected.push(
|
this.datesSelected.push(
|
||||||
new SelectedDate(vacType, new DateRange<Date>(startDay, endDay)),
|
new SelectedDate(vacType, new DateRange<Date>(startDay, endDay)),
|
||||||
);
|
);
|
||||||
strategies.push({
|
|
||||||
|
const heuristic = this.computeHeuristics();
|
||||||
|
|
||||||
|
// Track only if better than current best
|
||||||
|
if (heuristic > bestHeuristic) {
|
||||||
|
bestHeuristic = heuristic;
|
||||||
|
const capturedDay = startDay;
|
||||||
|
bestStrategy = {
|
||||||
apply: () => {
|
apply: () => {
|
||||||
this._datesSelected = [...tempDatesBackup];
|
this._datesSelected = [...tempDatesBackup];
|
||||||
this.push(
|
this.push(
|
||||||
new SelectedDate(
|
new SelectedDate(
|
||||||
vacType,
|
vacType,
|
||||||
new DateRange<Date>(startDay, endDay),
|
new DateRange<Date>(capturedDay, capturedDay),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
heuristic: this.computeHeuristics(),
|
heuristic: heuristic,
|
||||||
value: startDay,
|
value: capturedDay,
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore backup before returning
|
||||||
this._datesSelected = [...tempDatesBackup];
|
this._datesSelected = [...tempDatesBackup];
|
||||||
this.datesSelected = this._datesSelected.slice();
|
this.datesSelected = this._datesSelected.slice();
|
||||||
this.grouping();
|
this.grouping();
|
||||||
}
|
|
||||||
}
|
// Apply best strategy if found
|
||||||
}
|
if (bestStrategy) {
|
||||||
}
|
|
||||||
// Apply the strategy with the best heuristic score
|
|
||||||
if (strategies.length > 0) {
|
|
||||||
const bestStrategy = strategies.reduce((best, current) =>
|
|
||||||
current.heuristic > best.heuristic ? current : best,
|
|
||||||
);
|
|
||||||
bestStrategy.apply();
|
bestStrategy.apply();
|
||||||
return bestStrategy;
|
return bestStrategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { apply: () => {}, heuristic: -1 };
|
return { apply: () => {}, heuristic: -1 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user