Refactor: create DateCacheService to reduce date duplication
Co-authored-by: THoguet <61246905+THoguet@users.noreply.github.com>
This commit is contained in:
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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user