UNPKG

relative-time

Version:

Formats JavaScript dates to relative time strings (e.g., "3 hours ago")

301 lines (260 loc) 7.53 kB
const root = typeof global !== "undefined" ? global : typeof window !== "undefined" ? window : typeof self !== "undefined" ? self : {}; function getTemporal() { const Temporal = root.Temporal; if (!Temporal) { throw new TypeError("Temporal is required to use relative-time"); } return Temporal; } function defineCachedGetter(object, property, compute) { Object.defineProperty(object, property, { configurable: true, enumerable: true, get() { const value = compute(); Object.defineProperty(object, property, { configurable: true, enumerable: true, value, }); return value; }, }); } function isTemporalZonedDateTime(value, Temporal) { return Boolean( Temporal.ZonedDateTime && value instanceof Temporal.ZonedDateTime ); } function isTemporalPlainDateTime(value, Temporal) { return Boolean( Temporal.PlainDateTime && value instanceof Temporal.PlainDateTime ); } function resolvePlainNow(now, Temporal, target) { if (now === undefined || now === null) { const current = Temporal.Now.plainDateTimeISO(); return typeof current.withCalendar === "function" && target.calendar ? current.withCalendar(target.calendar) : current; } if (!isTemporalPlainDateTime(now, Temporal)) { throw new TypeError( "Unsupported now value; expected Temporal.PlainDateTime" ); } return typeof now.withCalendar === "function" && target.calendar ? now.withCalendar(target.calendar) : now; } function resolveZonedNow(now, Temporal, targetZone) { if (now === undefined || now === null) { return Temporal.Now.zonedDateTimeISO(targetZone); } if (!isTemporalZonedDateTime(now, Temporal)) { throw new TypeError( "Unsupported now value; expected Temporal.ZonedDateTime" ); } if (now.timeZoneId !== targetZone) { throw new TypeError( "Unsupported now value; expected Temporal.ZonedDateTime in the same time zone as the target date" ); } return now; } function differenceInUnit(now, target, unit) { if (unit === "year" || unit === "month") { const startDate = typeof now.toPlainDate === "function" ? now.toPlainDate() : now; const endDate = typeof target.toPlainDate === "function" ? target.toPlainDate() : target; const yearDelta = endDate.year - startDate.year; if (unit === "year") { return yearDelta; } return yearDelta * 12 + (endDate.month - startDate.month); } if (unit === "day") { const startDate = typeof now.toPlainDate === "function" ? now.toPlainDate() : now; const endDate = typeof target.toPlainDate === "function" ? target.toPlainDate() : target; const duration = startDate.until(endDate, { largestUnit: unit, smallestUnit: unit, roundingMode: "trunc", }); return duration.days; } if (unit === "hour") { const duration = now.until(target, { largestUnit: unit, smallestUnit: unit, roundingMode: "trunc", }); let hours = duration.hours; if (hours === 0) { const minutes = now.until(target, { largestUnit: "minute", smallestUnit: "minute", roundingMode: "trunc", }); if (minutes.minutes < 0) { return -1; } } return hours; } const duration = now.until(target, { largestUnit: unit, smallestUnit: unit, roundingMode: "trunc", }); return duration[unit + "s"]; } export class RelativeTimeResolver { constructor(options = {}) { this.Temporal = options.Temporal || this.constructor.Temporal; this.threshold = options.threshold || this.constructor.threshold; this.units = options.units || this.constructor.units; } resolve(date, { now, unit = "best-fit" } = {}) { const Temporal = this.Temporal || getTemporal(); let target; let resolvedNow; if (isTemporalZonedDateTime(date, Temporal)) { const targetZone = date.timeZoneId; target = date.withTimeZone(targetZone); resolvedNow = resolveZonedNow(now, Temporal, targetZone); } else if (isTemporalPlainDateTime(date, Temporal)) { target = date; resolvedNow = resolvePlainNow(now, Temporal, target); } else { throw new TypeError( "Unsupported date value; expected Temporal.ZonedDateTime or Temporal.PlainDateTime" ); } const diff = Object.create(null); const absDiff = Object.create(null); const diffUnits = this.units; diffUnits.forEach((currentUnit) => { defineCachedGetter(diff, currentUnit, () => { return differenceInUnit(resolvedNow, target, currentUnit); }); defineCachedGetter(absDiff, currentUnit, () => { return Math.abs(diff[currentUnit]); }); }); const resolvedUnit = unit === "best-fit" ? this.constructor.bestFit(absDiff, this.threshold) : unit; return { unit: resolvedUnit, value: diff[resolvedUnit], }; } static bestFit(absDiff, threshold = this.threshold) { switch (true) { case absDiff.year > 0 && absDiff.month > threshold.month: return "year"; case absDiff.month > 0 && absDiff.day > threshold.day: return "month"; // case absDiff.month > 0 && absDiff.week > threshold.week: return "month"; // case absDiff.week > 0 && absDiff.day > threshold.day: return "week"; case absDiff.day > 0 && absDiff.hour > threshold.hour: return "day"; case absDiff.hour > 0 && absDiff.minute > threshold.minute: return "hour"; case absDiff.minute > 0 && absDiff.second > threshold.second: return "minute"; default: return "second"; } } } RelativeTimeResolver.units = [ "year", "month", /* "week", */ "day", "hour", "minute", "second", ]; RelativeTimeResolver.threshold = { month: 2, // week: 4, day: 6, hour: 6, minute: 59, second: 59, }; export default class RelativeTime { constructor() { this.formatters = RelativeTime.initializeFormatters(...arguments); this.resolver = new RelativeTimeResolver(); } format(date, options = {}) { const { unit = "best-fit", now } = options; const { unit: resolvedUnit, value } = this.resolver.resolve(date, { unit, now, }); return this.formatters[resolvedUnit](value); } } RelativeTime.bestFit = RelativeTimeResolver.bestFit; RelativeTime.threshold = RelativeTimeResolver.threshold; RelativeTime.initializeFormatters = function (localesOrFormatter, options) { let locales = localesOrFormatter; let formatOptions = options; if ( localesOrFormatter && typeof localesOrFormatter.format === "function" && typeof localesOrFormatter.resolvedOptions === "function" ) { return createFormatterMap(localesOrFormatter); } if ( localesOrFormatter && typeof localesOrFormatter === "object" && !Array.isArray(localesOrFormatter) ) { formatOptions = localesOrFormatter; locales = undefined; } return createFormatterMap( new Intl.RelativeTimeFormat( locales, Object.assign( { numeric: "auto", }, formatOptions ) ) ); }; function createFormatterMap(formatter) { return [ "second", "minute", "hour", "day", /* "week", */ "month", "year", ].reduce(function (map, unit) { map[unit] = function (value) { return formatter.format(value, unit); }; return map; }, {}); }