52 lines
2.5 KiB
TypeScript
52 lines
2.5 KiB
TypeScript
export const DEFAULT_TIME_ZONE = 'America/New_York';
|
|
|
|
export function validTimeZone(value: unknown): value is string {
|
|
if (typeof value !== 'string' || !value.trim()) return false;
|
|
try {
|
|
new Intl.DateTimeFormat('en-US', { timeZone: value });
|
|
return true;
|
|
} catch { return false; }
|
|
}
|
|
|
|
export function availableTimeZones(): string[] {
|
|
// Older browsers can still select common zones when enumeration is unavailable.
|
|
const fallback = [DEFAULT_TIME_ZONE, 'America/Chicago', 'America/Denver', 'America/Los_Angeles',
|
|
'America/Anchorage', 'Pacific/Honolulu', 'Europe/London', 'Europe/Paris',
|
|
'Asia/Kolkata', 'Asia/Tokyo', 'Australia/Sydney'];
|
|
const zones = typeof Intl.supportedValuesOf === 'function' ? Intl.supportedValuesOf('timeZone') : fallback;
|
|
return [...new Set([DEFAULT_TIME_ZONE, 'UTC', ...zones])].sort();
|
|
}
|
|
|
|
type Timestamp = string | number | Date | null | undefined;
|
|
|
|
/** Database timestamps without an offset are UTC, never browser-local time. */
|
|
function parseTimestamp(value: Exclude<Timestamp, null | undefined>): Date {
|
|
if (value instanceof Date) return value;
|
|
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(\.\d+)?$/.test(value)) {
|
|
return new Date(value.replace(' ', 'T') + 'Z');
|
|
}
|
|
return new Date(value);
|
|
}
|
|
|
|
export function createTimeFormatter(zone: string) {
|
|
const timeZone = validTimeZone(zone) ? zone : DEFAULT_TIME_ZONE;
|
|
const date: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'short', day: 'numeric' };
|
|
const time: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: '2-digit', second: '2-digit', timeZoneName: 'short' };
|
|
function formatter(options: Intl.DateTimeFormatOptions) {
|
|
const format = new Intl.DateTimeFormat('en-US', { ...options, timeZone });
|
|
return (value: Timestamp): string => {
|
|
if (value === null || value === undefined || value === '') return '—';
|
|
const parsed = parseTimestamp(value);
|
|
return Number.isFinite(parsed.getTime()) ? format.format(parsed) : '—';
|
|
};
|
|
}
|
|
return {
|
|
dateTime: formatter({ ...date, ...time }),
|
|
date: formatter(date),
|
|
time: formatter(time),
|
|
clockTime: formatter({ hour: 'numeric', minute: '2-digit', second: '2-digit' }),
|
|
minuteTime: formatter({ hour: 'numeric', minute: '2-digit', timeZoneName: 'short' }),
|
|
period: formatter({ ...date, hour: 'numeric', minute: '2-digit', timeZoneName: 'short' })
|
|
};
|
|
}
|