/** * DateCalculator - Centralized date calculation logic for calendar * Handles all date computations with proper week start handling */ import { CalendarConfig } from '../core/CalendarConfig'; export class DateCalculator { private static config: CalendarConfig; /** * Initialize DateCalculator with configuration * @param config - Calendar configuration */ static initialize(config: CalendarConfig): void { DateCalculator.config = config; } /** * Validate that a date is valid * @param date - Date to validate * @param methodName - Name of calling method for error messages * @throws Error if date is invalid */ private static validateDate(date: Date, methodName: string): void { if (!date || !(date instanceof Date) || isNaN(date.getTime())) { throw new Error(`${methodName}: Invalid date provided - ${date}`); } } /** * Get dates for work week using ISO 8601 day numbering (Monday=1, Sunday=7) * @param weekStart - Any date in the week * @returns Array of dates for the configured work days */ static getWorkWeekDates(weekStart: Date): Date[] { DateCalculator.validateDate(weekStart, 'getWorkWeekDates'); const dates: Date[] = []; const workWeekSettings = DateCalculator.config.getWorkWeekSettings(); // Always use ISO week start (Monday) const mondayOfWeek = DateCalculator.getISOWeekStart(weekStart); // Calculate dates for each work day using ISO numbering workWeekSettings.workDays.forEach(isoDay => { const date = new Date(mondayOfWeek); // ISO day 1=Monday is +0 days, ISO day 7=Sunday is +6 days const daysFromMonday = isoDay === 7 ? 6 : isoDay - 1; date.setDate(mondayOfWeek.getDate() + daysFromMonday); dates.push(date); }); return dates; } /** * Get the start of the ISO week (Monday) for a given date * @param date - Any date in the week * @returns The Monday of the ISO week */ static getISOWeekStart(date: Date): Date { DateCalculator.validateDate(date, 'getISOWeekStart'); const monday = new Date(date); const currentDay = monday.getDay(); const daysToSubtract = currentDay === 0 ? 6 : currentDay - 1; monday.setDate(monday.getDate() - daysToSubtract); monday.setHours(0, 0, 0, 0); return monday; } /** * Get the end of the ISO week for a given date * @param date - Any date in the week * @returns The end date of the ISO week (Sunday) */ static getWeekEnd(date: Date): Date { DateCalculator.validateDate(date, 'getWeekEnd'); const weekStart = DateCalculator.getISOWeekStart(date); const weekEnd = new Date(weekStart); weekEnd.setDate(weekStart.getDate() + 6); weekEnd.setHours(23, 59, 59, 999); return weekEnd; } /** * Get week number for a date (ISO 8601) * @param date - The date to get week number for * @returns Week number (1-53) */ static getWeekNumber(date: Date): number { const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); const dayNum = d.getUTCDay() || 7; d.setUTCDate(d.getUTCDate() + 4 - dayNum); const yearStart = new Date(Date.UTC(d.getUTCFullYear(),0,1)); return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1)/7); } /** * Format a date range with customizable options * @param start - Start date * @param end - End date * @param options - Formatting options * @returns Formatted date range string */ static formatDateRange( start: Date, end: Date, options: { locale?: string; month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow'; day?: 'numeric' | '2-digit'; year?: 'numeric' | '2-digit'; } = {} ): string { const { locale = 'en-US', month = 'short', day = 'numeric' } = options; const startYear = start.getFullYear(); const endYear = end.getFullYear(); const formatter = new Intl.DateTimeFormat(locale, { month, day, year: startYear !== endYear ? 'numeric' : undefined }); // @ts-ignore if (typeof formatter.formatRange === 'function') { // @ts-ignore return formatter.formatRange(start, end); } return `${formatter.format(start)} - ${formatter.format(end)}`; } /** * Format a date to ISO date string (YYYY-MM-DD) * @param date - Date to format * @returns ISO date string */ static formatISODate(date: Date): string { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; } /** * Check if a date is today * @param date - Date to check * @returns True if the date is today */ static isToday(date: Date): boolean { const today = new Date(); return date.toDateString() === today.toDateString(); } /** * Add days to a date * @param date - Base date * @param days - Number of days to add (can be negative) * @returns New date */ static addDays(date: Date, days: number): Date { const result = new Date(date); result.setDate(result.getDate() + days); return result; } /** * Add weeks to a date * @param date - Base date * @param weeks - Number of weeks to add (can be negative) * @returns New date */ static addWeeks(date: Date, weeks: number): Date { return DateCalculator.addDays(date, weeks * 7); } /** * Get all dates in a week * @param weekStart - Start of the week * @returns Array of 7 dates for the full week */ static getFullWeekDates(weekStart: Date): Date[] { const dates: Date[] = []; for (let i = 0; i < 7; i++) { dates.push(DateCalculator.addDays(weekStart, i)); } return dates; } /** * Get the day name for a date using Intl.DateTimeFormat * @param date - Date to get day name for * @param format - 'short' or 'long' * @returns Day name */ static getDayName(date: Date, format: 'short' | 'long' = 'short'): string { const formatter = new Intl.DateTimeFormat('en-US', { weekday: format }); return formatter.format(date); } /** * Format time to HH:MM * @param date - Date to format * @returns Time string */ static formatTime(date: Date): string { return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`; } /** * Format time to 12-hour format * @param date - Date to format * @returns 12-hour time string */ static formatTime12(date: Date): string { const hours = date.getHours(); const minutes = date.getMinutes(); const period = hours >= 12 ? 'PM' : 'AM'; const displayHours = hours % 12 || 12; return `${displayHours}:${String(minutes).padStart(2, '0')} ${period}`; } /** * Convert minutes since midnight to time string * @param minutes - Minutes since midnight * @returns Time string */ static minutesToTime(minutes: number): string { const hours = Math.floor(minutes / 60); const mins = minutes % 60; const period = hours >= 12 ? 'PM' : 'AM'; const displayHours = hours % 12 || 12; return `${displayHours}:${String(mins).padStart(2, '0')} ${period}`; } /** * Convert time string to minutes since midnight * @param timeStr - Time string * @returns Minutes since midnight */ static timeToMinutes(timeStr: string): number { const [time] = timeStr.split('T').pop()!.split('.'); const [hours, minutes] = time.split(':').map(Number); return hours * 60 + minutes; } /** * Get minutes since start of day * @param date - Date or ISO string * @returns Minutes since midnight */ static getMinutesSinceMidnight(date: Date | string): number { const d = typeof date === 'string' ? new Date(date) : date; return d.getHours() * 60 + d.getMinutes(); } /** * Calculate duration in minutes between two dates * @param start - Start date or ISO string * @param end - End date or ISO string * @returns Duration in minutes */ static getDurationMinutes(start: Date | string, end: Date | string): number { const startDate = typeof start === 'string' ? new Date(start) : start; const endDate = typeof end === 'string' ? new Date(end) : end; return Math.floor((endDate.getTime() - startDate.getTime()) / 60000); } /** * Check if two dates are on the same day * @param date1 - First date * @param date2 - Second date * @returns True if same day */ static isSameDay(date1: Date, date2: Date): boolean { return date1.toDateString() === date2.toDateString(); } /** * Check if event spans multiple days * @param start - Start date or ISO string * @param end - End date or ISO string * @returns True if spans multiple days */ static isMultiDay(start: Date | string, end: Date | string): boolean { const startDate = typeof start === 'string' ? new Date(start) : start; const endDate = typeof end === 'string' ? new Date(end) : end; return !DateCalculator.isSameDay(startDate, endDate); } // Legacy constructor for backward compatibility constructor() { // Empty constructor - all methods are now static } } // Legacy factory function - deprecated, use static methods instead export function createDateCalculator(config: CalendarConfig): DateCalculator { DateCalculator.initialize(config); return new DateCalculator(); }