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

@ -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