Centralizes time formatting with timezone support
Addresses inconsistent time formatting and lack of timezone handling throughout the application by introducing a `TimeFormatter` utility. This class centralizes time formatting logic, providing timezone conversion (defaults to Europe/Copenhagen) and support for both 12-hour and 24-hour formats, configurable via `CalendarConfig`. It also updates event rendering to utilize the new `TimeFormatter` for consistent time displays.
This commit is contained in:
parent
c07d83d86f
commit
8b96376d1f
7 changed files with 489 additions and 199 deletions
187
src/utils/TimeFormatter.ts
Normal file
187
src/utils/TimeFormatter.ts
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
/**
|
||||
* TimeFormatter - Centralized time formatting with timezone support
|
||||
*
|
||||
* Handles conversion from UTC/Zulu time to configured timezone (default: Europe/Copenhagen)
|
||||
* Supports both 12-hour and 24-hour format configuration
|
||||
*
|
||||
* All events in the system are stored in UTC and must be converted to local timezone
|
||||
*/
|
||||
|
||||
export interface TimeFormatSettings {
|
||||
timezone: string;
|
||||
use24HourFormat: boolean;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export class TimeFormatter {
|
||||
private static settings: TimeFormatSettings = {
|
||||
timezone: 'Europe/Copenhagen', // Default to Denmark
|
||||
use24HourFormat: true, // 24-hour format standard in Denmark
|
||||
locale: 'da-DK' // Danish locale
|
||||
};
|
||||
|
||||
/**
|
||||
* Configure time formatting settings
|
||||
*/
|
||||
static configure(settings: Partial<TimeFormatSettings>): void {
|
||||
TimeFormatter.settings = { ...TimeFormatter.settings, ...settings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current time format settings
|
||||
*/
|
||||
static getSettings(): TimeFormatSettings {
|
||||
return { ...TimeFormatter.settings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert UTC date to configured timezone
|
||||
* @param utcDate - Date in UTC (or assumed to be UTC)
|
||||
* @returns Date object adjusted to configured timezone
|
||||
*/
|
||||
static convertToLocalTime(utcDate: Date): Date {
|
||||
// Create a new date to avoid mutating the original
|
||||
const localDate = new Date(utcDate);
|
||||
|
||||
// If the date doesn't have timezone info, treat it as UTC
|
||||
// This handles cases where mock data doesn't have 'Z' suffix
|
||||
if (!utcDate.toISOString().endsWith('Z') && utcDate.getTimezoneOffset() === new Date().getTimezoneOffset()) {
|
||||
// Adjust for the fact that we're treating local time as UTC
|
||||
localDate.setMinutes(localDate.getMinutes() + localDate.getTimezoneOffset());
|
||||
}
|
||||
|
||||
return localDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timezone offset for configured timezone
|
||||
* @param date - Reference date for calculating offset (handles DST)
|
||||
* @returns Offset in minutes
|
||||
*/
|
||||
static getTimezoneOffset(date: Date = new Date()): number {
|
||||
const utc = new Date(date.getTime() + (date.getTimezoneOffset() * 60000));
|
||||
const targetTime = new Date(utc.toLocaleString('en-US', { timeZone: TimeFormatter.settings.timezone }));
|
||||
return (targetTime.getTime() - utc.getTime()) / 60000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time in 12-hour format
|
||||
* @param date - Date to format
|
||||
* @returns Formatted time string (e.g., "9:00 AM")
|
||||
*/
|
||||
static format12Hour(date: Date): string {
|
||||
const localDate = TimeFormatter.convertToLocalTime(date);
|
||||
|
||||
return localDate.toLocaleTimeString(TimeFormatter.settings.locale, {
|
||||
timeZone: TimeFormatter.settings.timezone,
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time in 24-hour format
|
||||
* @param date - Date to format
|
||||
* @returns Formatted time string (e.g., "09:00")
|
||||
*/
|
||||
static format24Hour(date: Date): string {
|
||||
const localDate = TimeFormatter.convertToLocalTime(date);
|
||||
|
||||
return localDate.toLocaleTimeString(TimeFormatter.settings.locale, {
|
||||
timeZone: TimeFormatter.settings.timezone,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time according to current configuration
|
||||
* @param date - Date to format
|
||||
* @returns Formatted time string
|
||||
*/
|
||||
static formatTime(date: Date): string {
|
||||
return TimeFormatter.settings.use24HourFormat
|
||||
? TimeFormatter.format24Hour(date)
|
||||
: TimeFormatter.format12Hour(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time from total minutes since midnight
|
||||
* @param totalMinutes - Minutes since midnight (e.g., 540 for 9:00 AM)
|
||||
* @returns Formatted time string
|
||||
*/
|
||||
static formatTimeFromMinutes(totalMinutes: number): string {
|
||||
const hours = Math.floor(totalMinutes / 60) % 24;
|
||||
const minutes = totalMinutes % 60;
|
||||
|
||||
// Create a date object for today with the specified time
|
||||
const date = new Date();
|
||||
date.setHours(hours, minutes, 0, 0);
|
||||
|
||||
return TimeFormatter.formatTime(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date and time together
|
||||
* @param date - Date to format
|
||||
* @returns Formatted date and time string
|
||||
*/
|
||||
static formatDateTime(date: Date): string {
|
||||
const localDate = TimeFormatter.convertToLocalTime(date);
|
||||
|
||||
const dateStr = localDate.toLocaleDateString(TimeFormatter.settings.locale, {
|
||||
timeZone: TimeFormatter.settings.timezone,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
});
|
||||
|
||||
const timeStr = TimeFormatter.formatTime(date);
|
||||
|
||||
return `${dateStr} ${timeStr}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time range (start - end)
|
||||
* @param startDate - Start date
|
||||
* @param endDate - End date
|
||||
* @returns Formatted time range string (e.g., "09:00 - 10:30")
|
||||
*/
|
||||
static formatTimeRange(startDate: Date, endDate: Date): string {
|
||||
const startTime = TimeFormatter.formatTime(startDate);
|
||||
const endTime = TimeFormatter.formatTime(endDate);
|
||||
return `${startTime} - ${endTime}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current timezone observes daylight saving time
|
||||
* @param date - Reference date
|
||||
* @returns True if DST is active
|
||||
*/
|
||||
static isDaylightSavingTime(date: Date = new Date()): boolean {
|
||||
const january = new Date(date.getFullYear(), 0, 1);
|
||||
const july = new Date(date.getFullYear(), 6, 1);
|
||||
|
||||
const janOffset = TimeFormatter.getTimezoneOffset(january);
|
||||
const julOffset = TimeFormatter.getTimezoneOffset(july);
|
||||
|
||||
return Math.max(janOffset, julOffset) !== TimeFormatter.getTimezoneOffset(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timezone abbreviation (e.g., "CET", "CEST")
|
||||
* @param date - Reference date
|
||||
* @returns Timezone abbreviation
|
||||
*/
|
||||
static getTimezoneAbbreviation(date: Date = new Date()): string {
|
||||
const localDate = TimeFormatter.convertToLocalTime(date);
|
||||
|
||||
return localDate.toLocaleTimeString('en-US', {
|
||||
timeZone: TimeFormatter.settings.timezone,
|
||||
timeZoneName: 'short'
|
||||
}).split(' ').pop() || '';
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue