2026-09-24 00:08:58 -04:00
|
|
|
|
/**
|
|
|
|
|
|
* Date/time formatting for the Today tab.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Calendar dates ("YYYY-MM-DD") are parsed by their components and formatted
|
|
|
|
|
|
* in UTC, so the printed day never shifts with the viewer's offset — unlike
|
|
|
|
|
|
* `new Date("YYYY-MM-DD")`, which is UTC midnight and reads as the previous
|
2026-09-30 09:50:25 -04:00
|
|
|
|
* day in negative-offset zones. Instants are shown in the application
|
|
|
|
|
|
* timezone (time/zone.ts).
|
2026-09-24 00:08:58 -04:00
|
|
|
|
*/
|
2026-09-30 09:50:25 -04:00
|
|
|
|
import { zoneOption } from "../time/zone";
|
2026-09-24 00:08:58 -04:00
|
|
|
|
|
|
|
|
|
|
const LOCALE = "en-US";
|
|
|
|
|
|
|
|
|
|
|
|
function calendarDate(iso: string): Date {
|
|
|
|
|
|
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
|
|
|
|
|
|
if (!match) throw new Error(`Invalid calendar date: ${iso}`);
|
|
|
|
|
|
const [, year, month, day] = match;
|
|
|
|
|
|
return new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function format(date: Date, options: Intl.DateTimeFormatOptions): string {
|
|
|
|
|
|
// Newer ICU puts a narrow no-break space before AM/PM; normalise it.
|
|
|
|
|
|
return new Intl.DateTimeFormat(LOCALE, options)
|
|
|
|
|
|
.format(date)
|
|
|
|
|
|
.replace(/ /g, " ");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** "Wednesday, September 23" */
|
|
|
|
|
|
export function formatLongDate(iso: string): string {
|
|
|
|
|
|
const date = calendarDate(iso);
|
|
|
|
|
|
const weekday = format(date, { weekday: "long", timeZone: "UTC" });
|
|
|
|
|
|
const monthDay = format(date, { month: "long", day: "numeric", timeZone: "UTC" });
|
|
|
|
|
|
return `${weekday}, ${monthDay}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** "Sep 23" */
|
|
|
|
|
|
export function formatShortDate(iso: string): string {
|
|
|
|
|
|
return format(calendarDate(iso), { month: "short", day: "numeric", timeZone: "UTC" });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** "09:00" -> "9:00 AM" (a wall-clock time, no zone conversion). */
|
|
|
|
|
|
export function formatDueTime(hhmm: string): string {
|
|
|
|
|
|
const match = /^(\d{2}):(\d{2})/.exec(hhmm);
|
|
|
|
|
|
if (!match) return hhmm;
|
|
|
|
|
|
const date = new Date(Date.UTC(2000, 0, 1, Number(match[1]), Number(match[2])));
|
|
|
|
|
|
return format(date, { hour: "numeric", minute: "2-digit", timeZone: "UTC" });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-30 09:50:25 -04:00
|
|
|
|
/** A UTC ISO timestamp shown as wall-clock time in `timeZone` ("" = browser), "7:04 AM". */
|
|
|
|
|
|
export function formatClockTime(isoTimestamp: string, timeZone: string): string {
|
|
|
|
|
|
return format(new Date(isoTimestamp), {
|
|
|
|
|
|
hour: "numeric",
|
|
|
|
|
|
minute: "2-digit",
|
|
|
|
|
|
timeZone: zoneOption(timeZone),
|
|
|
|
|
|
});
|
2026-09-24 00:08:58 -04:00
|
|
|
|
}
|