Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 1 | // 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 | |
| 5 | const 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 |
| 13 | export 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 | |
| 23 | export const SHORT_FORMATTER = new Intl.DateTimeFormat(DEFAULT_DATE_LOCALE, { |
| 24 | year: 'numeric', |
| 25 | month: 'short', |
| 26 | day: 'numeric', |
| 27 | }); |
| 28 | |
| 29 | export const MS_PER_MINUTE = 60 * 1000; |
| 30 | export const MS_PER_HOUR = MS_PER_MINUTE * 60; |
| 31 | export const MS_PER_DAY = MS_PER_HOUR * 24; |
| 32 | export 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 | */ |
| 40 | function 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 | */ |
| 51 | export 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 | */ |
| 70 | export 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 | } |