diff --git a/src/app/components/calendar-month/calendar-month.ts b/src/app/components/calendar-month/calendar-month.ts index 72de621..761cdcd 100644 --- a/src/app/components/calendar-month/calendar-month.ts +++ b/src/app/components/calendar-month/calendar-month.ts @@ -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 { MatTooltip } from '@angular/material/tooltip'; import { CalendarSettingsService } from '../../services/calendar-settings-service'; +import { DateCacheService } from '../../services/date-cache.service'; @Component({ selector: 'app-calendar-month', @@ -10,7 +11,8 @@ import { CalendarSettingsService } from '../../services/calendar-settings-servic styleUrl: './calendar-month.scss', }) export class CalendarMonth implements OnInit { - constructor(protected calendarSettingsService: CalendarSettingsService) {} + protected calendarSettingsService = inject(CalendarSettingsService); + private dateCache = inject(DateCacheService); @Input({ required: true }) monthIndex!: number; @@ -18,18 +20,9 @@ export class CalendarMonth implements OnInit { @Input({ required: true }) year!: number; - // Cache "now" as a class property to avoid recreating it on every call - // 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; - })(); - + // Use DateCacheService instead of creating Date objects getNow(): Date { - return this.now; + return this.dateCache.getNow(); } currentMonth: Date = new Date(); diff --git a/src/app/components/calendar-year/calendar-year.ts b/src/app/components/calendar-year/calendar-year.ts index 262c677..f0223b7 100644 --- a/src/app/components/calendar-year/calendar-year.ts +++ b/src/app/components/calendar-year/calendar-year.ts @@ -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 { MatCard, MatCardContent } from '@angular/material/card'; import { CalendarMonth } from '../calendar-month/calendar-month'; @@ -6,6 +6,7 @@ import { CalendarService, SelectedDates } from '../../services/calendar-service' import { CommonModule } from '@angular/common'; import { MatProgressSpinner } from '@angular/material/progress-spinner'; import { CalendarSettingsService } from '../../services/calendar-settings-service'; +import { DateCacheService } from '../../services/date-cache.service'; @Component({ selector: 'app-calendar-year', @@ -14,17 +15,23 @@ import { CalendarSettingsService } from '../../services/calendar-settings-servic styleUrl: './calendar-year.scss', }) export class CalendarYear { + private dateCache = inject(DateCacheService); + private userInput = inject(UserInputService); + private calendarService = inject(CalendarService); + private calendarSettingsService = inject(CalendarSettingsService); + protected isLoading = signal(true); - // Cache current month and year to avoid repeated Date object creation - private readonly currentMonth = new Date().getMonth(); - private readonly currentYear = new Date().getFullYear(); + // Use DateCacheService instead of creating Date objects + private get currentMonth() { + return this.dateCache.getCurrentMonth(); + } - constructor( - private userInput: UserInputService, - private calendarService: CalendarService, - private calendarSettingsService: CalendarSettingsService, - ) { + private get currentYear() { + return this.dateCache.getCurrentYear(); + } + + constructor() { effect(() => { this.selectedDates = this.calendarService.getSelectedDatesForYear(); const vacationData = this.userInput.vacationNumberSignal(); diff --git a/src/app/services/calendar-service.ts b/src/app/services/calendar-service.ts index 44cb2f4..ecbe90a 100644 --- a/src/app/services/calendar-service.ts +++ b/src/app/services/calendar-service.ts @@ -1,10 +1,11 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { DateRange } from '@angular/material/datepicker'; import Holidays, { HolidaysTypes } from 'date-holidays'; import dayjs from 'dayjs'; import weekOfYear from 'dayjs/plugin/weekOfYear'; import isoWeek from 'dayjs/plugin/isoWeek'; import { CalendarSettingsService } from './calendar-settings-service'; +import { DateCacheService } from './date-cache.service'; dayjs.extend(weekOfYear); dayjs.extend(isoWeek); @@ -19,6 +20,8 @@ const allTypesOptions: HolidaysTypes.Options = { providedIn: 'root', }) export class CalendarService { + private dateCache = inject(DateCacheService); + // Cache for holiday timestamps private holidayTimestampsCache: Set | null = null; private holidayCacheYear: number | null = null; @@ -36,11 +39,12 @@ export class CalendarService { const monthInAYear = CalendarService.monthsInAYearFromNow(); 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(); + // Use DateCacheService to get today's date components + const { + date: todayDate, + month: todayMonth, + year: todayYear, + } = this.dateCache.getTodayComponents(); // Create a Set of holiday timestamps for O(1) lookup instead of O(n) const holidayTimestamps = new Set(holidays.map((h) => h.getTime())); @@ -99,7 +103,7 @@ export class CalendarService { getHolidays(): Date[] { // Cache holidays to avoid recalculation - const currentYear = new Date().getFullYear(); + const currentYear = this.dateCache.getCurrentYear(); if (this.holidayCacheYear === currentYear && this.holidayTimestampsCache) { // Return timestamps directly for efficient lookup return Array.from(this.holidayTimestampsCache).map((ts) => new Date(ts)); @@ -164,9 +168,10 @@ export class CalendarService { } static monthsInAYearFromNow(): Year { + const dateCache = inject(DateCacheService); const months: Year = []; - const currentMonthIndex = new Date().getMonth(); - const currentYear = new Date().getFullYear(); + const currentMonthIndex = dateCache.getCurrentMonth(); + const currentYear = dateCache.getCurrentYear(); for (let i = 0; i < 12; i++) { const monthIndex = (currentMonthIndex + i) % 12; const year = currentYear + Math.floor((currentMonthIndex + i) / 12); diff --git a/src/app/services/date-cache.service.spec.ts b/src/app/services/date-cache.service.spec.ts new file mode 100644 index 0000000..c51d794 --- /dev/null +++ b/src/app/services/date-cache.service.spec.ts @@ -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 + }); +}); diff --git a/src/app/services/date-cache.service.ts b/src/app/services/date-cache.service.ts new file mode 100644 index 0000000..8edf3f2 --- /dev/null +++ b/src/app/services/date-cache.service.ts @@ -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; + } +}