UNPKG

@hypothesis/frontend-shared

Version:

Shared components, styles and utilities for Hypothesis projects

228 lines (216 loc) 6.28 kB
const SECOND = 1000; const MINUTE = 60 * SECOND; const HOUR = 60 * MINUTE; /** * Map of stringified `DateTimeFormatOptions` to cached `DateTimeFormat` instances. */ let formatters = new Map(); /** * Clears the cache of formatters. */ export function clearFormatters() { formatters = new Map(); } /** * Calculate time delta in milliseconds between two `Date` objects */ function delta(date, now) { // @ts-ignore return now - date; } /** * Return date string formatted with `options`. * * This is a caching wrapper for `Intl.DateTimeFormat.format`, useful because * constructing a `DateTimeFormat` is expensive. * * @param Intl - Test seam. JS `Intl` API implementation. */ function format(date, options, /* istanbul ignore next */ Intl = window.Intl) { const key = JSON.stringify(options); let formatter = formatters.get(key); if (!formatter) { formatter = new Intl.DateTimeFormat(undefined, options); formatters.set(key, formatter); } return formatter.format(date); } /** * @return formatted date */ const nSec = (date, now) => { const n = Math.floor(delta(date, now) / SECOND); return `${n} secs ago`; }; const nMin = (date, now) => { const n = Math.floor(delta(date, now) / MINUTE); const plural = n > 1 ? 's' : ''; return `${n} min${plural} ago`; }; const nHr = (date, now) => { const n = Math.floor(delta(date, now) / HOUR); const plural = n > 1 ? 's' : ''; return `${n} hr${plural} ago`; }; const dayAndMonth = (date, now, Intl) => { return format(date, { month: 'short', day: 'numeric' }, Intl); }; const dayAndMonthAndYear = (date, now, Intl) => { return format(date, { day: 'numeric', month: 'short', year: 'numeric' }, Intl); }; const BREAKPOINTS = [{ // Less than 30 seconds test: (date, now) => delta(date, now) < 30 * SECOND, formatter: () => 'Just now', nextUpdate: 1 * SECOND }, { // Less than 1 minute test: (date, now) => delta(date, now) < 1 * MINUTE, formatter: nSec, nextUpdate: 1 * SECOND }, { // Less than one hour test: (date, now) => delta(date, now) < 1 * HOUR, formatter: nMin, nextUpdate: 1 * MINUTE }, { // Less than one day test: (date, now) => delta(date, now) < 24 * HOUR, formatter: nHr, nextUpdate: 1 * HOUR }, { // This year test: (date, now) => date.getFullYear() === now.getFullYear(), formatter: dayAndMonth, nextUpdate: null }]; const DEFAULT_BREAKPOINT = { test: /* istanbul ignore next */() => true, formatter: dayAndMonthAndYear, nextUpdate: null }; /** * Returns a dict that describes how to format the date based on the delta * between date and now. * * @param date - The date to consider as the timestamp to format. * @param now - The date to consider as the current time. * @return An object that describes how to format the date. */ function getBreakpoint(date, now) { for (const breakpoint of BREAKPOINTS) { if (breakpoint.test(date, now)) { return breakpoint; } } return DEFAULT_BREAKPOINT; } /** * Determines if provided date represents a specific instant of time. * See https://262.ecma-international.org/6.0/#sec-time-values-and-time-range */ function isDateValid(date) { return !isNaN(date.valueOf()); } /** * Return the number of milliseconds until the next update for a given date * should be handled, based on the delta between `date` and `now`. * * @return ms until next update or `null` if no update should occur */ export function nextFuzzyUpdate(date, now) { if (!date || !isDateValid(date) || !isDateValid(now)) { return null; } let nextUpdate = getBreakpoint(date, now).nextUpdate; if (nextUpdate === null) { return null; } // We don't want to refresh anything more often than 5 seconds nextUpdate = Math.max(nextUpdate, 5 * SECOND); // setTimeout limit is MAX_INT32=(2^31-1) (in ms), // which is about 24.8 days. So we don't set up any timeouts // longer than 24 days, that is, 2073600 seconds. nextUpdate = Math.min(nextUpdate, 2073600 * SECOND); return nextUpdate; } /** * Start an interval whose frequency depends on the age of a timestamp. * * This is useful for refreshing UI components displaying timestamps generated * by `formatRelativeDate`, since the output changes less often for older timestamps. * * @param date - Date string to use to determine the interval frequency * @param callback - Interval callback * @return A function that cancels the interval */ export function decayingInterval(date, callback) { let timer; const timestamp = new Date(date); const update = () => { const fuzzyUpdate = nextFuzzyUpdate(timestamp, new Date()); if (fuzzyUpdate === null) { return; } const nextUpdate = fuzzyUpdate + 500; timer = setTimeout(() => { callback(); update(); }, nextUpdate); }; update(); return () => clearTimeout(timer); } /** * Formats a date as a short approximate string relative to the current date. * * The level of precision is proportional to how recent the date is. * * For example: * * - "Just now" * - "5 minutes ago" * - "25 Oct 2018" * * @param date - The date to consider as the timestamp to format. * @param now - The date to consider as the current time. * @param Intl - Test seam. JS `Intl` API implementation. * @return A 'fuzzy' string describing the relative age of the date. */ export function formatRelativeDate(date, now, Intl) { if (!date) { return ''; } return getBreakpoint(date, now).formatter(date, now, Intl); } /** * Formats a date as an absolute string in a human-readable format. * * The exact format will vary depending on the locale, but the verbosity will * be consistent across locales. In en-US for example this will look like: * * "Dec 17, 2017, 10:00 AM" */ export function formatDateTime(date, { includeWeekday = false, includeTime = true } = {}, /* istanbul ignore next - Test seam. JS `Intl` API implementation. */ Intl) { return format(typeof date === 'string' ? new Date(date) : date, { year: 'numeric', month: 'short', day: '2-digit', weekday: includeWeekday ? 'long' : undefined, hour: includeTime ? '2-digit' : undefined, minute: includeTime ? '2-digit' : undefined }, Intl); } //# sourceMappingURL=date-and-time.js.map