@hypothesis/frontend-shared
Version:
Shared components, styles and utilities for Hypothesis projects
228 lines (216 loc) • 6.28 kB
JavaScript
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