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:
Janus Knudsen 2025-09-12 22:21:56 +02:00
parent c07d83d86f
commit 8b96376d1f
7 changed files with 489 additions and 199 deletions

View file

View file

@ -0,0 +1,216 @@
# TimeFormatter Specification
## Problem
- Alle events i systemet/mock JSON er i Zulu tid (UTC)
- Nuværende formatTime() metoder håndterer ikke timezone konvertering
- Ingen support for 12/24 timers format baseret på configuration
- Duplikeret formattering logik flere steder
## Løsning: Centraliseret TimeFormatter
### Requirements
1. **Timezone Support**
- Konverter fra UTC/Zulu til brugerens lokale timezone
- Respekter browser timezone settings
- Håndter sommertid korrekt
2. **12/24 Timer Format**
- Læs format præference fra CalendarConfig
- Support både 12-timer (AM/PM) og 24-timer format
- Gør det konfigurerbart per bruger
3. **Centralisering**
- Én enkelt kilde til al tidsformattering
- Konsistent formattering gennem hele applikationen
- Nem at teste og vedligeholde
### Proposed Implementation
```typescript
// src/utils/TimeFormatter.ts
export class TimeFormatter {
private static use24HourFormat: boolean = false;
private static userTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
/**
* Initialize formatter with user preferences
*/
static initialize(config: { use24Hour: boolean; timezone?: string }) {
this.use24HourFormat = config.use24Hour;
if (config.timezone) {
this.userTimezone = config.timezone;
}
}
/**
* Format UTC/Zulu time to local time with correct format
* @param input - UTC Date, ISO string, or minutes from midnight
* @returns Formatted time string in user's preferred format
*/
static formatTime(input: Date | string | number): string {
let date: Date;
if (typeof input === 'number') {
// Minutes from midnight - create date for today
const today = new Date();
today.setHours(0, 0, 0, 0);
today.setMinutes(input);
date = today;
} else if (typeof input === 'string') {
// ISO string - parse as UTC
date = new Date(input);
} else {
date = input;
}
// Convert to local timezone
const localDate = this.convertToLocalTime(date);
// Format based on user preference
if (this.use24HourFormat) {
return this.format24Hour(localDate);
} else {
return this.format12Hour(localDate);
}
}
/**
* Convert UTC date to local timezone
*/
private static convertToLocalTime(utcDate: Date): Date {
// Use Intl.DateTimeFormat for proper timezone conversion
const formatter = new Intl.DateTimeFormat('en-US', {
timeZone: this.userTimezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
const parts = formatter.formatToParts(utcDate);
const dateParts: any = {};
parts.forEach(part => {
dateParts[part.type] = part.value;
});
return new Date(
parseInt(dateParts.year),
parseInt(dateParts.month) - 1,
parseInt(dateParts.day),
parseInt(dateParts.hour),
parseInt(dateParts.minute),
parseInt(dateParts.second)
);
}
/**
* Format time in 24-hour format (HH:mm)
*/
private static format24Hour(date: Date): string {
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
}
/**
* Format time in 12-hour format (h:mm AM/PM)
*/
private static format12Hour(date: Date): string {
const hours = date.getHours();
const minutes = date.getMinutes();
const period = hours >= 12 ? 'PM' : 'AM';
const displayHours = hours > 12 ? hours - 12 : (hours === 0 ? 12 : hours);
return `${displayHours}:${minutes.toString().padStart(2, '0')} ${period}`;
}
/**
* Format date and time together
*/
static formatDateTime(date: Date | string): string {
const localDate = typeof date === 'string' ? new Date(date) : date;
const convertedDate = this.convertToLocalTime(localDate);
const dateStr = convertedDate.toLocaleDateString();
const timeStr = this.formatTime(convertedDate);
return `${dateStr} ${timeStr}`;
}
/**
* Get timezone offset in hours
*/
static getTimezoneOffset(): number {
return new Date().getTimezoneOffset() / -60;
}
}
```
### Configuration Integration
```typescript
// src/core/CalendarConfig.ts
export interface TimeFormatSettings {
use24HourFormat: boolean;
timezone?: string; // Optional override, defaults to browser timezone
}
// Add to CalendarConfig
getTimeFormatSettings(): TimeFormatSettings {
return {
use24HourFormat: this.config.use24HourFormat ?? false,
timezone: this.config.timezone // undefined = use browser default
};
}
```
### Usage Examples
```typescript
// Initialize on app start
TimeFormatter.initialize({
use24Hour: calendarConfig.getTimeFormatSettings().use24HourFormat,
timezone: calendarConfig.getTimeFormatSettings().timezone
});
// Format UTC event time to local
const utcEventTime = "2024-01-15T14:30:00Z"; // 2:30 PM UTC
const localTime = TimeFormatter.formatTime(utcEventTime);
// Result (Copenhagen, 24h): "15:30"
// Result (Copenhagen, 12h): "3:30 PM"
// Result (New York, 12h): "9:30 AM"
// Format minutes from midnight
const minutes = 570; // 9:30 AM
const formatted = TimeFormatter.formatTime(minutes);
// Result (24h): "09:30"
// Result (12h): "9:30 AM"
```
### Testing Considerations
1. Test timezone conversions:
- UTC to Copenhagen (UTC+1/+2)
- UTC to New York (UTC-5/-4)
- UTC to Tokyo (UTC+9)
2. Test daylight saving transitions
3. Test 12/24 hour format switching
4. Test edge cases:
- Midnight (00:00 / 12:00 AM)
- Noon (12:00 / 12:00 PM)
- Events spanning multiple days
### Migration Plan
1. Implement TimeFormatter class
2. Add configuration options to CalendarConfig
3. Replace all existing formatTime() calls
4. Update mock data loader to handle UTC properly
5. Test thoroughly with different timezones

View file

@ -3,6 +3,7 @@
import { eventBus } from './EventBus'; import { eventBus } from './EventBus';
import { CoreEvents } from '../constants/CoreEvents'; import { CoreEvents } from '../constants/CoreEvents';
import { CalendarConfig as ICalendarConfig, ViewPeriod, CalendarMode } from '../types/CalendarTypes'; import { CalendarConfig as ICalendarConfig, ViewPeriod, CalendarMode } from '../types/CalendarTypes';
import { TimeFormatter, TimeFormatSettings } from '../utils/TimeFormatter';
/** /**
* All-day event layout constants * All-day event layout constants
@ -69,6 +70,15 @@ interface ResourceViewSettings {
showAllDay: boolean; // Show all-day event row showAllDay: boolean; // Show all-day event row
} }
/**
* Time format configuration settings
*/
interface TimeFormatConfig {
timezone: string;
use24HourFormat: boolean;
locale: string;
}
/** /**
* Calendar configuration management * Calendar configuration management
*/ */
@ -80,6 +90,7 @@ export class CalendarConfig {
private dateViewSettings: DateViewSettings; private dateViewSettings: DateViewSettings;
private resourceViewSettings: ResourceViewSettings; private resourceViewSettings: ResourceViewSettings;
private currentWorkWeek: string = 'standard'; private currentWorkWeek: string = 'standard';
private timeFormatConfig: TimeFormatConfig;
constructor() { constructor() {
this.config = { this.config = {
@ -142,9 +153,19 @@ export class CalendarConfig {
showAllDay: true showAllDay: true
}; };
// Time format settings - default to Denmark
this.timeFormatConfig = {
timezone: 'Europe/Copenhagen',
use24HourFormat: true,
locale: 'da-DK'
};
// Set computed values // Set computed values
this.config.minEventDuration = this.gridSettings.snapInterval; this.config.minEventDuration = this.gridSettings.snapInterval;
// Initialize TimeFormatter with default settings
TimeFormatter.configure(this.timeFormatConfig);
// Load calendar type from URL parameter // Load calendar type from URL parameter
this.loadCalendarType(); this.loadCalendarType();
@ -472,6 +493,57 @@ export class CalendarConfig {
return this.currentWorkWeek; return this.currentWorkWeek;
} }
/**
* Get time format settings
*/
getTimeFormatSettings(): TimeFormatConfig {
return { ...this.timeFormatConfig };
}
/**
* Update time format settings
*/
updateTimeFormatSettings(updates: Partial<TimeFormatConfig>): void {
this.timeFormatConfig = { ...this.timeFormatConfig, ...updates };
// Update TimeFormatter with new settings
TimeFormatter.configure(this.timeFormatConfig);
// Emit time format change event
eventBus.emit(CoreEvents.REFRESH_REQUESTED, {
key: 'timeFormatSettings',
value: this.timeFormatConfig
});
}
/**
* Set timezone (convenience method)
*/
setTimezone(timezone: string): void {
this.updateTimeFormatSettings({ timezone });
}
/**
* Set 12/24 hour format (convenience method)
*/
set24HourFormat(use24Hour: boolean): void {
this.updateTimeFormatSettings({ use24HourFormat: use24Hour });
}
/**
* Get configured timezone
*/
getTimezone(): string {
return this.timeFormatConfig.timezone;
}
/**
* Check if using 24-hour format
*/
is24HourFormat(): boolean {
return this.timeFormatConfig.use24HourFormat;
}
} }
// Create singleton instance // Create singleton instance

View file

@ -1,5 +1,6 @@
import { CalendarEvent } from '../types/CalendarTypes'; import { CalendarEvent } from '../types/CalendarTypes';
import { calendarConfig } from '../core/CalendarConfig'; import { calendarConfig } from '../core/CalendarConfig';
import { TimeFormatter } from '../utils/TimeFormatter';
/** /**
* Abstract base class for event DOM elements * Abstract base class for event DOM elements
@ -39,14 +40,10 @@ export abstract class BaseEventElement {
} }
/** /**
* Format time for display * Format time for display using TimeFormatter
*/ */
protected formatTime(date: Date): string { protected formatTime(date: Date): string {
const hours = date.getHours(); return TimeFormatter.formatTime(date);
const minutes = date.getMinutes();
const period = hours >= 12 ? 'PM' : 'AM';
const displayHours = hours > 12 ? hours - 12 : (hours === 0 ? 12 : hours);
return `${displayHours}:${minutes.toString().padStart(2, '0')} ${period}`;
} }
/** /**
@ -87,12 +84,11 @@ export class SwpEventElement extends BaseEventElement {
* Create inner HTML structure * Create inner HTML structure
*/ */
private createInnerStructure(): void { private createInnerStructure(): void {
const startTime = this.formatTime(this.event.start); const timeRange = TimeFormatter.formatTimeRange(this.event.start, this.event.end);
const endTime = this.formatTime(this.event.end);
const durationMinutes = (this.event.end.getTime() - this.event.start.getTime()) / (1000 * 60); const durationMinutes = (this.event.end.getTime() - this.event.start.getTime()) / (1000 * 60);
this.element.innerHTML = ` this.element.innerHTML = `
<swp-event-time data-duration="${durationMinutes}">${startTime} - ${endTime}</swp-event-time> <swp-event-time data-duration="${durationMinutes}">${timeRange}</swp-event-time>
<swp-event-title>${this.event.title}</swp-event-title> <swp-event-title>${this.event.title}</swp-event-title>
`; `;
} }

View file

@ -10,165 +10,6 @@ import { DateCalculator } from '../utils/DateCalculator';
*/ */
export class AllDayEventRenderer { export class AllDayEventRenderer {
/**
* Render all-day events in the header container
*/
public renderAllDayEvents(events: CalendarEvent[], container: HTMLElement): void {
const allDayEvents = events.filter(event => event.allDay);
// Find the calendar header
const calendarHeader = container.querySelector('swp-calendar-header');
if (!calendarHeader) {
return;
}
// Find or create all-day container
let allDayContainer = calendarHeader.querySelector('swp-allday-container') as HTMLElement;
if (!allDayContainer) {
allDayContainer = document.createElement('swp-allday-container');
calendarHeader.appendChild(allDayContainer);
}
// Clear existing events
allDayContainer.innerHTML = '';
if (allDayEvents.length === 0) {
return;
}
// Build date to column mapping
const dayHeaders = calendarHeader.querySelectorAll('swp-day-header');
const dateToColumnMap = new Map<string, number>();
dayHeaders.forEach((header, index) => {
const dateStr = (header as HTMLElement).dataset.date;
if (dateStr) {
dateToColumnMap.set(dateStr, index + 1);
}
});
// Calculate grid positioning for events
const eventPlacements = this.calculateEventPlacements(allDayEvents, dateToColumnMap);
// Render events using factory pattern
eventPlacements.forEach(({ event, gridColumn, gridRow }) => {
const eventDateStr = DateCalculator.formatISODate(event.start);
const swpAllDayEvent = SwpAllDayEventElement.fromCalendarEvent(event, eventDateStr);
const allDayElement = swpAllDayEvent.getElement();
// Apply grid positioning
(allDayElement as HTMLElement).style.gridColumn = gridColumn;
(allDayElement as HTMLElement).style.gridRow = gridRow.toString();
// Use event metadata for color if available
if (event.metadata?.color) {
(allDayElement as HTMLElement).style.backgroundColor = event.metadata.color;
}
allDayContainer.appendChild(allDayElement);
});
}
/**
* Calculate grid positioning for all-day events with overlap detection
*/
private calculateEventPlacements(events: CalendarEvent[], dateToColumnMap: Map<string, number>) {
// Calculate spans for each event
const eventItems = events.map(event => {
const eventDateStr = DateCalculator.formatISODate(event.start);
const endDateStr = DateCalculator.formatISODate(event.end);
const startColumn = dateToColumnMap.get(eventDateStr);
const endColumn = dateToColumnMap.get(endDateStr);
if (startColumn === undefined) {
return null;
}
const columnSpan = endColumn !== undefined && endColumn >= startColumn
? endColumn - startColumn + 1
: 1;
return {
event,
span: {
startColumn: startColumn,
columnSpan: columnSpan
}
};
}).filter(item => item !== null) as Array<{
event: CalendarEvent;
span: { startColumn: number; columnSpan: number };
}>;
// Calculate row placement to avoid overlaps
interface EventPlacement {
event: CalendarEvent;
gridColumn: string;
gridRow: number;
}
const eventPlacements: EventPlacement[] = [];
eventItems.forEach(eventItem => {
let assignedRow = 1;
// Find first available row
while (true) {
// Check if this row has any conflicts
const rowEvents = eventPlacements.filter(p => p.gridRow === assignedRow);
const hasOverlap = rowEvents.some(rowEvent => {
// Parse the existing grid column to check overlap
const existingSpan = this.parseGridColumn(rowEvent.gridColumn);
return this.spansOverlap(eventItem.span, existingSpan);
});
if (!hasOverlap) {
break; // Found available row
}
assignedRow++;
}
const gridColumn = eventItem.span.columnSpan > 1
? `${eventItem.span.startColumn} / span ${eventItem.span.columnSpan}`
: `${eventItem.span.startColumn}`;
eventPlacements.push({
event: eventItem.event,
gridColumn,
gridRow: assignedRow
});
});
return eventPlacements;
}
/**
* Check if two column spans overlap
*/
private spansOverlap(span1: { startColumn: number; columnSpan: number }, span2: { startColumn: number; columnSpan: number }): boolean {
const span1End = span1.startColumn + span1.columnSpan - 1;
const span2End = span2.startColumn + span2.columnSpan - 1;
return !(span1End < span2.startColumn || span2End < span1.startColumn);
}
/**
* Parse grid column string back to span object
*/
private parseGridColumn(gridColumn: string): { startColumn: number; columnSpan: number } {
if (gridColumn.includes('span')) {
const parts = gridColumn.split(' / span ');
return {
startColumn: parseInt(parts[0]),
columnSpan: parseInt(parts[1])
};
} else {
return {
startColumn: parseInt(gridColumn),
columnSpan: 1
};
}
}
} }

View file

@ -7,6 +7,7 @@ import { eventBus } from '../core/EventBus';
import { CoreEvents } from '../constants/CoreEvents'; import { CoreEvents } from '../constants/CoreEvents';
import { OverlapDetector, OverlapResult, EventId } from '../utils/OverlapDetector'; import { OverlapDetector, OverlapResult, EventId } from '../utils/OverlapDetector';
import { SwpEventElement, SwpAllDayEventElement } from '../elements/SwpEventElement'; import { SwpEventElement, SwpAllDayEventElement } from '../elements/SwpEventElement';
import { TimeFormatter } from '../utils/TimeFormatter';
/** /**
* Interface for event rendering strategies * Interface for event rendering strategies
@ -184,12 +185,11 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
* Create event inner structure (swp-event-time and swp-event-title) * Create event inner structure (swp-event-time and swp-event-title)
*/ */
private createEventInnerStructure(event: CalendarEvent): string { private createEventInnerStructure(event: CalendarEvent): string {
const startTime = this.formatTime(event.start); const timeRange = TimeFormatter.formatTimeRange(event.start, event.end);
const endTime = this.formatTime(event.end);
const durationMinutes = (event.end.getTime() - event.start.getTime()) / (1000 * 60); const durationMinutes = (event.end.getTime() - event.start.getTime()) / (1000 * 60);
return ` return `
<swp-event-time data-duration="${durationMinutes}">${startTime} - ${endTime}</swp-event-time> <swp-event-time data-duration="${durationMinutes}">${timeRange}</swp-event-time>
<swp-event-title>${event.title}</swp-event-title> <swp-event-title>${event.title}</swp-event-title>
`; `;
} }
@ -269,33 +269,12 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
// Update display // Update display
const timeElement = clone.querySelector('swp-event-time'); const timeElement = clone.querySelector('swp-event-time');
if (timeElement) { if (timeElement) {
const newTimeText = `${this.formatTime(snappedStartMinutes)} - ${this.formatTime(endTotalMinutes)}`; const startTime = TimeFormatter.formatTimeFromMinutes(snappedStartMinutes);
timeElement.textContent = newTimeText; const endTime = TimeFormatter.formatTimeFromMinutes(endTotalMinutes);
timeElement.textContent = `${startTime} - ${endTime}`;
} }
} }
/**
* Unified time formatting method - handles both total minutes and Date objects
*/
private formatTime(input: number | Date | string): string {
let hours: number, minutes: number;
if (typeof input === 'number') {
// Total minutes input
hours = Math.floor(input / 60) % 24;
minutes = input % 60;
} else {
// Date or ISO string input
const date = typeof input === 'string' ? new Date(input) : input;
hours = date.getHours();
minutes = date.getMinutes();
}
const period = hours >= 12 ? 'PM' : 'AM';
const displayHours = hours > 12 ? hours - 12 : (hours === 0 ? 12 : hours);
return `${displayHours}:${minutes.toString().padStart(2, '0')} ${period}`;
}
/** /**
* Handle drag start event * Handle drag start event
*/ */
@ -590,9 +569,8 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
// Update the time display // Update the time display
const timeElement = element.querySelector('swp-event-time'); const timeElement = element.querySelector('swp-event-time');
if (timeElement) { if (timeElement) {
const startTime = this.formatTime(event.start); const timeRange = TimeFormatter.formatTimeRange(event.start, event.end);
const endTime = this.formatTime(event.end); timeElement.textContent = timeRange;
timeElement.textContent = `${startTime} - ${endTime}`;
} }
} }

187
src/utils/TimeFormatter.ts Normal file
View 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() || '';
}
}