293 lines
8 KiB
TypeScript
293 lines
8 KiB
TypeScript
|
|
/**
|
||
|
|
* DateService - Unified date/time service using date-fns
|
||
|
|
* Handles all date operations, timezone conversions, and formatting
|
||
|
|
*/
|
||
|
|
|
||
|
|
import {
|
||
|
|
format,
|
||
|
|
parse,
|
||
|
|
addMinutes,
|
||
|
|
differenceInMinutes,
|
||
|
|
startOfDay,
|
||
|
|
endOfDay,
|
||
|
|
setHours,
|
||
|
|
setMinutes as setMins,
|
||
|
|
getHours,
|
||
|
|
getMinutes,
|
||
|
|
parseISO,
|
||
|
|
isValid,
|
||
|
|
addDays,
|
||
|
|
startOfWeek,
|
||
|
|
endOfWeek,
|
||
|
|
addWeeks,
|
||
|
|
isSameDay
|
||
|
|
} from 'date-fns';
|
||
|
|
import {
|
||
|
|
toZonedTime,
|
||
|
|
fromZonedTime,
|
||
|
|
formatInTimeZone
|
||
|
|
} from 'date-fns-tz';
|
||
|
|
|
||
|
|
export class DateService {
|
||
|
|
private timezone: string;
|
||
|
|
|
||
|
|
constructor(timezone: string = 'Europe/Copenhagen') {
|
||
|
|
this.timezone = timezone;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// CORE CONVERSIONS
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Convert local date to UTC ISO string
|
||
|
|
* @param localDate - Date in local timezone
|
||
|
|
* @returns ISO string in UTC (with 'Z' suffix)
|
||
|
|
*/
|
||
|
|
public toUTC(localDate: Date): string {
|
||
|
|
return fromZonedTime(localDate, this.timezone).toISOString();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Convert UTC ISO string to local date
|
||
|
|
* @param utcString - ISO string in UTC
|
||
|
|
* @returns Date in local timezone
|
||
|
|
*/
|
||
|
|
public fromUTC(utcString: string): Date {
|
||
|
|
return toZonedTime(parseISO(utcString), this.timezone);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// FORMATTING
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Format time as HH:mm or HH:mm:ss
|
||
|
|
* @param date - Date to format
|
||
|
|
* @param showSeconds - Include seconds in output
|
||
|
|
* @returns Formatted time string
|
||
|
|
*/
|
||
|
|
public formatTime(date: Date, showSeconds = false): string {
|
||
|
|
const pattern = showSeconds ? 'HH:mm:ss' : 'HH:mm';
|
||
|
|
return format(date, pattern);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Format time range as "HH:mm - HH:mm"
|
||
|
|
* @param start - Start date
|
||
|
|
* @param end - End date
|
||
|
|
* @returns Formatted time range
|
||
|
|
*/
|
||
|
|
public formatTimeRange(start: Date, end: Date): string {
|
||
|
|
return `${this.formatTime(start)} - ${this.formatTime(end)}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Format date and time in technical format: yyyy-MM-dd HH:mm:ss
|
||
|
|
* @param date - Date to format
|
||
|
|
* @returns Technical datetime string
|
||
|
|
*/
|
||
|
|
public formatTechnicalDateTime(date: Date): string {
|
||
|
|
return format(date, 'yyyy-MM-dd HH:mm:ss');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Format date as yyyy-MM-dd
|
||
|
|
* @param date - Date to format
|
||
|
|
* @returns ISO date string
|
||
|
|
*/
|
||
|
|
public formatDate(date: Date): string {
|
||
|
|
return format(date, 'yyyy-MM-dd');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Format date as ISO string (same as formatDate for compatibility)
|
||
|
|
* @param date - Date to format
|
||
|
|
* @returns ISO date string
|
||
|
|
*/
|
||
|
|
public formatISODate(date: Date): string {
|
||
|
|
return this.formatDate(date);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// TIME CALCULATIONS
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Convert time string (HH:mm or HH:mm:ss) to total minutes since midnight
|
||
|
|
* @param timeString - Time in format HH:mm or HH:mm:ss
|
||
|
|
* @returns Total minutes since midnight
|
||
|
|
*/
|
||
|
|
public timeToMinutes(timeString: string): number {
|
||
|
|
const parts = timeString.split(':').map(Number);
|
||
|
|
const hours = parts[0] || 0;
|
||
|
|
const minutes = parts[1] || 0;
|
||
|
|
return hours * 60 + minutes;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Convert total minutes since midnight to time string HH:mm
|
||
|
|
* @param totalMinutes - Minutes since midnight
|
||
|
|
* @returns Time string in format HH:mm
|
||
|
|
*/
|
||
|
|
public minutesToTime(totalMinutes: number): string {
|
||
|
|
const hours = Math.floor(totalMinutes / 60);
|
||
|
|
const minutes = totalMinutes % 60;
|
||
|
|
const date = setMins(setHours(new Date(), hours), minutes);
|
||
|
|
return format(date, 'HH:mm');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Format time from total minutes (alias for minutesToTime)
|
||
|
|
* @param totalMinutes - Minutes since midnight
|
||
|
|
* @returns Time string in format HH:mm
|
||
|
|
*/
|
||
|
|
public formatTimeFromMinutes(totalMinutes: number): string {
|
||
|
|
return this.minutesToTime(totalMinutes);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get minutes since midnight for a given date
|
||
|
|
* @param date - Date to calculate from
|
||
|
|
* @returns Minutes since midnight
|
||
|
|
*/
|
||
|
|
public getMinutesSinceMidnight(date: Date): number {
|
||
|
|
return getHours(date) * 60 + getMinutes(date);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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
|
||
|
|
*/
|
||
|
|
public getDurationMinutes(start: Date | string, end: Date | string): number {
|
||
|
|
const startDate = typeof start === 'string' ? parseISO(start) : start;
|
||
|
|
const endDate = typeof end === 'string' ? parseISO(end) : end;
|
||
|
|
return differenceInMinutes(endDate, startDate);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// WEEK OPERATIONS
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get start and end of week (Monday to Sunday)
|
||
|
|
* @param date - Reference date
|
||
|
|
* @returns Object with start and end dates
|
||
|
|
*/
|
||
|
|
public getWeekBounds(date: Date): { start: Date; end: Date } {
|
||
|
|
return {
|
||
|
|
start: startOfWeek(date, { weekStartsOn: 1 }), // Monday
|
||
|
|
end: endOfWeek(date, { weekStartsOn: 1 }) // Sunday
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Add weeks to a date
|
||
|
|
* @param date - Base date
|
||
|
|
* @param weeks - Number of weeks to add (can be negative)
|
||
|
|
* @returns New date
|
||
|
|
*/
|
||
|
|
public addWeeks(date: Date, weeks: number): Date {
|
||
|
|
return addWeeks(date, weeks);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// GRID HELPERS
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a date at a specific time (minutes since midnight)
|
||
|
|
* @param baseDate - Base date (date component)
|
||
|
|
* @param totalMinutes - Minutes since midnight
|
||
|
|
* @returns New date with specified time
|
||
|
|
*/
|
||
|
|
public createDateAtTime(baseDate: Date, totalMinutes: number): Date {
|
||
|
|
const hours = Math.floor(totalMinutes / 60);
|
||
|
|
const minutes = totalMinutes % 60;
|
||
|
|
return setMins(setHours(startOfDay(baseDate), hours), minutes);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Snap date to nearest interval
|
||
|
|
* @param date - Date to snap
|
||
|
|
* @param intervalMinutes - Snap interval in minutes
|
||
|
|
* @returns Snapped date
|
||
|
|
*/
|
||
|
|
public snapToInterval(date: Date, intervalMinutes: number): Date {
|
||
|
|
const minutes = this.getMinutesSinceMidnight(date);
|
||
|
|
const snappedMinutes = Math.round(minutes / intervalMinutes) * intervalMinutes;
|
||
|
|
return this.createDateAtTime(date, snappedMinutes);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// UTILITY METHODS
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if two dates are the same day
|
||
|
|
* @param date1 - First date
|
||
|
|
* @param date2 - Second date
|
||
|
|
* @returns True if same day
|
||
|
|
*/
|
||
|
|
public isSameDay(date1: Date, date2: Date): boolean {
|
||
|
|
return isSameDay(date1, date2);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get start of day
|
||
|
|
* @param date - Date
|
||
|
|
* @returns Start of day (00:00:00)
|
||
|
|
*/
|
||
|
|
public startOfDay(date: Date): Date {
|
||
|
|
return startOfDay(date);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get end of day
|
||
|
|
* @param date - Date
|
||
|
|
* @returns End of day (23:59:59.999)
|
||
|
|
*/
|
||
|
|
public endOfDay(date: Date): Date {
|
||
|
|
return endOfDay(date);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Add days to a date
|
||
|
|
* @param date - Base date
|
||
|
|
* @param days - Number of days to add (can be negative)
|
||
|
|
* @returns New date
|
||
|
|
*/
|
||
|
|
public addDays(date: Date, days: number): Date {
|
||
|
|
return addDays(date, days);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Add minutes to a date
|
||
|
|
* @param date - Base date
|
||
|
|
* @param minutes - Number of minutes to add (can be negative)
|
||
|
|
* @returns New date
|
||
|
|
*/
|
||
|
|
public addMinutes(date: Date, minutes: number): Date {
|
||
|
|
return addMinutes(date, minutes);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Parse ISO string to date
|
||
|
|
* @param isoString - ISO date string
|
||
|
|
* @returns Parsed date
|
||
|
|
*/
|
||
|
|
public parseISO(isoString: string): Date {
|
||
|
|
return parseISO(isoString);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if date is valid
|
||
|
|
* @param date - Date to check
|
||
|
|
* @returns True if valid
|
||
|
|
*/
|
||
|
|
public isValid(date: Date): boolean {
|
||
|
|
return isValid(date);
|
||
|
|
}
|
||
|
|
}
|