95 lines
3.0 KiB
TypeScript
95 lines
3.0 KiB
TypeScript
/**
|
|
* Centralized date/time formatting utilities.
|
|
*
|
|
* All formatters are timezone-aware, using Intl.DateTimeFormat.
|
|
* Consolidates duplicated formatUtcTimestamp / formatZonedTimestamp / resolveExplicitTimezone
|
|
* that previously lived in envelope.ts and session-updates.ts.
|
|
*/
|
|
|
|
/**
|
|
* Validate an IANA timezone string. Returns the string if valid, undefined otherwise.
|
|
*/
|
|
export function resolveTimezone(value: string): string | undefined {
|
|
try {
|
|
new Intl.DateTimeFormat("en-US", { timeZone: value }).format(new Date());
|
|
return value;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
export type FormatTimestampOptions = {
|
|
/** Include seconds in the output. Default: false */
|
|
displaySeconds?: boolean;
|
|
};
|
|
|
|
export type FormatZonedTimestampOptions = FormatTimestampOptions & {
|
|
/** IANA timezone string (e.g., 'America/New_York'). Default: system timezone */
|
|
timeZone?: string;
|
|
};
|
|
|
|
/**
|
|
* Format a Date as a UTC timestamp string.
|
|
*
|
|
* Without seconds: `2024-01-15T14:30Z`
|
|
* With seconds: `2024-01-15T14:30:05Z`
|
|
*/
|
|
export function formatUtcTimestamp(date: Date, options?: FormatTimestampOptions): string {
|
|
const yyyy = String(date.getUTCFullYear()).padStart(4, "0");
|
|
const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
const dd = String(date.getUTCDate()).padStart(2, "0");
|
|
const hh = String(date.getUTCHours()).padStart(2, "0");
|
|
const min = String(date.getUTCMinutes()).padStart(2, "0");
|
|
if (!options?.displaySeconds) {
|
|
return `${yyyy}-${mm}-${dd}T${hh}:${min}Z`;
|
|
}
|
|
const sec = String(date.getUTCSeconds()).padStart(2, "0");
|
|
return `${yyyy}-${mm}-${dd}T${hh}:${min}:${sec}Z`;
|
|
}
|
|
|
|
/**
|
|
* Format a Date with timezone display using Intl.DateTimeFormat.
|
|
*
|
|
* Without seconds: `2024-01-15 14:30 EST`
|
|
* With seconds: `2024-01-15 14:30:05 EST`
|
|
*
|
|
* Returns undefined if Intl formatting fails.
|
|
*/
|
|
export function formatZonedTimestamp(
|
|
date: Date,
|
|
options?: FormatZonedTimestampOptions,
|
|
): string | undefined {
|
|
const intlOptions: Intl.DateTimeFormatOptions = {
|
|
timeZone: options?.timeZone,
|
|
year: "numeric",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
hourCycle: "h23",
|
|
timeZoneName: "short",
|
|
};
|
|
if (options?.displaySeconds) {
|
|
intlOptions.second = "2-digit";
|
|
}
|
|
const parts = new Intl.DateTimeFormat("en-US", intlOptions).formatToParts(date);
|
|
const pick = (type: string) => parts.find((part) => part.type === type)?.value;
|
|
const yyyy = pick("year");
|
|
const mm = pick("month");
|
|
const dd = pick("day");
|
|
const hh = pick("hour");
|
|
const min = pick("minute");
|
|
const sec = options?.displaySeconds ? pick("second") : undefined;
|
|
const tz = [...parts]
|
|
.toReversed()
|
|
.find((part) => part.type === "timeZoneName")
|
|
?.value?.trim();
|
|
if (!yyyy || !mm || !dd || !hh || !min) {
|
|
return undefined;
|
|
}
|
|
if (options?.displaySeconds && sec) {
|
|
return `${yyyy}-${mm}-${dd} ${hh}:${min}:${sec}${tz ? ` ${tz}` : ""}`;
|
|
}
|
|
return `${yyyy}-${mm}-${dd} ${hh}:${min}${tz ? ` ${tz}` : ""}`;
|
|
}
|