Files
daydream/src/scripts/time.js
T
2026-09-03 18:28:33 +02:00

101 lines
3.5 KiB
JavaScript

/**
* 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');
}