Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame^] | 1 | # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 | # Use of this source code is governed by a BSD-style |
| 3 | # license that can be found in the LICENSE file or at |
| 4 | # https://developers.google.com/open-source/licenses/bsd |
| 5 | |
| 6 | """Time-to-string and time-from-string routines.""" |
| 7 | from __future__ import print_function |
| 8 | from __future__ import division |
| 9 | from __future__ import absolute_import |
| 10 | |
| 11 | import calendar |
| 12 | import datetime |
| 13 | import time |
| 14 | |
| 15 | |
| 16 | class Error(Exception): |
| 17 | """Exception used to indicate problems with time routines.""" |
| 18 | pass |
| 19 | |
| 20 | |
| 21 | HTML_TIME_FMT = '%a, %d %b %Y %H:%M:%S GMT' |
| 22 | HTML_DATE_WIDGET_FORMAT = '%Y-%m-%d' |
| 23 | |
| 24 | MONTH_YEAR_FMT = '%b %Y' |
| 25 | MONTH_DAY_FMT = '%b %d' |
| 26 | MONTH_DAY_YEAR_FMT = '%b %d %Y' |
| 27 | |
| 28 | # We assume that all server clocks are synchronized within this amount. |
| 29 | MAX_CLOCK_SKEW_SEC = 30 |
| 30 | |
| 31 | |
| 32 | def TimeForHTMLHeader(when=None): |
| 33 | """Return the given time (or now) in HTML header format.""" |
| 34 | if when is None: |
| 35 | when = int(time.time()) |
| 36 | return time.strftime(HTML_TIME_FMT, time.gmtime(when)) |
| 37 | |
| 38 | |
| 39 | def TimestampToDateWidgetStr(when): |
| 40 | """Format a timestamp int for use by HTML <input type="date">.""" |
| 41 | return time.strftime(HTML_DATE_WIDGET_FORMAT, time.gmtime(when)) |
| 42 | |
| 43 | |
| 44 | def DateWidgetStrToTimestamp(val_str): |
| 45 | """Parse the HTML <input type="date"> string into a timestamp int.""" |
| 46 | return int(calendar.timegm(time.strptime(val_str, HTML_DATE_WIDGET_FORMAT))) |
| 47 | |
| 48 | |
| 49 | def FormatAbsoluteDate( |
| 50 | timestamp, clock=datetime.datetime.utcnow, |
| 51 | recent_format=MONTH_DAY_FMT, old_format=MONTH_YEAR_FMT): |
| 52 | """Format timestamp like 'Sep 5', or 'Yesterday', or 'Today'. |
| 53 | |
| 54 | Args: |
| 55 | timestamp: Seconds since the epoch in UTC. |
| 56 | clock: callable that returns a datetime.datetime object when called with no |
| 57 | arguments, giving the current time to use when computing what to display. |
| 58 | recent_format: Format string to pass to strftime to present dates between |
| 59 | six months ago and yesterday. |
| 60 | old_format: Format string to pass to strftime to present dates older than |
| 61 | six months or more than skew_tolerance in the future. |
| 62 | |
| 63 | Returns: |
| 64 | If timestamp's date is today, "Today". If timestamp's date is yesterday, |
| 65 | "Yesterday". If timestamp is within six months before today, return the |
| 66 | time as formatted by recent_format. Otherwise, return the time as formatted |
| 67 | by old_format. |
| 68 | """ |
| 69 | ts = datetime.datetime.utcfromtimestamp(timestamp) |
| 70 | now = clock() |
| 71 | month_delta = 12 * now.year + now.month - (12 * ts.year + ts.month) |
| 72 | delta = now - ts |
| 73 | |
| 74 | if ts > now: |
| 75 | # If the time is slightly in the future due to clock skew, treat as today. |
| 76 | skew_tolerance = datetime.timedelta(seconds=MAX_CLOCK_SKEW_SEC) |
| 77 | if -delta <= skew_tolerance: |
| 78 | return 'Today' |
| 79 | # Otherwise treat it like an old date. |
| 80 | else: |
| 81 | fmt = old_format |
| 82 | elif month_delta > 6 or delta.days >= 365: |
| 83 | fmt = old_format |
| 84 | elif delta.days == 1: |
| 85 | return 'Yesterday' |
| 86 | elif delta.days == 0: |
| 87 | return 'Today' |
| 88 | else: |
| 89 | fmt = recent_format |
| 90 | |
| 91 | return time.strftime(fmt, time.gmtime(timestamp)).replace(' 0', ' ') |
| 92 | |
| 93 | |
| 94 | def FormatRelativeDate(timestamp, days_only=False, clock=None): |
| 95 | """Return a short string that makes timestamp more meaningful to the user. |
| 96 | |
| 97 | Describe the timestamp relative to the current time, e.g., '4 |
| 98 | hours ago'. In cases where the timestamp is more than 6 days ago, |
| 99 | we return '' so that an alternative display can be used instead. |
| 100 | |
| 101 | Args: |
| 102 | timestamp: Seconds since the epoch in UTC. |
| 103 | days_only: If True, return 'N days ago' even for more than 6 days. |
| 104 | clock: optional function to return an int time, like int(time.time()). |
| 105 | |
| 106 | Returns: |
| 107 | String describing relative time. |
| 108 | """ |
| 109 | if clock: |
| 110 | now = clock() |
| 111 | else: |
| 112 | now = int(time.time()) |
| 113 | |
| 114 | # TODO(jrobbins): i18n of date strings |
| 115 | delta = int(now - timestamp) |
| 116 | d_minutes = delta // 60 |
| 117 | d_hours = d_minutes // 60 |
| 118 | d_days = d_hours // 24 |
| 119 | if days_only: |
| 120 | if d_days > 1: |
| 121 | return '%s days ago' % d_days |
| 122 | else: |
| 123 | return '' |
| 124 | |
| 125 | if d_days > 6: |
| 126 | return '' |
| 127 | if d_days > 1: |
| 128 | return '%s days ago' % d_days # starts at 2 days |
| 129 | if d_hours > 1: |
| 130 | return '%s hours ago' % d_hours # starts at 2 hours |
| 131 | if d_minutes > 1: |
| 132 | return '%s minutes ago' % d_minutes |
| 133 | if d_minutes > 0: |
| 134 | return '1 minute ago' |
| 135 | if delta > -MAX_CLOCK_SKEW_SEC: |
| 136 | return 'moments ago' |
| 137 | return '' |
| 138 | |
| 139 | |
| 140 | def GetHumanScaleDate(timestamp, now=None): |
| 141 | """Formats a timestamp to a course-grained and fine-grained time phrase. |
| 142 | |
| 143 | Args: |
| 144 | timestamp: Seconds since the epoch in UTC. |
| 145 | now: Current time in seconds since the epoch in UTC. |
| 146 | |
| 147 | Returns: |
| 148 | A pair (course_grain, fine_grain) where course_grain is a string |
| 149 | such as 'Today', 'Yesterday', etc.; and fine_grained is a string describing |
| 150 | relative hours for Today and Yesterday, or an exact date for longer ago. |
| 151 | """ |
| 152 | if now is None: |
| 153 | now = int(time.time()) |
| 154 | |
| 155 | now_year = datetime.datetime.fromtimestamp(now).year |
| 156 | then_year = datetime.datetime.fromtimestamp(timestamp).year |
| 157 | delta = int(now - timestamp) |
| 158 | delta_minutes = delta // 60 |
| 159 | delta_hours = delta_minutes // 60 |
| 160 | delta_days = delta_hours // 24 |
| 161 | |
| 162 | if 0 <= delta_hours < 24: |
| 163 | if delta_hours > 1: |
| 164 | return 'Today', '%s hours ago' % delta_hours |
| 165 | if delta_minutes > 1: |
| 166 | return 'Today', '%s min ago' % delta_minutes |
| 167 | if delta_minutes > 0: |
| 168 | return 'Today', '1 min ago' |
| 169 | if delta > 0: |
| 170 | return 'Today', 'moments ago' |
| 171 | if 0 <= delta_hours < 48: |
| 172 | return 'Yesterday', '%s hours ago' % delta_hours |
| 173 | if 0 <= delta_days < 7: |
| 174 | return 'Last 7 days', time.strftime( |
| 175 | '%b %d, %Y', (time.localtime(timestamp))) |
| 176 | if 0 <= delta_days < 30: |
| 177 | return 'Last 30 days', time.strftime( |
| 178 | '%b %d, %Y', (time.localtime(timestamp))) |
| 179 | if delta > 0: |
| 180 | if now_year == then_year: |
| 181 | return 'Earlier this year', time.strftime( |
| 182 | '%b %d, %Y', (time.localtime(timestamp))) |
| 183 | return ('Before this year', |
| 184 | time.strftime('%b %d, %Y', (time.localtime(timestamp)))) |
| 185 | if delta > -MAX_CLOCK_SKEW_SEC: |
| 186 | return 'Today', 'moments ago' |
| 187 | # Only say something is in the future if it is more than just clock skew. |
| 188 | return 'Future', 'Later' |