blob: 2b32e8c1a0919b8a63f2d161ebc59b1d67dca420 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001# 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."""
7from __future__ import print_function
8from __future__ import division
9from __future__ import absolute_import
10
11import calendar
12import datetime
13import time
14
15
16class Error(Exception):
17 """Exception used to indicate problems with time routines."""
18 pass
19
20
21HTML_TIME_FMT = '%a, %d %b %Y %H:%M:%S GMT'
22HTML_DATE_WIDGET_FORMAT = '%Y-%m-%d'
23
24MONTH_YEAR_FMT = '%b %Y'
25MONTH_DAY_FMT = '%b %d'
26MONTH_DAY_YEAR_FMT = '%b %d %Y'
27
28# We assume that all server clocks are synchronized within this amount.
29MAX_CLOCK_SKEW_SEC = 30
30
31
32def 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
39def 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
44def 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
49def 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
94def 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
140def 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'