blob: 2fa1dc28575a008f5d3196ffa28e69c5ffb62b36 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001// Copyright 2019 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5const DEFAULT_DATE_LOCALE = 'en-US';
6
7// Creating the datetime formatter costs ~1.5 ms, so when formatting
8// multiple timestamps, it's more performant to reuse the formatter object.
9// Export FORMATTER and SHORT_FORMATTER for testing. The return value differs
10// based on time zone and browser, so we can't use static strings for testing.
11// We can't stub out the method because it's native code and can't be modified.
12// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format#Avoid_comparing_formatted_date_values_to_static_values
13export const FORMATTER = new Intl.DateTimeFormat(DEFAULT_DATE_LOCALE, {
14 weekday: 'short',
15 year: 'numeric',
16 month: 'short',
17 day: 'numeric',
18 hour: 'numeric',
19 minute: '2-digit',
20 timeZoneName: 'short',
21});
22
23export const SHORT_FORMATTER = new Intl.DateTimeFormat(DEFAULT_DATE_LOCALE, {
24 year: 'numeric',
25 month: 'short',
26 day: 'numeric',
27});
28
29export const MS_PER_MINUTE = 60 * 1000;
30export const MS_PER_HOUR = MS_PER_MINUTE * 60;
31export const MS_PER_DAY = MS_PER_HOUR * 24;
32export const MS_PER_MONTH = MS_PER_DAY * 30;
33
34/**
35 * Helper to determine if a Date was less than a month ago.
36 * @param {Date} date The date to check.
37 * @return {boolean} Whether the date was less than a
38 * month ago.
39 */
40function isLessThanAMonthAgo(date) {
41 const now = new Date();
42 const msDiff = Math.abs(Math.floor((now.getTime() - date.getTime())));
43 return msDiff < MS_PER_MONTH;
44}
45
46/**
47 * Displays timestamp in a standardized format to be re-used.
48 * @param {Date} date
49 * @return {string}
50 */
51export function standardTime(date) {
52 if (!date) return;
53 const absoluteTime = FORMATTER.format(date);
54
55 let timeAgoBit = '';
56 if (isLessThanAMonthAgo(date)) {
57 // Only show relative time if the time is less than a
58 // month ago because otherwise, it's not as useful.
59 timeAgoBit = ` (${relativeTime(date)})`;
60 }
61 return `${absoluteTime}${timeAgoBit}`;
62}
63
64/**
65 * Displays a timestamp in a format that's easy for a human to immediately
66 * reason about, based on long ago the time was.
67 * @param {Date} date native JavaScript Data Object.
68 * @return {string} Human-readable string of the date.
69 */
70export function relativeTime(date) {
71 if (!date) return;
72
73 const now = new Date();
74 let msDiff = now.getTime() - date.getTime();
75
76 // Use different wording depending on whether the time is in the
77 // future or past.
78 const pastOrPresentSuffix = msDiff < 0 ? 'from now' : 'ago';
79 msDiff = Math.abs(msDiff);
80
81 if (msDiff < MS_PER_MINUTE) {
82 // Less than a minute.
83 return 'just now';
84 } else if (msDiff < MS_PER_HOUR) {
85 // Less than an hour.
86 const minutes = Math.floor(msDiff / MS_PER_MINUTE);
87 if (minutes === 1) {
88 return `a minute ${pastOrPresentSuffix}`;
89 }
90 return `${minutes} minutes ${pastOrPresentSuffix}`;
91 } else if (msDiff < MS_PER_DAY) {
92 // Less than an day.
93 const hours = Math.floor(msDiff / MS_PER_HOUR);
94 if (hours === 1) {
95 return `an hour ${pastOrPresentSuffix}`;
96 }
97 return `${hours} hours ${pastOrPresentSuffix}`;
98 } else if (msDiff < MS_PER_MONTH) {
99 // Less than a month.
100 const days = Math.floor(msDiff / MS_PER_DAY);
101 if (days === 1) {
102 return `a day ${pastOrPresentSuffix}`;
103 }
104 return `${days} days ${pastOrPresentSuffix}`;
105 }
106
107 // A month or more ago. Better to show an exact date at this point.
108 return SHORT_FORMATTER.format(date);
109}