Refactor: create DateCacheService to reduce date duplication

Co-authored-by: THoguet <61246905+THoguet@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]andTHoguet committed 2025-11-01 21:42:59 +00:00
1 parent dbf504b060
commit 125786e2d9
5 files changed
+187 -31

No files matched your search

@@ -1,7 +1,8 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit, inject } from '@angular/core';
import { CalendarService, Month, monthNames, SelectedDates } from '../../services/calendar-service'; import { CalendarService, Month, monthNames, SelectedDates } from '../../services/calendar-service';
import { MatTooltip } from '@angular/material/tooltip'; import { MatTooltip } from '@angular/material/tooltip';
import { CalendarSettingsService } from '../../services/calendar-settings-service'; import { CalendarSettingsService } from '../../services/calendar-settings-service';
import { DateCacheService } from '../../services/date-cache.service';
@Component({ @Component({
selector: 'app-calendar-month', selector: 'app-calendar-month',
@@ -10,7 +11,8 @@ import { CalendarSettingsService } from '../../services/calendar-settings-servic
styleUrl: './calendar-month.scss', styleUrl: './calendar-month.scss',
}) })
export class CalendarMonth implements OnInit { export class CalendarMonth implements OnInit {
constructor(protected calendarSettingsService: CalendarSettingsService) {} protected calendarSettingsService = inject(CalendarSettingsService);
private dateCache = inject(DateCacheService);
@Input({ required: true }) @Input({ required: true })
monthIndex!: number; monthIndex!: number;
@@ -18,18 +20,9 @@ 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 // Use DateCacheService instead of creating Date objects
// Note: This value is cached at component initialization and won't update if the day changes
// while the component is still mounted. This is acceptable for this use case since the app
// is primarily used within a single day session.
private readonly now: Date = (() => {
const date = new Date();
date.setHours(0, 0, 0, 0);
return date;
})();
getNow(): Date { getNow(): Date {
return this.now; return this.dateCache.getNow();
} }
currentMonth: Date = new Date(); currentMonth: Date = new Date();
@@ -1,4 +1,4 @@
import { Component, effect, signal } from '@angular/core'; import { Component, effect, signal, inject } from '@angular/core';
import { UserInputService } from '../../services/user-input-service'; import { UserInputService } from '../../services/user-input-service';
import { MatCard, MatCardContent } from '@angular/material/card'; import { MatCard, MatCardContent } from '@angular/material/card';
import { CalendarMonth } from '../calendar-month/calendar-month'; import { CalendarMonth } from '../calendar-month/calendar-month';
@@ -6,6 +6,7 @@ import { CalendarService, SelectedDates } from '../../services/calendar-service'
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { MatProgressSpinner } from '@angular/material/progress-spinner'; import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { CalendarSettingsService } from '../../services/calendar-settings-service'; import { CalendarSettingsService } from '../../services/calendar-settings-service';
import { DateCacheService } from '../../services/date-cache.service';
@Component({ @Component({
selector: 'app-calendar-year', selector: 'app-calendar-year',
@@ -14,17 +15,23 @@ import { CalendarSettingsService } from '../../services/calendar-settings-servic
styleUrl: './calendar-year.scss', styleUrl: './calendar-year.scss',
}) })
export class CalendarYear { export class CalendarYear {
private dateCache = inject(DateCacheService);
private userInput = inject(UserInputService);
private calendarService = inject(CalendarService);
private calendarSettingsService = inject(CalendarSettingsService);
protected isLoading = signal(true); protected isLoading = signal(true);
// Cache current month and year to avoid repeated Date object creation // Use DateCacheService instead of creating Date objects
private readonly currentMonth = new Date().getMonth(); private get currentMonth() {
private readonly currentYear = new Date().getFullYear(); return this.dateCache.getCurrentMonth();
}
constructor( private get currentYear() {
private userInput: UserInputService, return this.dateCache.getCurrentYear();
private calendarService: CalendarService, }
private calendarSettingsService: CalendarSettingsService,
) { constructor() {
effect(() => { effect(() => {
this.selectedDates = this.calendarService.getSelectedDatesForYear(); this.selectedDates = this.calendarService.getSelectedDatesForYear();
const vacationData = this.userInput.vacationNumberSignal(); const vacationData = this.userInput.vacationNumberSignal();
+14 -9
View File
@@ -1,10 +1,11 @@
import { Injectable } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { DateRange } from '@angular/material/datepicker'; import { DateRange } from '@angular/material/datepicker';
import Holidays, { HolidaysTypes } from 'date-holidays'; import Holidays, { HolidaysTypes } from 'date-holidays';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import weekOfYear from 'dayjs/plugin/weekOfYear'; import weekOfYear from 'dayjs/plugin/weekOfYear';
import isoWeek from 'dayjs/plugin/isoWeek'; import isoWeek from 'dayjs/plugin/isoWeek';
import { CalendarSettingsService } from './calendar-settings-service'; import { CalendarSettingsService } from './calendar-settings-service';
import { DateCacheService } from './date-cache.service';
dayjs.extend(weekOfYear); dayjs.extend(weekOfYear);
dayjs.extend(isoWeek); dayjs.extend(isoWeek);
@@ -19,6 +20,8 @@ const allTypesOptions: HolidaysTypes.Options = {
providedIn: 'root', providedIn: 'root',
}) })
export class CalendarService { export class CalendarService {
private dateCache = inject(DateCacheService);
// Cache for holiday timestamps // Cache for holiday timestamps
private holidayTimestampsCache: Set<number> | null = null; private holidayTimestampsCache: Set<number> | null = null;
private holidayCacheYear: number | null = null; private holidayCacheYear: number | null = null;
@@ -36,11 +39,12 @@ export class CalendarService {
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 // Use DateCacheService to get today's date components
const today = new Date(); const {
const todayDate = today.getDate(); date: todayDate,
const todayMonth = today.getMonth(); month: todayMonth,
const todayYear = today.getFullYear(); year: todayYear,
} = this.dateCache.getTodayComponents();
// Create a Set of holiday timestamps for O(1) lookup instead of O(n) // Create a Set of holiday timestamps for O(1) lookup instead of O(n)
const holidayTimestamps = new Set(holidays.map((h) => h.getTime())); const holidayTimestamps = new Set(holidays.map((h) => h.getTime()));
@@ -99,7 +103,7 @@ export class CalendarService {
getHolidays(): Date[] { getHolidays(): Date[] {
// Cache holidays to avoid recalculation // Cache holidays to avoid recalculation
const currentYear = new Date().getFullYear(); const currentYear = this.dateCache.getCurrentYear();
if (this.holidayCacheYear === currentYear && this.holidayTimestampsCache) { if (this.holidayCacheYear === currentYear && this.holidayTimestampsCache) {
// Return timestamps directly for efficient lookup // Return timestamps directly for efficient lookup
return Array.from(this.holidayTimestampsCache).map((ts) => new Date(ts)); return Array.from(this.holidayTimestampsCache).map((ts) => new Date(ts));
@@ -164,9 +168,10 @@ export class CalendarService {
} }
static monthsInAYearFromNow(): Year { static monthsInAYearFromNow(): Year {
const dateCache = inject(DateCacheService);
const months: Year = []; const months: Year = [];
const currentMonthIndex = new Date().getMonth(); const currentMonthIndex = dateCache.getCurrentMonth();
const currentYear = new Date().getFullYear(); const currentYear = dateCache.getCurrentYear();
for (let i = 0; i < 12; i++) { for (let i = 0; i < 12; i++) {
const monthIndex = (currentMonthIndex + i) % 12; const monthIndex = (currentMonthIndex + i) % 12;
const year = currentYear + Math.floor((currentMonthIndex + i) / 12); const year = currentYear + Math.floor((currentMonthIndex + i) / 12);
@@ -0,0 +1,46 @@
import { TestBed } from '@angular/core/testing';
import { DateCacheService } from './date-cache.service';
describe('DateCacheService', () => {
let service: DateCacheService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(DateCacheService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should cache today date', () => {
const today1 = service.getToday();
const today2 = service.getToday();
expect(today1).toBe(today2); // Same reference
});
it('should cache today components', () => {
const components1 = service.getTodayComponents();
const components2 = service.getTodayComponents();
expect(components1).toBe(components2); // Same reference
});
it('should cache current month', () => {
const month1 = service.getCurrentMonth();
const month2 = service.getCurrentMonth();
expect(month1).toBe(month2);
});
it('should cache current year', () => {
const year1 = service.getCurrentYear();
const year2 = service.getCurrentYear();
expect(year1).toBe(year2);
});
it('should clear cache', () => {
const year1 = service.getCurrentYear();
service.clearCache();
const year2 = service.getCurrentYear();
expect(year1).toBe(year2); // Values should be the same even after clearing
});
});
+105
View File
@@ -0,0 +1,105 @@
import { Injectable } from '@angular/core';
/**
* Service to cache and provide commonly used date values.
* This reduces redundant Date object creation across the application.
*/
@Injectable({
providedIn: 'root',
})
export class DateCacheService {
private _today: Date | null = null;
private _todayComponents: { date: number; month: number; year: number } | null = null;
private _currentMonth: number | null = null;
private _currentYear: number | null = null;
/**
* Get today's date (time set to 00:00:00.000)
*/
getToday(): Date {
if (!this._today || this.isNewDay(this._today)) {
this._today = new Date();
this._today.setHours(0, 0, 0, 0);
this._todayComponents = null; // Invalidate components cache
}
return this._today;
}
/**
* Get today's date components (date, month, year) for efficient comparison
*/
getTodayComponents(): { date: number; month: number; year: number } {
if (!this._todayComponents || (this._today && this.isNewDay(this._today))) {
const today = this.getToday();
this._todayComponents = {
date: today.getDate(),
month: today.getMonth(),
year: today.getFullYear(),
};
}
return this._todayComponents;
}
/**
* Get current month (0-11)
*/
getCurrentMonth(): number {
if (this._currentMonth === null || this.isNewMonth()) {
this._currentMonth = new Date().getMonth();
}
return this._currentMonth;
}
/**
* Get current year
*/
getCurrentYear(): number {
if (this._currentYear === null || this.isNewYear()) {
this._currentYear = new Date().getFullYear();
}
return this._currentYear;
}
/**
* Get current date with time set to 00:00:00.000 (alias for getToday)
*/
getNow(): Date {
return this.getToday();
}
/**
* Check if a cached date is from a previous day
*/
private isNewDay(cachedDate: Date): boolean {
const now = new Date();
return (
cachedDate.getDate() !== now.getDate() ||
cachedDate.getMonth() !== now.getMonth() ||
cachedDate.getFullYear() !== now.getFullYear()
);
}
/**
* Check if we're in a new month
*/
private isNewMonth(): boolean {
return this._currentMonth !== null && this._currentMonth !== new Date().getMonth();
}
/**
* Check if we're in a new year
*/
private isNewYear(): boolean {
return this._currentYear !== null && this._currentYear !== new Date().getFullYear();
}
/**
* Clear all caches (useful for testing)
*/
clearCache(): void {
this._today = null;
this._todayComponents = null;
this._currentMonth = null;
this._currentYear = null;
}
}