time-service.ts•6.66 kB
import { TimezoneInfo, TimeConversionResult } from './interfaces.js';
export class TimeService {
private static readonly TIMEZONE_REGEX = /^[A-Za-z_]+\/[A-Za-z_]+(?:\/[A-Za-z_]+)*$/;
private static readonly TIME_REGEX = /^([01]?[0-9]|2[0-3]):([0-5][0-9])(?::([0-5][0-9]))?$/;
static validateTimezone(timezone: string): void {
if (!this.TIMEZONE_REGEX.test(timezone)) {
throw new Error(`Invalid timezone format: ${timezone}. Use IANA timezone names like 'America/New_York'`);
}
try {
new Date().toLocaleString('en-US', { timeZone: timezone });
} catch (error) {
throw new Error(`Unsupported timezone: ${timezone}`);
}
}
static validateTime(time: string): void {
if (!this.TIME_REGEX.test(time)) {
throw new Error(`Invalid time format: ${time}. Use HH:MM or HH:MM:SS format`);
}
}
static getSystemTimezone(): string {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
}
static getCurrentTime(timezone: string, format: 'iso' | 'local' | 'full' = 'iso'): TimezoneInfo {
this.validateTimezone(timezone);
const now = new Date();
const isDST = this.isDaylightSavingTime(now, timezone);
const utcOffset = this.getUTCOffset(now, timezone);
let datetime: string;
let localizedFormat: string;
switch (format) {
case 'iso':
datetime = new Date(now.toLocaleString('en-US', { timeZone: timezone })).toISOString();
localizedFormat = datetime;
break;
case 'local':
datetime = now.toLocaleString('en-US', {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
localizedFormat = datetime;
break;
case 'full':
datetime = now.toLocaleString('en-US', { timeZone: timezone });
localizedFormat = now.toLocaleString('en-US', {
timeZone: timezone,
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
timeZoneName: 'long'
});
break;
}
return {
timezone,
datetime,
isDST,
utcOffset,
localizedFormat
};
}
static convertTime(
sourceTimezone: string,
targetTimezone: string,
time: string,
date?: string
): TimeConversionResult {
this.validateTimezone(sourceTimezone);
this.validateTimezone(targetTimezone);
this.validateTime(time);
const baseDate = date ? new Date(date) : new Date();
const [hours, minutes, seconds = 0] = time.split(':').map(Number);
const sourceDate = new Date(baseDate);
sourceDate.setHours(hours, minutes, seconds, 0);
const sourceUTC = new Date(sourceDate.toLocaleString('en-US', { timeZone: 'UTC' }));
const sourceLocal = new Date(sourceDate.toLocaleString('en-US', { timeZone: sourceTimezone }));
const offset = sourceUTC.getTime() - sourceLocal.getTime();
const adjustedDate = new Date(sourceDate.getTime() + offset);
const targetDate = new Date(adjustedDate.toLocaleString('en-US', { timeZone: targetTimezone }));
const sourceInfo: TimezoneInfo = {
timezone: sourceTimezone,
datetime: sourceDate.toLocaleString('en-US', {
timeZone: sourceTimezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}),
isDST: this.isDaylightSavingTime(sourceDate, sourceTimezone),
utcOffset: this.getUTCOffset(sourceDate, sourceTimezone),
localizedFormat: sourceDate.toLocaleString('en-US', {
timeZone: sourceTimezone,
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
timeZoneName: 'short'
})
};
const targetInfo: TimezoneInfo = {
timezone: targetTimezone,
datetime: targetDate.toLocaleString('en-US', {
timeZone: targetTimezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}),
isDST: this.isDaylightSavingTime(adjustedDate, targetTimezone),
utcOffset: this.getUTCOffset(adjustedDate, targetTimezone),
localizedFormat: adjustedDate.toLocaleString('en-US', {
timeZone: targetTimezone,
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
timeZoneName: 'short'
})
};
const timeDifference = this.calculateTimeDifference(
new Date(`2000-01-01T${time}`),
targetDate
);
return {
source: sourceInfo,
target: targetInfo,
timeDifference
};
}
private static isDaylightSavingTime(date: Date, timezone: string): boolean {
const jan = new Date(date.getFullYear(), 0, 1);
const jul = new Date(date.getFullYear(), 6, 1);
const janOffset = this.getTimezoneOffset(jan, timezone);
const julOffset = this.getTimezoneOffset(jul, timezone);
const currentOffset = this.getTimezoneOffset(date, timezone);
return Math.max(janOffset, julOffset) === currentOffset;
}
private static getTimezoneOffset(date: Date, timezone: string): number {
const utc = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' }));
const target = new Date(date.toLocaleString('en-US', { timeZone: timezone }));
return (utc.getTime() - target.getTime()) / (1000 * 60);
}
private static getUTCOffset(date: Date, timezone: string): string {
const offsetMinutes = this.getTimezoneOffset(date, timezone);
const sign = offsetMinutes >= 0 ? '+' : '-';
const absMinutes = Math.abs(offsetMinutes);
const hours = Math.floor(absMinutes / 60);
const minutes = absMinutes % 60;
return `${sign}${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
}
private static calculateTimeDifference(sourceTime: Date, targetTime: Date): string {
const diffMs = Math.abs(targetTime.getTime() - sourceTime.getTime());
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
const sign = targetTime.getTime() >= sourceTime.getTime() ? '+' : '-';
return `${sign}${diffHours}h ${diffMinutes}m`;
}
}