UNPKG

8.03 kBJavaScriptView Raw
1import { defaultLocale } from "./_lib/defaultLocale.js";
2import { getDefaultOptions } from "./_lib/defaultOptions.js";
3import { getTimezoneOffsetInMilliseconds } from "./_lib/getTimezoneOffsetInMilliseconds.js";
4import { normalizeDates } from "./_lib/normalizeDates.js";
5import { compareAsc } from "./compareAsc.js";
6import { minutesInDay, minutesInMonth } from "./constants.js";
7import { differenceInMonths } from "./differenceInMonths.js";
8import { differenceInSeconds } from "./differenceInSeconds.js";
9
10/**
11 * The {@link formatDistance} function options.
12 */
13
14/**
15 * @name formatDistance
16 * @category Common Helpers
17 * @summary Return the distance between the given dates in words.
18 *
19 * @description
20 * Return the distance between the given dates in words.
21 *
22 * | Distance between dates | Result |
23 * |-------------------------------------------------------------------|---------------------|
24 * | 0 ... 30 secs | less than a minute |
25 * | 30 secs ... 1 min 30 secs | 1 minute |
26 * | 1 min 30 secs ... 44 mins 30 secs | [2..44] minutes |
27 * | 44 mins ... 30 secs ... 89 mins 30 secs | about 1 hour |
28 * | 89 mins 30 secs ... 23 hrs 59 mins 30 secs | about [2..24] hours |
29 * | 23 hrs 59 mins 30 secs ... 41 hrs 59 mins 30 secs | 1 day |
30 * | 41 hrs 59 mins 30 secs ... 29 days 23 hrs 59 mins 30 secs | [2..30] days |
31 * | 29 days 23 hrs 59 mins 30 secs ... 44 days 23 hrs 59 mins 30 secs | about 1 month |
32 * | 44 days 23 hrs 59 mins 30 secs ... 59 days 23 hrs 59 mins 30 secs | about 2 months |
33 * | 59 days 23 hrs 59 mins 30 secs ... 1 yr | [2..12] months |
34 * | 1 yr ... 1 yr 3 months | about 1 year |
35 * | 1 yr 3 months ... 1 yr 9 month s | over 1 year |
36 * | 1 yr 9 months ... 2 yrs | almost 2 years |
37 * | N yrs ... N yrs 3 months | about N years |
38 * | N yrs 3 months ... N yrs 9 months | over N years |
39 * | N yrs 9 months ... N+1 yrs | almost N+1 years |
40 *
41 * With `options.includeSeconds == true`:
42 * | Distance between dates | Result |
43 * |------------------------|----------------------|
44 * | 0 secs ... 5 secs | less than 5 seconds |
45 * | 5 secs ... 10 secs | less than 10 seconds |
46 * | 10 secs ... 20 secs | less than 20 seconds |
47 * | 20 secs ... 40 secs | half a minute |
48 * | 40 secs ... 60 secs | less than a minute |
49 * | 60 secs ... 90 secs | 1 minute |
50 *
51 * @param laterDate - The date
52 * @param earlierDate - The date to compare with
53 * @param options - An object with options
54 *
55 * @returns The distance in words
56 *
57 * @throws `date` must not be Invalid Date
58 * @throws `baseDate` must not be Invalid Date
59 * @throws `options.locale` must contain `formatDistance` property
60 *
61 * @example
62 * // What is the distance between 2 July 2014 and 1 January 2015?
63 * const result = formatDistance(new Date(2014, 6, 2), new Date(2015, 0, 1))
64 * //=> '6 months'
65 *
66 * @example
67 * // What is the distance between 1 January 2015 00:00:15
68 * // and 1 January 2015 00:00:00, including seconds?
69 * const result = formatDistance(
70 * new Date(2015, 0, 1, 0, 0, 15),
71 * new Date(2015, 0, 1, 0, 0, 0),
72 * { includeSeconds: true }
73 * )
74 * //=> 'less than 20 seconds'
75 *
76 * @example
77 * // What is the distance from 1 January 2016
78 * // to 1 January 2015, with a suffix?
79 * const result = formatDistance(new Date(2015, 0, 1), new Date(2016, 0, 1), {
80 * addSuffix: true
81 * })
82 * //=> 'about 1 year ago'
83 *
84 * @example
85 * // What is the distance between 1 August 2016 and 1 January 2015 in Esperanto?
86 * import { eoLocale } from 'date-fns/locale/eo'
87 * const result = formatDistance(new Date(2016, 7, 1), new Date(2015, 0, 1), {
88 * locale: eoLocale
89 * })
90 * //=> 'pli ol 1 jaro'
91 */
92export function formatDistance(laterDate, earlierDate, options) {
93 const defaultOptions = getDefaultOptions();
94 const locale = options?.locale ?? defaultOptions.locale ?? defaultLocale;
95 const minutesInAlmostTwoDays = 2520;
96
97 const comparison = compareAsc(laterDate, earlierDate);
98
99 if (isNaN(comparison)) throw new RangeError("Invalid time value");
100
101 const localizeOptions = Object.assign({}, options, {
102 addSuffix: options?.addSuffix,
103 comparison: comparison,
104 });
105
106 const [laterDate_, earlierDate_] = normalizeDates(
107 options?.in,
108 ...(comparison > 0 ? [earlierDate, laterDate] : [laterDate, earlierDate]),
109 );
110
111 const seconds = differenceInSeconds(earlierDate_, laterDate_);
112 const offsetInSeconds =
113 (getTimezoneOffsetInMilliseconds(earlierDate_) -
114 getTimezoneOffsetInMilliseconds(laterDate_)) /
115 1000;
116 const minutes = Math.round((seconds - offsetInSeconds) / 60);
117 let months;
118
119 // 0 up to 2 mins
120 if (minutes < 2) {
121 if (options?.includeSeconds) {
122 if (seconds < 5) {
123 return locale.formatDistance("lessThanXSeconds", 5, localizeOptions);
124 } else if (seconds < 10) {
125 return locale.formatDistance("lessThanXSeconds", 10, localizeOptions);
126 } else if (seconds < 20) {
127 return locale.formatDistance("lessThanXSeconds", 20, localizeOptions);
128 } else if (seconds < 40) {
129 return locale.formatDistance("halfAMinute", 0, localizeOptions);
130 } else if (seconds < 60) {
131 return locale.formatDistance("lessThanXMinutes", 1, localizeOptions);
132 } else {
133 return locale.formatDistance("xMinutes", 1, localizeOptions);
134 }
135 } else {
136 if (minutes === 0) {
137 return locale.formatDistance("lessThanXMinutes", 1, localizeOptions);
138 } else {
139 return locale.formatDistance("xMinutes", minutes, localizeOptions);
140 }
141 }
142
143 // 2 mins up to 0.75 hrs
144 } else if (minutes < 45) {
145 return locale.formatDistance("xMinutes", minutes, localizeOptions);
146
147 // 0.75 hrs up to 1.5 hrs
148 } else if (minutes < 90) {
149 return locale.formatDistance("aboutXHours", 1, localizeOptions);
150
151 // 1.5 hrs up to 24 hrs
152 } else if (minutes < minutesInDay) {
153 const hours = Math.round(minutes / 60);
154 return locale.formatDistance("aboutXHours", hours, localizeOptions);
155
156 // 1 day up to 1.75 days
157 } else if (minutes < minutesInAlmostTwoDays) {
158 return locale.formatDistance("xDays", 1, localizeOptions);
159
160 // 1.75 days up to 30 days
161 } else if (minutes < minutesInMonth) {
162 const days = Math.round(minutes / minutesInDay);
163 return locale.formatDistance("xDays", days, localizeOptions);
164
165 // 1 month up to 2 months
166 } else if (minutes < minutesInMonth * 2) {
167 months = Math.round(minutes / minutesInMonth);
168 return locale.formatDistance("aboutXMonths", months, localizeOptions);
169 }
170
171 months = differenceInMonths(earlierDate_, laterDate_);
172
173 // 2 months up to 12 months
174 if (months < 12) {
175 const nearestMonth = Math.round(minutes / minutesInMonth);
176 return locale.formatDistance("xMonths", nearestMonth, localizeOptions);
177
178 // 1 year up to max Date
179 } else {
180 const monthsSinceStartOfYear = months % 12;
181 const years = Math.trunc(months / 12);
182
183 // N years up to 1 years 3 months
184 if (monthsSinceStartOfYear < 3) {
185 return locale.formatDistance("aboutXYears", years, localizeOptions);
186
187 // N years 3 months up to N years 9 months
188 } else if (monthsSinceStartOfYear < 9) {
189 return locale.formatDistance("overXYears", years, localizeOptions);
190
191 // N years 9 months up to N year 12 months
192 } else {
193 return locale.formatDistance("almostXYears", years + 1, localizeOptions);
194 }
195 }
196}
197
198// Fallback for modularized imports:
199export default formatDistance;