Refactors date handling with DateService

Replaces DateCalculator with DateService for improved date and time operations, including timezone handling.

This change enhances the calendar's accuracy and flexibility in managing dates, especially concerning timezone configurations.
It also corrects a typo in the `allDay` dataset attribute.
This commit is contained in:
Janus C. H. Knudsen 2025-10-03 20:50:40 +02:00
parent 4859f42450
commit 6bbf2d8adb
17 changed files with 159 additions and 749 deletions

View file

@ -244,7 +244,7 @@ export class SwpAllDayEventElement extends BaseEventElement {
* Set all-day specific attributes
*/
private setAllDayAttributes(): void {
this.element.dataset.allDay = "true";
this.element.dataset.allday = "true";
this.element.dataset.start = this.event.start.toISOString();
this.element.dataset.end = this.event.end.toISOString();
}

View file

@ -3,7 +3,6 @@ import { eventBus } from './core/EventBus';
import { calendarConfig } from './core/CalendarConfig';
import { CalendarTypeFactory } from './factories/CalendarTypeFactory';
import { ManagerFactory } from './factories/ManagerFactory';
import { DateCalculator } from './utils/DateCalculator';
import { URLManager } from './utils/URLManager';
import { CalendarManagers } from './types/ManagerTypes';
@ -40,9 +39,6 @@ async function initializeCalendar(): Promise<void> {
// Use the singleton calendar configuration
const config = calendarConfig;
// Initialize DateCalculator with config first
DateCalculator.initialize(config);
// Initialize the CalendarTypeFactory before creating managers
CalendarTypeFactory.initialize();

View file

@ -7,7 +7,7 @@ import { GridManager } from './GridManager';
import { HeaderManager } from './HeaderManager';
import { EventRenderingService } from '../renderers/EventRendererManager';
import { ScrollManager } from './ScrollManager';
import { DateCalculator } from '../utils/DateCalculator';
import { DateService } from '../utils/DateService';
import { EventFilterManager } from './EventFilterManager';
import { InitializationReport } from '../types/ManagerTypes';
@ -23,7 +23,7 @@ export class CalendarManager {
private eventRenderer: EventRenderingService;
private scrollManager: ScrollManager;
private eventFilterManager: EventFilterManager;
private dateCalculator: DateCalculator;
private dateService: DateService;
private currentView: CalendarView = 'week';
private currentDate: Date = new Date();
private isInitialized: boolean = false;
@ -42,8 +42,8 @@ export class CalendarManager {
this.eventRenderer = eventRenderer;
this.scrollManager = scrollManager;
this.eventFilterManager = new EventFilterManager();
DateCalculator.initialize(calendarConfig);
this.dateCalculator = new DateCalculator();
const timezone = calendarConfig.getTimezone?.() || 'Europe/Copenhagen';
this.dateService = new DateService(timezone);
this.setupEventListeners();
}
@ -451,10 +451,10 @@ export class CalendarManager {
const lastDate = new Date(lastDateStr);
// Calculate week number from first date
const weekNumber = DateCalculator.getWeekNumber(firstDate);
const weekNumber = this.dateService.getWeekNumber(firstDate);
// Format date range
const dateRange = DateCalculator.formatDateRange(firstDate, lastDate);
const dateRange = this.dateService.formatDateRange(firstDate, lastDate);
// Emit week info update
this.eventBus.emit(CoreEvents.PERIOD_INFO_UPDATE, {

View file

@ -42,9 +42,6 @@ export class DragDropManager {
private currentColumnBounds: ColumnBounds | null = null;
private isDragStarted = false;
// Header tracking state
private isInHeader = false;
// Movement threshold to distinguish click from drag
private readonly dragThreshold = 5; // pixels
@ -460,7 +457,6 @@ export class DragDropManager {
this.draggedElement = null;
this.draggedClone = null;
this.isDragStarted = false;
this.isInHeader = false;
}
/**
@ -492,15 +488,11 @@ export class DragDropManager {
const elementAtPosition = document.elementFromPoint(event.clientX, event.clientY);
if (!elementAtPosition) return;
// Check if we're in a header area
const headerElement = elementAtPosition.closest('swp-day-header, swp-calendar-header');
const isCurrentlyInHeader = !!headerElement;
// Detect header enter
if (!this.isInHeader && isCurrentlyInHeader && this.draggedClone) {
this.isInHeader = true;
if (isCurrentlyInHeader && !this.draggedClone?.hasAttribute("data-allday")) {
// Calculate target date using existing method
const targetColumn = ColumnDetectionUtils.getColumnBounds(position);
if (targetColumn) {
@ -510,15 +502,14 @@ export class DragDropManager {
targetColumn: targetColumn,
mousePosition: { x: event.clientX, y: event.clientY },
originalElement: this.draggedElement,
draggedClone: this.draggedClone
draggedClone: this.draggedClone!!
};
this.eventBus.emit('drag:mouseenter-header', dragMouseEnterPayload);
}
}
// Detect header leave
if (this.isInHeader && !isCurrentlyInHeader) {
this.isInHeader = false;
if (isCurrentlyInHeader && this.draggedClone?.hasAttribute("data-allday")) {
console.log('🚪 DragDropManager: Emitting drag:mouseleave-header');

View file

@ -2,7 +2,7 @@ import { EventBus } from '../core/EventBus';
import { IEventBus, CalendarEvent, ResourceCalendarData } from '../types/CalendarTypes';
import { CoreEvents } from '../constants/CoreEvents';
import { calendarConfig } from '../core/CalendarConfig';
import { DateCalculator } from '../utils/DateCalculator';
import { DateService } from '../utils/DateService';
import { ResourceData } from '../types/ManagerTypes';
interface RawEventData {
@ -26,9 +26,12 @@ export class EventManager {
private rawData: ResourceCalendarData | RawEventData[] | null = null;
private eventCache = new Map<string, CalendarEvent[]>(); // Cache for period queries
private lastCacheKey: string = '';
private dateService: DateService;
constructor(eventBus: IEventBus) {
this.eventBus = eventBus;
const timezone = calendarConfig.getTimezone?.() || 'Europe/Copenhagen';
this.dateService = new DateService(timezone);
}
/**
@ -196,11 +199,11 @@ export class EventManager {
}
/**
* Optimized events for period with caching and DateCalculator
* Optimized events for period with caching and DateService
*/
public getEventsForPeriod(startDate: Date, endDate: Date): CalendarEvent[] {
// Create cache key using DateCalculator for consistent formatting
const cacheKey = `${DateCalculator.formatISODate(startDate)}_${DateCalculator.formatISODate(endDate)}`;
// Create cache key using DateService for consistent formatting
const cacheKey = `${this.dateService.formatISODate(startDate)}_${this.dateService.formatISODate(endDate)}`;
// Return cached result if available
if (this.lastCacheKey === cacheKey && this.eventCache.has(cacheKey)) {

View file

@ -9,7 +9,7 @@ import { CoreEvents } from '../constants/CoreEvents';
import { ResourceCalendarData, CalendarView } from '../types/CalendarTypes';
import { GridRenderer } from '../renderers/GridRenderer';
import { GridStyleManager } from '../renderers/GridStyleManager';
import { DateCalculator } from '../utils/DateCalculator';
import { DateService } from '../utils/DateService';
/**
* Simplified GridManager focused on coordination, delegates rendering to GridRenderer
@ -21,18 +21,35 @@ export class GridManager {
private currentView: CalendarView = 'week';
private gridRenderer: GridRenderer;
private styleManager: GridStyleManager;
private dateService: DateService;
constructor() {
// Initialize GridRenderer and StyleManager with config
this.gridRenderer = new GridRenderer();
this.styleManager = new GridStyleManager();
this.dateService = new DateService('Europe/Copenhagen');
this.init();
}
private init(): void {
this.findElements();
this.subscribeToEvents();
}
/**
* Get the start of the ISO week (Monday) for a given date
*/
private getISOWeekStart(date: Date): Date {
const weekBounds = this.dateService.getWeekBounds(date);
return this.dateService.startOfDay(weekBounds.start);
}
/**
* Get the end of the ISO week (Sunday) for a given date
*/
private getWeekEnd(date: Date): Date {
const weekBounds = this.dateService.getWeekBounds(date);
return this.dateService.endOfDay(weekBounds.end);
}
private findElements(): void {
@ -91,7 +108,7 @@ export class GridManager {
this.resourceData
);
// Calculate period range using DateCalculator
// Calculate period range
const periodRange = this.getPeriodRange();
// Get layout config based on current view
@ -110,42 +127,42 @@ export class GridManager {
/**
* Get current period label using DateCalculator
* Get current period label
*/
public getCurrentPeriodLabel(): string {
switch (this.currentView) {
case 'week':
case 'day':
const weekStart = DateCalculator.getISOWeekStart(this.currentDate);
const weekEnd = DateCalculator.getWeekEnd(this.currentDate);
return DateCalculator.formatDateRange(weekStart, weekEnd);
const weekStart = this.getISOWeekStart(this.currentDate);
const weekEnd = this.getWeekEnd(this.currentDate);
return this.dateService.formatDateRange(weekStart, weekEnd);
case 'month':
return this.currentDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
default:
const defaultWeekStart = DateCalculator.getISOWeekStart(this.currentDate);
const defaultWeekEnd = DateCalculator.getWeekEnd(this.currentDate);
return DateCalculator.formatDateRange(defaultWeekStart, defaultWeekEnd);
const defaultWeekStart = this.getISOWeekStart(this.currentDate);
const defaultWeekEnd = this.getWeekEnd(this.currentDate);
return this.dateService.formatDateRange(defaultWeekStart, defaultWeekEnd);
}
}
/**
* Navigate to next period using DateCalculator
* Navigate to next period
*/
public navigateNext(): void {
let nextDate: Date;
switch (this.currentView) {
case 'week':
nextDate = DateCalculator.addWeeks(this.currentDate, 1);
nextDate = this.dateService.addWeeks(this.currentDate, 1);
break;
case 'month':
nextDate = this.addMonths(this.currentDate, 1);
break;
case 'day':
nextDate = DateCalculator.addDays(this.currentDate, 1);
nextDate = this.dateService.addDays(this.currentDate, 1);
break;
default:
nextDate = DateCalculator.addWeeks(this.currentDate, 1);
nextDate = this.dateService.addWeeks(this.currentDate, 1);
}
this.currentDate = nextDate;
@ -160,23 +177,23 @@ export class GridManager {
}
/**
* Navigate to previous period using DateCalculator
* Navigate to previous period
*/
public navigatePrevious(): void {
let prevDate: Date;
switch (this.currentView) {
case 'week':
prevDate = DateCalculator.addWeeks(this.currentDate, -1);
prevDate = this.dateService.addWeeks(this.currentDate, -1);
break;
case 'month':
prevDate = this.addMonths(this.currentDate, -1);
break;
case 'day':
prevDate = DateCalculator.addDays(this.currentDate, -1);
prevDate = this.dateService.addDays(this.currentDate, -1);
break;
default:
prevDate = DateCalculator.addWeeks(this.currentDate, -1);
prevDate = this.dateService.addWeeks(this.currentDate, -1);
}
this.currentDate = prevDate;
@ -205,20 +222,20 @@ export class GridManager {
}
/**
* Get current view's display dates using DateCalculator
* Get current view's display dates
*/
public getDisplayDates(): Date[] {
switch (this.currentView) {
case 'week':
const weekStart = DateCalculator.getISOWeekStart(this.currentDate);
return DateCalculator.getFullWeekDates(weekStart);
const weekStart = this.getISOWeekStart(this.currentDate);
return this.dateService.getFullWeekDates(weekStart);
case 'month':
return this.getMonthDates(this.currentDate);
case 'day':
return [this.currentDate];
default:
const defaultWeekStart = DateCalculator.getISOWeekStart(this.currentDate);
return DateCalculator.getFullWeekDates(defaultWeekStart);
const defaultWeekStart = this.getISOWeekStart(this.currentDate);
return this.dateService.getFullWeekDates(defaultWeekStart);
}
}
@ -228,8 +245,8 @@ export class GridManager {
private getPeriodRange(): { startDate: Date; endDate: Date } {
switch (this.currentView) {
case 'week':
const weekStart = DateCalculator.getISOWeekStart(this.currentDate);
const weekEnd = DateCalculator.getWeekEnd(this.currentDate);
const weekStart = this.getISOWeekStart(this.currentDate);
const weekEnd = this.getWeekEnd(this.currentDate);
return {
startDate: weekStart,
endDate: weekEnd
@ -245,8 +262,8 @@ export class GridManager {
endDate: this.currentDate
};
default:
const defaultWeekStart = DateCalculator.getISOWeekStart(this.currentDate);
const defaultWeekEnd = DateCalculator.getWeekEnd(this.currentDate);
const defaultWeekStart = this.getISOWeekStart(this.currentDate);
const defaultWeekEnd = this.getWeekEnd(this.currentDate);
return {
startDate: defaultWeekStart,
endDate: defaultWeekEnd

View file

@ -1,6 +1,6 @@
import { IEventBus } from '../types/CalendarTypes';
import { EventRenderingService } from '../renderers/EventRendererManager';
import { DateCalculator } from '../utils/DateCalculator';
import { DateService } from '../utils/DateService';
import { CoreEvents } from '../constants/CoreEvents';
import { NavigationRenderer } from '../renderers/NavigationRenderer';
import { GridRenderer } from '../renderers/GridRenderer';
@ -14,18 +14,17 @@ export class NavigationManager {
private eventBus: IEventBus;
private navigationRenderer: NavigationRenderer;
private gridRenderer: GridRenderer;
private dateCalculator: DateCalculator;
private dateService: DateService;
private currentWeek: Date;
private targetWeek: Date;
private animationQueue: number = 0;
constructor(eventBus: IEventBus, eventRenderer: EventRenderingService) {
this.eventBus = eventBus;
DateCalculator.initialize(calendarConfig);
this.dateCalculator = new DateCalculator();
this.dateService = new DateService('Europe/Copenhagen');
this.navigationRenderer = new NavigationRenderer(eventBus, eventRenderer);
this.gridRenderer = new GridRenderer();
this.currentWeek = DateCalculator.getISOWeekStart(new Date());
this.currentWeek = this.getISOWeekStart(new Date());
this.targetWeek = new Date(this.currentWeek);
this.init();
}
@ -34,6 +33,16 @@ export class NavigationManager {
this.setupEventListeners();
}
/**
* 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
*/
private getISOWeekStart(date: Date): Date {
const weekBounds = this.dateService.getWeekBounds(date);
return this.dateService.startOfDay(weekBounds.start);
}
private getCalendarContainer(): HTMLElement | null {
return document.querySelector('swp-calendar-container');
@ -113,7 +122,7 @@ export class NavigationManager {
* Navigate to specific event date and emit scroll event after navigation
*/
private navigateToEventDate(eventDate: Date, eventStartTime: string): void {
const weekStart = DateCalculator.getISOWeekStart(eventDate);
const weekStart = this.getISOWeekStart(eventDate);
this.targetWeek = new Date(weekStart);
const currentTime = this.currentWeek.getTime();
@ -159,7 +168,7 @@ export class NavigationManager {
private navigateToToday(): void {
const today = new Date();
const todayWeekStart = DateCalculator.getISOWeekStart(today);
const todayWeekStart = this.getISOWeekStart(today);
// Reset to today
this.targetWeek = new Date(todayWeekStart);
@ -177,7 +186,7 @@ export class NavigationManager {
}
private navigateToDate(date: Date): void {
const weekStart = DateCalculator.getISOWeekStart(date);
const weekStart = this.getISOWeekStart(date);
this.targetWeek = new Date(weekStart);
const currentTime = this.currentWeek.getTime();
@ -277,9 +286,9 @@ export class NavigationManager {
}
private updateWeekInfo(): void {
const weekNumber = DateCalculator.getWeekNumber(this.currentWeek);
const weekEnd = DateCalculator.addDays(this.currentWeek, 6);
const dateRange = DateCalculator.formatDateRange(this.currentWeek, weekEnd);
const weekNumber = this.dateService.getWeekNumber(this.currentWeek);
const weekEnd = this.dateService.addDays(this.currentWeek, 6);
const dateRange = this.dateService.formatDateRange(this.currentWeek, weekEnd);
// Notify other managers about week info update - DOM manipulation should happen via events
this.eventBus.emit(CoreEvents.PERIOD_INFO_UPDATE, {

View file

@ -1,6 +1,6 @@
// Work hours management for per-column scheduling
import { DateCalculator } from '../utils/DateCalculator';
import { DateService } from '../utils/DateService';
import { calendarConfig } from '../core/CalendarConfig';
import { PositionUtils } from '../utils/PositionUtils';
@ -34,12 +34,12 @@ export interface WorkScheduleConfig {
* Manages work hours scheduling with weekly defaults and date-specific overrides
*/
export class WorkHoursManager {
private dateCalculator: DateCalculator;
private dateService: DateService;
private workSchedule: WorkScheduleConfig;
constructor() {
DateCalculator.initialize(calendarConfig);
this.dateCalculator = new DateCalculator();
const timezone = calendarConfig.getTimezone?.() || 'Europe/Copenhagen';
this.dateService = new DateService(timezone);
// Default work schedule - will be loaded from JSON later
this.workSchedule = {
@ -64,7 +64,7 @@ export class WorkHoursManager {
* Get work hours for a specific date
*/
getWorkHoursForDate(date: Date): DayWorkHours | 'off' {
const dateString = DateCalculator.formatISODate(date);
const dateString = this.dateService.formatISODate(date);
// Check for date-specific override first
if (this.workSchedule.dateOverrides[dateString]) {
@ -83,7 +83,7 @@ export class WorkHoursManager {
const workHoursMap = new Map<string, DayWorkHours | 'off'>();
dates.forEach(date => {
const dateString = DateCalculator.formatISODate(date);
const dateString = this.dateService.formatISODate(date);
const workHours = this.getWorkHoursForDate(date);
workHoursMap.set(dateString, workHours);
});

View file

@ -2,7 +2,7 @@
import { CalendarConfig } from '../core/CalendarConfig';
import { ResourceCalendarData } from '../types/CalendarTypes';
import { DateCalculator } from '../utils/DateCalculator';
import { DateService } from '../utils/DateService';
import { WorkHoursManager } from '../managers/WorkHoursManager';
/**
@ -25,25 +25,26 @@ export interface ColumnRenderContext {
* Date-based column renderer (original functionality)
*/
export class DateColumnRenderer implements ColumnRenderer {
private dateCalculator!: DateCalculator;
private dateService!: DateService;
private workHoursManager!: WorkHoursManager;
render(columnContainer: HTMLElement, context: ColumnRenderContext): void {
const { currentWeek, config } = context;
// Initialize date calculator and work hours manager
DateCalculator.initialize(config);
this.dateCalculator = new DateCalculator();
// Initialize date service and work hours manager
const timezone = config.getTimezone?.() || 'Europe/Copenhagen';
this.dateService = new DateService(timezone);
this.workHoursManager = new WorkHoursManager();
const dates = DateCalculator.getWorkWeekDates(currentWeek);
const workWeekSettings = config.getWorkWeekSettings();
const dates = this.dateService.getWorkWeekDates(currentWeek, workWeekSettings.workDays);
const dateSettings = config.getDateViewSettings();
const daysToShow = dates.slice(0, dateSettings.weekDays);
daysToShow.forEach((date) => {
const column = document.createElement('swp-day-column');
(column as any).dataset.date = DateCalculator.formatISODate(date);
(column as any).dataset.date = this.dateService.formatISODate(date);
// Apply work hours styling
this.applyWorkHoursToColumn(column, date);

View file

@ -2,7 +2,6 @@
import { CalendarEvent } from '../types/CalendarTypes';
import { calendarConfig } from '../core/CalendarConfig';
import { DateCalculator } from '../utils/DateCalculator';
import { eventBus } from '../core/EventBus';
import { OverlapDetector, OverlapResult } from '../utils/OverlapDetector';
import { SwpEventElement } from '../elements/SwpEventElement';
@ -38,18 +37,13 @@ export interface EventRendererStrategy {
*/
export class DateEventRenderer implements EventRendererStrategy {
private dateService: DateService;
constructor(dateCalculator?: DateCalculator) {
if (!dateCalculator) {
DateCalculator.initialize(calendarConfig);
}
this.dateCalculator = dateCalculator || new DateCalculator();
constructor() {
const timezone = calendarConfig.getTimezone?.() || 'Europe/Copenhagen';
this.dateService = new DateService(timezone);
this.setupDragEventListeners();
}
private dateCalculator: DateCalculator;
private draggedClone: HTMLElement | null = null;
private originalEvent: HTMLElement | null = null;
@ -634,10 +628,9 @@ export class DateEventRenderer implements EventRendererStrategy {
}
const columnEvents = events.filter(event => {
const eventDateStr = DateCalculator.formatISODate(event.start);
const eventDateStr = this.dateService.formatISODate(event.start);
const matches = eventDateStr === columnDate;
return matches;
});

View file

@ -3,7 +3,7 @@ import { ResourceCalendarData, CalendarView } from '../types/CalendarTypes';
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
import { ColumnRenderContext } from './ColumnRenderer';
import { eventBus } from '../core/EventBus';
import { DateCalculator } from '../utils/DateCalculator';
import { DateService } from '../utils/DateService';
import { CoreEvents } from '../constants/CoreEvents';
/**
@ -13,8 +13,11 @@ import { CoreEvents } from '../constants/CoreEvents';
export class GridRenderer {
private cachedGridContainer: HTMLElement | null = null;
private cachedTimeAxis: HTMLElement | null = null;
private dateService: DateService;
constructor() {
const timezone = calendarConfig.getTimezone?.() || 'Europe/Copenhagen';
this.dateService = new DateService(timezone);
}
/**
@ -239,7 +242,7 @@ export class GridRenderer {
console.log('Parent container:', parentContainer);
console.log('Using same grid creation as initial load');
const weekEnd = DateCalculator.addDays(weekStart, 6);
const weekEnd = this.dateService.addDays(weekStart, 6);
// Use SAME method as initial load - respects workweek and resource settings
const newGrid = this.createOptimizedGridContainer(weekStart, null, 'week');

View file

@ -2,7 +2,7 @@
import { CalendarConfig } from '../core/CalendarConfig';
import { ResourceCalendarData } from '../types/CalendarTypes';
import { DateCalculator } from '../utils/DateCalculator';
import { DateService } from '../utils/DateService';
/**
* Interface for header rendering strategies
@ -25,7 +25,7 @@ export interface HeaderRenderContext {
* Date-based header renderer (original functionality)
*/
export class DateHeaderRenderer implements HeaderRenderer {
private dateCalculator!: DateCalculator;
private dateService!: DateService;
render(calendarHeader: HTMLElement, context: HeaderRenderContext): void {
const { currentWeek, config } = context;
@ -34,27 +34,28 @@ export class DateHeaderRenderer implements HeaderRenderer {
const allDayContainer = document.createElement('swp-allday-container');
calendarHeader.appendChild(allDayContainer);
// Initialize date calculator with config
DateCalculator.initialize(config);
this.dateCalculator = new DateCalculator();
// Initialize date service with config
const timezone = config.getTimezone?.() || 'Europe/Copenhagen';
this.dateService = new DateService(timezone);
const dates = DateCalculator.getWorkWeekDates(currentWeek);
const workWeekSettings = config.getWorkWeekSettings();
const dates = this.dateService.getWorkWeekDates(currentWeek, workWeekSettings.workDays);
const weekDays = config.getDateViewSettings().weekDays;
const daysToShow = dates.slice(0, weekDays);
daysToShow.forEach((date, index) => {
const header = document.createElement('swp-day-header');
if (DateCalculator.isToday(date)) {
if (this.dateService.isSameDay(date, new Date())) {
(header as any).dataset.today = 'true';
}
const dayName = DateCalculator.getDayName(date, 'short');
const dayName = this.dateService.getDayName(date, 'short');
header.innerHTML = `
<swp-day-name>${dayName}</swp-day-name>
<swp-day-date>${date.getDate()}</swp-day-date>
`;
(header as any).dataset.date = DateCalculator.formatISODate(date);
(header as any).dataset.date = this.dateService.formatISODate(date);
calendarHeader.appendChild(header);
});

View file

@ -4,16 +4,15 @@
*/
import { ViewStrategy, ViewContext, ViewLayoutConfig } from './ViewStrategy';
import { DateCalculator } from '../utils/DateCalculator';
import { DateService } from '../utils/DateService';
import { calendarConfig } from '../core/CalendarConfig';
import { CalendarEvent } from '../types/CalendarTypes';
export class MonthViewStrategy implements ViewStrategy {
private dateCalculator: DateCalculator;
private dateService: DateService;
constructor() {
DateCalculator.initialize(calendarConfig);
this.dateCalculator = new DateCalculator();
this.dateService = new DateService('Europe/Copenhagen');
}
getLayoutConfig(): ViewLayoutConfig {
@ -74,7 +73,7 @@ export class MonthViewStrategy implements ViewStrategy {
dates.forEach(date => {
const cell = document.createElement('div');
cell.className = 'month-day-cell';
cell.dataset.date = DateCalculator.formatISODate(date);
cell.dataset.date = this.dateService.formatISODate(date);
cell.style.border = '1px solid #e0e0e0';
cell.style.minHeight = '100px';
cell.style.padding = '4px';
@ -88,7 +87,7 @@ export class MonthViewStrategy implements ViewStrategy {
dayNumber.style.marginBottom = '4px';
// Check if today
if (DateCalculator.isToday(date)) {
if (this.dateService.isSameDay(date, new Date())) {
dayNumber.style.color = '#1976d2';
cell.style.backgroundColor = '#f5f5f5';
}
@ -103,12 +102,13 @@ export class MonthViewStrategy implements ViewStrategy {
const firstOfMonth = new Date(monthDate.getFullYear(), monthDate.getMonth(), 1);
// Get Monday of the week containing first day
const startDate = DateCalculator.getISOWeekStart(firstOfMonth);
const weekBounds = this.dateService.getWeekBounds(firstOfMonth);
const startDate = this.dateService.startOfDay(weekBounds.start);
// Generate 42 days (6 weeks)
const dates: Date[] = [];
for (let i = 0; i < 42; i++) {
dates.push(DateCalculator.addDays(startDate, i));
dates.push(this.dateService.addDays(startDate, i));
}
return dates;
@ -143,10 +143,11 @@ export class MonthViewStrategy implements ViewStrategy {
const firstOfMonth = new Date(baseDate.getFullYear(), baseDate.getMonth(), 1);
// Get Monday of the week containing first day
const startDate = DateCalculator.getISOWeekStart(firstOfMonth);
const weekBounds = this.dateService.getWeekBounds(firstOfMonth);
const startDate = this.dateService.startOfDay(weekBounds.start);
// End date is 41 days after start (42 total days)
const endDate = DateCalculator.addDays(startDate, 41);
const endDate = this.dateService.addDays(startDate, 41);
return {
startDate,

View file

@ -4,19 +4,19 @@
*/
import { ViewStrategy, ViewContext, ViewLayoutConfig } from './ViewStrategy';
import { DateCalculator } from '../utils/DateCalculator';
import { DateService } from '../utils/DateService';
import { calendarConfig } from '../core/CalendarConfig';
import { GridRenderer } from '../renderers/GridRenderer';
import { GridStyleManager } from '../renderers/GridStyleManager';
export class WeekViewStrategy implements ViewStrategy {
private dateCalculator: DateCalculator;
private dateService: DateService;
private gridRenderer: GridRenderer;
private styleManager: GridStyleManager;
constructor() {
DateCalculator.initialize(calendarConfig);
this.dateCalculator = new DateCalculator();
const timezone = calendarConfig.getTimezone?.() || 'Europe/Copenhagen';
this.dateService = new DateService(timezone);
this.gridRenderer = new GridRenderer();
this.styleManager = new GridStyleManager();
}
@ -43,28 +43,31 @@ export class WeekViewStrategy implements ViewStrategy {
}
getNextPeriod(currentDate: Date): Date {
return DateCalculator.addWeeks(currentDate, 1);
return this.dateService.addWeeks(currentDate, 1);
}
getPreviousPeriod(currentDate: Date): Date {
return DateCalculator.addWeeks(currentDate, -1);
return this.dateService.addWeeks(currentDate, -1);
}
getPeriodLabel(date: Date): string {
const weekStart = DateCalculator.getISOWeekStart(date);
const weekEnd = DateCalculator.addDays(weekStart, 6);
const weekNumber = DateCalculator.getWeekNumber(date);
const weekBounds = this.dateService.getWeekBounds(date);
const weekStart = this.dateService.startOfDay(weekBounds.start);
const weekEnd = this.dateService.addDays(weekStart, 6);
const weekNumber = this.dateService.getWeekNumber(date);
return `Week ${weekNumber}: ${DateCalculator.formatDateRange(weekStart, weekEnd)}`;
return `Week ${weekNumber}: ${this.dateService.formatDateRange(weekStart, weekEnd)}`;
}
getDisplayDates(baseDate: Date): Date[] {
return DateCalculator.getWorkWeekDates(baseDate);
const workWeekSettings = calendarConfig.getWorkWeekSettings();
return this.dateService.getWorkWeekDates(baseDate, workWeekSettings.workDays);
}
getPeriodRange(baseDate: Date): { startDate: Date; endDate: Date } {
const weekStart = DateCalculator.getISOWeekStart(baseDate);
const weekEnd = DateCalculator.addDays(weekStart, 6);
const weekBounds = this.dateService.getWeekBounds(baseDate);
const weekStart = this.dateService.startOfDay(weekBounds.start);
const weekEnd = this.dateService.addDays(weekStart, 6);
return {
startDate: weekStart,

View file

@ -1,300 +0,0 @@
/**
* DateCalculator - Centralized date calculation logic for calendar
* Now uses DateService internally for all date operations
* Handles all date computations with proper week start handling
*/
import { CalendarConfig } from '../core/CalendarConfig';
import { DateService } from './DateService';
export class DateCalculator {
private static config: CalendarConfig;
private static dateService: DateService = new DateService('Europe/Copenhagen');
/**
* Initialize DateCalculator with configuration
* @param config - Calendar configuration
*/
static initialize(config: CalendarConfig): void {
DateCalculator.config = config;
// Update DateService with timezone from config if available
const timezone = config.getTimezone?.() || 'Europe/Copenhagen';
DateCalculator.dateService = new DateService(timezone);
}
/**
* Validate that a date is valid
* @param date - Date to validate
* @returns True if date is valid, false otherwise
*/
private static validateDate(date: Date): boolean {
return date && date instanceof Date && DateCalculator.dateService.isValid(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[] {
if (!DateCalculator.validateDate(weekStart)) {
throw new Error('getWorkWeekDates: Invalid date provided');
}
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 using DateService
* @param date - Any date in the week
* @returns The Monday of the ISO week
*/
static getISOWeekStart(date: Date): Date {
if (!DateCalculator.validateDate(date)) {
throw new Error('getISOWeekStart: Invalid date provided');
}
const weekBounds = DateCalculator.dateService.getWeekBounds(date);
return DateCalculator.dateService.startOfDay(weekBounds.start);
}
/**
* Get the end of the ISO week for a given date using DateService
* @param date - Any date in the week
* @returns The end date of the ISO week (Sunday)
*/
static getWeekEnd(date: Date): Date {
if (!DateCalculator.validateDate(date)) {
throw new Error('getWeekEnd: Invalid date provided');
}
const weekBounds = DateCalculator.dateService.getWeekBounds(date);
return DateCalculator.dateService.endOfDay(weekBounds.end);
}
/**
* 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) using DateService
* @param date - Date to format
* @returns ISO date string or empty string if invalid
*/
static formatISODate(date: Date): string {
if (!DateCalculator.validateDate(date)) {
return '';
}
return DateCalculator.dateService.formatDate(date);
}
/**
* Check if a date is today using DateService
* @param date - Date to check
* @returns True if the date is today
*/
static isToday(date: Date): boolean {
return DateCalculator.dateService.isSameDay(date, new Date());
}
/**
* Add days to a date using DateService
* @param date - Base date
* @param days - Number of days to add (can be negative)
* @returns New date
*/
static addDays(date: Date, days: number): Date {
return DateCalculator.dateService.addDays(date, days);
}
/**
* Add weeks to a date using DateService
* @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.dateService.addWeeks(date, weeks);
}
/**
* 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 using DateService
* @param date - Date to format
* @returns Time string
*/
static formatTime(date: Date): string {
return DateCalculator.dateService.formatTime(date);
}
/**
* 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 using DateService
* @param minutes - Minutes since midnight
* @returns Time string
*/
static minutesToTime(minutes: number): string {
return DateCalculator.dateService.minutesToTime(minutes);
}
/**
* Convert time string to minutes since midnight using DateService
* @param timeStr - Time string
* @returns Minutes since midnight
*/
static timeToMinutes(timeStr: string): number {
return DateCalculator.dateService.timeToMinutes(timeStr);
}
/**
* Get minutes since start of day using DateService
* @param date - Date or ISO string
* @returns Minutes since midnight
*/
static getMinutesSinceMidnight(date: Date | string): number {
const d = typeof date === 'string' ? DateCalculator.dateService.parseISO(date) : date;
return DateCalculator.dateService.getMinutesSinceMidnight(d);
}
/**
* Calculate duration in minutes between two dates using DateService
* @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 {
return DateCalculator.dateService.getDurationMinutes(start, end);
}
/**
* Check if two dates are on the same day using DateService
* @param date1 - First date
* @param date2 - Second date
* @returns True if same day
*/
static isSameDay(date1: Date, date2: Date): boolean {
return DateCalculator.dateService.isSameDay(date1, date2);
}
/**
* 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' ? DateCalculator.dateService.parseISO(start) : start;
const endDate = typeof end === 'string' ? DateCalculator.dateService.parseISO(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();
}

View file

@ -1,15 +1,17 @@
import { calendarConfig } from '../core/CalendarConfig';
import { ColumnBounds } from './ColumnDetectionUtils';
import { DateCalculator } from './DateCalculator';
import { DateService } from './DateService';
import { TimeFormatter } from './TimeFormatter';
/**
* PositionUtils - Static positioning utilities using singleton calendarConfig
* Focuses on pixel/position calculations while delegating date operations
*
* Note: Uses DateCalculator and TimeFormatter which internally use DateService with date-fns
* Note: Uses DateService with date-fns for all date/time operations
*/
export class PositionUtils {
private static dateService = new DateService('Europe/Copenhagen');
/**
* Convert minutes to pixels
*/
@ -29,10 +31,10 @@ export class PositionUtils {
}
/**
* Convert time (HH:MM) to pixels from day start using DateCalculator
* Convert time (HH:MM) to pixels from day start using DateService
*/
public static timeToPixels(timeString: string): number {
const totalMinutes = DateCalculator.timeToMinutes(timeString);
const totalMinutes = PositionUtils.dateService.timeToMinutes(timeString);
const gridSettings = calendarConfig.getGridSettings();
const dayStartMinutes = gridSettings.dayStartHour * 60;
const minutesFromDayStart = totalMinutes - dayStartMinutes;
@ -41,10 +43,10 @@ export class PositionUtils {
}
/**
* Convert Date object to pixels from day start using DateCalculator
* Convert Date object to pixels from day start using DateService
*/
public static dateToPixels(date: Date): number {
const totalMinutes = DateCalculator.getMinutesSinceMidnight(date);
const totalMinutes = PositionUtils.dateService.getMinutesSinceMidnight(date);
const gridSettings = calendarConfig.getGridSettings();
const dayStartMinutes = gridSettings.dayStartHour * 60;
const minutesFromDayStart = totalMinutes - dayStartMinutes;
@ -53,7 +55,7 @@ export class PositionUtils {
}
/**
* Convert pixels to time using DateCalculator
* Convert pixels to time using DateService
*/
public static pixelsToTime(pixels: number): string {
const minutes = PositionUtils.pixelsToMinutes(pixels);
@ -61,7 +63,7 @@ export class PositionUtils {
const dayStartMinutes = gridSettings.dayStartHour * 60;
const totalMinutes = dayStartMinutes + minutes;
return DateCalculator.minutesToTime(totalMinutes);
return PositionUtils.dateService.minutesToTime(totalMinutes);
}
/**
@ -109,15 +111,15 @@ export class PositionUtils {
}
/**
* Snap time to interval using DateCalculator
* Snap time to interval using DateService
*/
public static snapTimeToInterval(timeString: string): string {
const totalMinutes = DateCalculator.timeToMinutes(timeString);
const totalMinutes = PositionUtils.dateService.timeToMinutes(timeString);
const gridSettings = calendarConfig.getGridSettings();
const snapInterval = gridSettings.snapInterval;
const snappedMinutes = Math.round(totalMinutes / snapInterval) * snapInterval;
return DateCalculator.minutesToTime(snappedMinutes);
return PositionUtils.dateService.minutesToTime(snappedMinutes);
}
/**
@ -220,10 +222,10 @@ export class PositionUtils {
}
/**
* Convert time string to ISO datetime using DateCalculator
* Convert time string to ISO datetime using DateService
*/
public static timeStringToIso(timeString: string, date: Date = new Date()): string {
const totalMinutes = DateCalculator.timeToMinutes(timeString);
const totalMinutes = PositionUtils.dateService.timeToMinutes(timeString);
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
@ -234,10 +236,10 @@ export class PositionUtils {
}
/**
* Calculate event duration using DateCalculator
* Calculate event duration using DateService
*/
public static calculateDuration(startTime: string | Date, endTime: string | Date): number {
return DateCalculator.getDurationMinutes(startTime, endTime);
return PositionUtils.dateService.getDurationMinutes(startTime, endTime);
}
/**

View file

@ -1,310 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { DateCalculator } from '../../src/utils/DateCalculator';
import { CalendarConfig } from '../../src/core/CalendarConfig';
describe('DateCalculator', () => {
let testConfig: CalendarConfig;
beforeEach(() => {
testConfig = new CalendarConfig();
DateCalculator.initialize(testConfig);
});
describe('Week Operations', () => {
it('should get ISO week start (Monday)', () => {
// Wednesday, January 17, 2024
const date = new Date(2024, 0, 17);
const weekStart = DateCalculator.getISOWeekStart(date);
// Should be Monday, January 15
expect(weekStart.getDate()).toBe(15);
expect(weekStart.getDay()).toBe(1); // Monday
expect(weekStart.getHours()).toBe(0);
expect(weekStart.getMinutes()).toBe(0);
});
it('should get ISO week start for Sunday', () => {
// Sunday, January 21, 2024
const date = new Date(2024, 0, 21);
const weekStart = DateCalculator.getISOWeekStart(date);
// Should be Monday, January 15
expect(weekStart.getDate()).toBe(15);
expect(weekStart.getDay()).toBe(1);
});
it('should get week end (Sunday)', () => {
// Wednesday, January 17, 2024
const date = new Date(2024, 0, 17);
const weekEnd = DateCalculator.getWeekEnd(date);
// Should be Sunday, January 21
expect(weekEnd.getDate()).toBe(21);
expect(weekEnd.getDay()).toBe(0); // Sunday
expect(weekEnd.getHours()).toBe(23);
expect(weekEnd.getMinutes()).toBe(59);
});
it('should get work week dates (Mon-Fri)', () => {
const date = new Date(2024, 0, 17); // Wednesday
const workDays = DateCalculator.getWorkWeekDates(date);
expect(workDays).toHaveLength(5);
expect(workDays[0].getDay()).toBe(1); // Monday
expect(workDays[4].getDay()).toBe(5); // Friday
});
it('should get full week dates (7 days)', () => {
const weekStart = new Date(2024, 0, 15); // Monday
const fullWeek = DateCalculator.getFullWeekDates(weekStart);
expect(fullWeek).toHaveLength(7);
expect(fullWeek[0].getDay()).toBe(1); // Monday
expect(fullWeek[6].getDay()).toBe(0); // Sunday
});
it('should calculate ISO week number', () => {
const date1 = new Date(2024, 0, 1); // January 1, 2024
const weekNum1 = DateCalculator.getWeekNumber(date1);
expect(weekNum1).toBe(1);
const date2 = new Date(2024, 0, 15); // January 15, 2024
const weekNum2 = DateCalculator.getWeekNumber(date2);
expect(weekNum2).toBe(3);
});
it('should handle year boundary for week numbers', () => {
const date = new Date(2023, 11, 31); // December 31, 2023
const weekNum = DateCalculator.getWeekNumber(date);
// Week 52 or 53 depending on year
expect(weekNum).toBeGreaterThanOrEqual(52);
});
});
describe('Date Manipulation', () => {
it('should add days', () => {
const date = new Date(2024, 0, 15);
const newDate = DateCalculator.addDays(date, 5);
expect(newDate.getDate()).toBe(20);
expect(newDate.getMonth()).toBe(0);
});
it('should subtract days', () => {
const date = new Date(2024, 0, 15);
const newDate = DateCalculator.addDays(date, -5);
expect(newDate.getDate()).toBe(10);
});
it('should add weeks', () => {
const date = new Date(2024, 0, 15);
const newDate = DateCalculator.addWeeks(date, 2);
expect(newDate.getDate()).toBe(29);
});
it('should subtract weeks', () => {
const date = new Date(2024, 0, 15);
const newDate = DateCalculator.addWeeks(date, -1);
expect(newDate.getDate()).toBe(8);
});
it('should handle month boundaries when adding days', () => {
const date = new Date(2024, 0, 30); // January 30
const newDate = DateCalculator.addDays(date, 5);
expect(newDate.getDate()).toBe(4); // February 4
expect(newDate.getMonth()).toBe(1);
});
});
describe('Time Formatting', () => {
it('should format time (24-hour)', () => {
const date = new Date(2024, 0, 15, 14, 30, 45);
const formatted = DateCalculator.formatTime(date);
expect(formatted).toBe('14:30');
});
it('should format time (12-hour)', () => {
const date1 = new Date(2024, 0, 15, 14, 30, 0);
const formatted1 = DateCalculator.formatTime12(date1);
expect(formatted1).toBe('2:30 PM');
const date2 = new Date(2024, 0, 15, 9, 15, 0);
const formatted2 = DateCalculator.formatTime12(date2);
expect(formatted2).toBe('9:15 AM');
const date3 = new Date(2024, 0, 15, 0, 0, 0);
const formatted3 = DateCalculator.formatTime12(date3);
expect(formatted3).toBe('12:00 AM');
});
it('should format ISO date', () => {
const date = new Date(2024, 0, 15, 14, 30, 0);
const formatted = DateCalculator.formatISODate(date);
expect(formatted).toBe('2024-01-15');
});
it('should format date range', () => {
const start = new Date(2024, 0, 15);
const end = new Date(2024, 0, 21);
const formatted = DateCalculator.formatDateRange(start, end);
expect(formatted).toContain('Jan');
expect(formatted).toContain('15');
expect(formatted).toContain('21');
});
it('should get day name (short)', () => {
const monday = new Date(2024, 0, 15); // Monday
const dayName = DateCalculator.getDayName(monday, 'short');
expect(dayName).toBe('Mon');
});
it('should get day name (long)', () => {
const monday = new Date(2024, 0, 15); // Monday
const dayName = DateCalculator.getDayName(monday, 'long');
expect(dayName).toBe('Monday');
});
});
describe('Time Calculations', () => {
it('should convert time string to minutes', () => {
expect(DateCalculator.timeToMinutes('09:00')).toBe(540);
expect(DateCalculator.timeToMinutes('14:30')).toBe(870);
expect(DateCalculator.timeToMinutes('00:00')).toBe(0);
expect(DateCalculator.timeToMinutes('23:59')).toBe(1439);
});
it('should convert minutes to time string', () => {
expect(DateCalculator.minutesToTime(540)).toBe('09:00');
expect(DateCalculator.minutesToTime(870)).toBe('14:30');
expect(DateCalculator.minutesToTime(0)).toBe('00:00');
expect(DateCalculator.minutesToTime(1439)).toBe('23:59');
});
it('should get minutes since midnight from Date', () => {
const date = new Date(2024, 0, 15, 14, 30, 0);
const minutes = DateCalculator.getMinutesSinceMidnight(date);
expect(minutes).toBe(870); // 14*60 + 30
});
it('should get minutes since midnight from ISO string', () => {
const isoString = '2024-01-15T14:30:00.000Z';
const minutes = DateCalculator.getMinutesSinceMidnight(isoString);
// Note: This will be in local time after parsing
expect(minutes).toBeGreaterThanOrEqual(0);
expect(minutes).toBeLessThan(1440);
});
it('should calculate duration in minutes', () => {
const start = new Date(2024, 0, 15, 9, 0, 0);
const end = new Date(2024, 0, 15, 10, 30, 0);
const duration = DateCalculator.getDurationMinutes(start, end);
expect(duration).toBe(90);
});
it('should calculate duration from ISO strings', () => {
const start = '2024-01-15T09:00:00.000Z';
const end = '2024-01-15T10:30:00.000Z';
const duration = DateCalculator.getDurationMinutes(start, end);
expect(duration).toBe(90);
});
it('should handle cross-midnight duration', () => {
const start = new Date(2024, 0, 15, 23, 0, 0);
const end = new Date(2024, 0, 16, 1, 0, 0);
const duration = DateCalculator.getDurationMinutes(start, end);
expect(duration).toBe(120); // 2 hours
});
});
describe('Date Comparisons', () => {
it('should check if date is today', () => {
const today = new Date();
const yesterday = DateCalculator.addDays(new Date(), -1);
expect(DateCalculator.isToday(today)).toBe(true);
expect(DateCalculator.isToday(yesterday)).toBe(false);
});
it('should check if same day', () => {
const date1 = new Date(2024, 0, 15, 10, 0, 0);
const date2 = new Date(2024, 0, 15, 14, 30, 0);
const date3 = new Date(2024, 0, 16, 10, 0, 0);
expect(DateCalculator.isSameDay(date1, date2)).toBe(true);
expect(DateCalculator.isSameDay(date1, date3)).toBe(false);
});
it('should check if multi-day event (Date objects)', () => {
const start = new Date(2024, 0, 15, 10, 0, 0);
const end1 = new Date(2024, 0, 15, 14, 0, 0);
const end2 = new Date(2024, 0, 16, 10, 0, 0);
expect(DateCalculator.isMultiDay(start, end1)).toBe(false);
expect(DateCalculator.isMultiDay(start, end2)).toBe(true);
});
it('should check if multi-day event (ISO strings)', () => {
const start = '2024-01-15T10:00:00.000Z';
const end1 = '2024-01-15T14:00:00.000Z';
const end2 = '2024-01-16T10:00:00.000Z';
expect(DateCalculator.isMultiDay(start, end1)).toBe(false);
expect(DateCalculator.isMultiDay(start, end2)).toBe(true);
});
});
describe('Edge Cases', () => {
it('should handle midnight', () => {
const date = new Date(2024, 0, 15, 0, 0, 0);
const minutes = DateCalculator.getMinutesSinceMidnight(date);
expect(minutes).toBe(0);
});
it('should handle end of day', () => {
const date = new Date(2024, 0, 15, 23, 59, 0);
const minutes = DateCalculator.getMinutesSinceMidnight(date);
expect(minutes).toBe(1439);
});
it('should handle leap year', () => {
const date = new Date(2024, 1, 29); // February 29, 2024 (leap year)
const nextDay = DateCalculator.addDays(date, 1);
expect(nextDay.getDate()).toBe(1); // March 1
expect(nextDay.getMonth()).toBe(2);
});
it('should handle DST transitions', () => {
// This test depends on timezone, but we test the basic functionality
const beforeDST = new Date(2024, 2, 30); // March 30, 2024
const afterDST = DateCalculator.addDays(beforeDST, 1);
expect(afterDST.getDate()).toBe(31);
});
});
describe('Error Handling', () => {
it('should handle invalid dates gracefully', () => {
const invalidDate = new Date('invalid');
const result = DateCalculator.formatISODate(invalidDate);
expect(result).toBe('');
});
});
});