101 lines
3.5 KiB
JavaScript
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');
|
|
}
|