date-vir
Version:
Easy and explicit dates and times.
109 lines (108 loc) • 4.05 kB
JavaScript
import { check } from '@augment-vir/assert';
import { arrayToObject, filterMap } from '@augment-vir/common';
import { DurationUnit, convertDuration, flattenUnitSelection, singularDurationUnitNames, } from '@date-vir/duration';
import { diffDates } from '../date-operations/diff-dates.js';
const defaultJustNowThresholds = {
minutes: 1.5,
seconds: 5,
milliseconds: 500,
};
/**
* This function starts with a duration (either by being directly passed a duration or by diffing
* two dates into a duration) and converts that duration into a relative string like "1 month ago"
* or "in 1 month". Rounding is automatically set to 0 decimal points, but that can be changed.
*
* When extremely close to a `0` difference, the output string will be `'just now'`, which can be
* disabled.
*
* @category Formatting
* @example
*
* ```ts
* import {toRelativeString, selectAllDurationUnits} from 'date-vir';
*
* toRelativeString({days: 1.6}, {days: true}); // `'in 2 days'`
* toRelativeString({days: 1.6}, {days: true, hours: true}); // `'in 1 day, 14 hours'`
* toRelativeString({seconds: 1}, selectAllDurationUnits); // `'just now'`
* ```
*/
export function toRelativeString(datesOrDuration, units, options = {}) {
const selectedUnits = flattenUnitSelection(units);
/** If there are no selected units, return nothing. */
if (!check.isLengthAtLeast(selectedUnits, 1)) {
return '';
}
const diff = convertDuration('start' in datesOrDuration ? diffDates(datesOrDuration, units) : datesOrDuration, units, {
roundToDigits: options.allowedDecimals || 0,
});
const isDiffPositive = convertDuration(diff, { milliseconds: true }).milliseconds >= 0;
const unitValues = filterMap(selectedUnits, (unit) => {
const quantity = diff[unit] || 0;
if (!quantity) {
return undefined;
}
return {
quantity,
unit,
};
}, check.isTruthy).reverse();
const justNowThresholds = options.justNowThresholds || defaultJustNowThresholds;
const shouldUseJustNow = !options.blockJustNow &&
(!check.isLengthAtLeast(unitValues, 1) ||
(unitValues[0].unit === DurationUnit.Minutes &&
/* node:coverage ignore next 1 */
Math.abs(diff.minutes || 0) < justNowThresholds.minutes) ||
(unitValues[0].unit === DurationUnit.Seconds &&
/* node:coverage ignore next 1 */
Math.abs(diff.seconds || 0) < justNowThresholds.seconds) ||
(unitValues[0].unit === DurationUnit.Milliseconds &&
/* node:coverage ignore next 1 */
Math.abs(diff.milliseconds || 0) < justNowThresholds.milliseconds));
if (shouldUseJustNow) {
return 'just now';
}
else if (options.useOnlyLargestUnit) {
if (!unitValues[0]) {
return '';
}
return toRelativeString(datesOrDuration, { [unitValues[0].unit]: true }, {
...options,
useOnlyLargestUnit: false,
});
}
else if (unitValues.length < selectedUnits.length) {
/**
* If the finalized units are less than the selected units, rerun the whole calculation to
* make sure we're using the right accuracy.
*/
return toRelativeString(datesOrDuration, arrayToObject(unitValues, ({ unit }) => {
return {
key: unit,
value: true,
};
}), options);
}
const unitsString = unitValues
.map(({ quantity, unit }) => {
const absoluteQuantity = Math.abs(quantity);
return [
absoluteQuantity,
' ',
singularDurationUnitNames[unit],
absoluteQuantity > 1 ? 's' : '',
].join('');
})
.join(', ');
if (isDiffPositive) {
return [
'in',
unitsString,
].join(' ');
}
else {
return [
unitsString,
'ago',
].join(' ');
}
}