Something went wrong. Try again.
Where attention appears to go across agent threads: collector, web explorer, and native iOS client
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170// @ts-check
/** @param {string | undefined} day */export function validDay(day) { return ( /^\d{4}-\d{2}-\d{2}$/.test(day || "") && !Number.isNaN(Date.parse(/** @type {string} */ (day))) && new Date(/** @type {string} */ (day)).toISOString().slice(0, 10) === day );}
/** @param {string} day @param {number} amount */export function shiftDay(day, amount) { const date = new Date(`${day}T12:00:00Z`); date.setUTCDate(date.getUTCDate() + amount); return date.toISOString().slice(0, 10);}
/** @param {number} n @param {string} one @param {string} [many] */export const plural = (n, one, many = one + "s") => `${n} ${n === 1 ? one : many}`;
/** @param {number} seconds */export function duration(seconds) { const minutes = Math.round(seconds / 60); return seconds < 60 ? `${Math.round(seconds)}s` : minutes < 60 ? `${minutes}m` : `${Math.floor(minutes / 60)}h ${minutes % 60}m`;}
/** @param {string} zone */export function isZone(zone) { try { new Intl.DateTimeFormat("en-US", { timeZone: zone }); return true; } catch { return false; }}
/** @param {string} day */const noonUTC = (day) => new Date(`${day}T12:00:00Z`);
/** * Date helpers bound to one IANA time zone. Day keys are YYYY-MM-DD in that * zone; calendar labels for a day key are formatted independently of zone. * @param {string} zone * @param {string | string[]} [locale] */export function calendar(zone, locale) { const parts = new Intl.DateTimeFormat("en-US", { timeZone: zone, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hourCycle: "h23", }); /** @param {number} ms */ const fields = (ms) => { /** @type {Record<string, string>} */ const out = {}; for (const part of parts.formatToParts(new Date(ms))) out[part.type] = part.value; return { year: out.year, month: out.month, day: out.day, hour: out.hour === "24" ? "00" : out.hour, minute: out.minute, second: out.second, }; }; const clock = new Intl.DateTimeFormat(locale, { timeZone: zone, hour: "numeric", minute: "2-digit", }); const stamp = new Intl.DateTimeFormat(locale, { timeZone: zone, month: "short", day: "numeric", hour: "numeric", minute: "2-digit", }); const shortAt = new Intl.DateTimeFormat(locale, { timeZone: zone, month: "short", day: "numeric", }); const monthYearAt = new Intl.DateTimeFormat(locale, { timeZone: zone, month: "long", year: "numeric", }); const utc = (/** @type {Intl.DateTimeFormatOptions} */ options) => new Intl.DateTimeFormat(locale, { timeZone: "UTC", ...options }); const longDay = utc({ month: "long", day: "numeric" }); const shortDay = utc({ month: "short", day: "numeric" }); const weekday = utc({ weekday: "long" }); const weekdayShort = utc({ weekday: "short" }); const title = utc({ weekday: "long", month: "short", day: "numeric" });
/** @param {number} at seconds since epoch */ const dateKey = (at) => { const p = fields(at * 1000); return `${p.year}-${p.month}-${p.day}`; }; const today = () => dateKey(Date.now() / 1000); /** @param {string} day */ const midnight = (day) => { const target = Date.parse(`${day}T00:00:00Z`); let at = target; for (let i = 0; i < 3; i++) { const p = fields(at); at += target - Date.parse( `${p.year}-${p.month}-${p.day}T${p.hour}:${p.minute}:${p.second}Z`, ); } return at / 1000; }; /** @param {string} day */ const dayName = (day) => { const now = today(); return day === now ? "Today" : day === shiftDay(now, -1) ? "Yesterday" : weekday.format(noonUTC(day)); }; return { zone, dateKey, today, /** @param {string} day @param {number} [period] @returns {[number, number]} */ bounds: (day, period = 1) => [ midnight(shiftDay(day, 1 - period)), midnight(shiftDay(day, 1)), ], /** @param {number} at */ time: (at) => clock.format(new Date(at * 1000)), /** @param {number} at */ stamp: (at) => stamp.format(new Date(at * 1000)), /** @param {number} at */ shortDate: (at) => shortAt.format(new Date(at * 1000)), /** @param {number} at */ monthYear: (at) => monthYearAt.format(new Date(at * 1000)), dayName, /** @param {string} day */ longDate: (day) => longDay.format(noonUTC(day)), /** @param {string} day */ shortDay: (day) => shortDay.format(noonUTC(day)), /** @param {string} day */ weekdayShort: (day) => weekdayShort.format(noonUTC(day)), /** @param {string} day */ dayNumber: (day) => Number(day.slice(8)), /** @param {string} day */ title: (day) => title.format(noonUTC(day)), /** @param {string} day */ fullLabel: (day) => `${dayName(day)}, ${longDay.format(noonUTC(day))}`, };}
/** @typedef {ReturnType<typeof calendar>} Calendar */