/** * Every entry carries the timezone it was written in, and every stamp is * formatted in that timezone rather than in the reader's current one - a * journal written on a trip should still read as the local time of the moment. * Timestamps cross the wire as UNIX seconds, so nothing here parses a string. */ const asFormatterCache = new Map(); function getFormatter(sLocale, sTimezone, asOptions) { const sKey = sLocale + '|' + sTimezone + '|' + JSON.stringify(asOptions); if(!asFormatterCache.has(sKey)) { let oFormatter; try { oFormatter = new Intl.DateTimeFormat(sLocale, {...asOptions, timeZone: sTimezone}); } catch{ //An unknown IANA name (renamed zone, hand-edited row) must not take //the whole book down - fall back to the browser's own zone. oFormatter = new Intl.DateTimeFormat(sLocale, asOptions); } asFormatterCache.set(sKey, oFormatter); } return asFormatterCache.get(sKey); } export function getBrowserTimezone() { try { return Intl.DateTimeFormat().resolvedOptions().timeZone || ''; } catch{ return ''; } } /** 'YYYY-MM-DD' for the given instant, as seen in the given timezone. */ export function getDayKey(iUnix, sTimezone) { return getFormatter('en-CA', sTimezone, {year: 'numeric', month: '2-digit', day: '2-digit'}).format(iUnix * 1000); } /** 'YYYY-MM-DD' for a local Date, without the UTC shift toISOString() adds. */ export function getLocalDayKey(oDate) { const sMonth = String(oDate.getMonth() + 1).padStart(2, '0'); const sDay = String(oDate.getDate()).padStart(2, '0'); return oDate.getFullYear() + '-' + sMonth + '-' + sDay; } export function formatDate(iUnix, sTimezone, sLocale) { return getFormatter(sLocale, sTimezone, {day: 'numeric', month: 'short', year: 'numeric'}).format(iUnix * 1000); } export function formatShortDate(iUnix, sTimezone, sLocale) { return getFormatter(sLocale, sTimezone, {day: 'numeric', month: 'short'}).format(iUnix * 1000); } export function formatTime(iUnix, sTimezone, sLocale) { return getFormatter(sLocale, sTimezone, {hour: '2-digit', minute: '2-digit'}).format(iUnix * 1000); } export function formatWeekday(iUnix, sTimezone, sLocale) { return getFormatter(sLocale, sTimezone, {weekday: 'long'}).format(iUnix * 1000); } export function formatFull(iUnix, sTimezone, sLocale) { return getFormatter(sLocale, sTimezone, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' }).format(iUnix * 1000); } export function formatMonthTitle(oDate, sLocale) { return new Intl.DateTimeFormat(sLocale, {month: 'long', year: 'numeric'}).format(oDate); } /** * Weekday initials starting on Monday, in the reader's locale. */ export function getWeekdayInitials(sLocale) { const oFormatter = new Intl.DateTimeFormat(sLocale, {weekday: 'short'}); const asLabels = []; //2024-01-01 was a Monday, which anchors the week without hard-coding names. for(let iDay = 0; iDay < 7; iDay++) { asLabels.push(oFormatter.format(new Date(Date.UTC(2024, 0, 1 + iDay)))); } return asLabels; } /** Short "how long ago", used by the autosave indicator only. */ export function formatSince(iUnix, sLocale, sJustNow) { const iSeconds = Math.round(Date.now() / 1000) - iUnix; if(iSeconds < 45) return sJustNow; const oFormatter = new Intl.RelativeTimeFormat(sLocale, {numeric: 'auto'}); if(iSeconds < 3600) return oFormatter.format(-Math.round(iSeconds / 60), 'minute'); if(iSeconds < 86400) return oFormatter.format(-Math.round(iSeconds / 3600), 'hour'); return oFormatter.format(-Math.round(iSeconds / 86400), 'day'); }