Index: appengine/monorail/framework/timestr.py |
diff --git a/appengine/monorail/framework/timestr.py b/appengine/monorail/framework/timestr.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..5ec1ee8a69eebb8112e00ff35c4057a35159ae43 |
--- /dev/null |
+++ b/appengine/monorail/framework/timestr.py |
@@ -0,0 +1,184 @@ |
+# Copyright 2016 The Chromium Authors. All rights reserved. |
+# Use of this source code is govered by a BSD-style |
+# license that can be found in the LICENSE file or at |
+# https://developers.google.com/open-source/licenses/bsd |
+ |
+"""Time-to-string and time-from-string routines.""" |
+ |
+import datetime |
+import time |
+ |
+ |
+class Error(Exception): |
+ """Exception used to indicate problems with time routines.""" |
+ pass |
+ |
+ |
+HTML_TIME_FMT = '%a, %d %b %Y %H:%M:%S GMT' |
+ |
+MONTH_YEAR_FMT = '%b %Y' |
+MONTH_DAY_FMT = '%b %d' |
+MONTH_DAY_YEAR_FMT = '%b %d %Y' |
+ |
+# We assume that all server clocks are synchronized within this amount. |
+MAX_CLOCK_SKEW_SEC = 30 |
+ |
+ |
+def TimeForHTMLHeader(when=None): |
+ """Return the given time (or now) in HTML header format.""" |
+ if when is None: |
+ when = int(time.time()) |
+ return time.strftime(HTML_TIME_FMT, time.gmtime(when)) |
+ |
+ |
+def FormatAbsoluteDate( |
+ timestamp, clock=datetime.datetime.utcnow, |
+ recent_format=MONTH_DAY_FMT, old_format=MONTH_YEAR_FMT): |
+ """Format timestamp like 'Sep 5', or 'Yesterday', or 'Today'. |
+ |
+ Args: |
+ timestamp: Seconds since the epoch in UTC. |
+ clock: callable that returns a datetime.datetime object when called with no |
+ arguments, giving the current time to use when computing what to display. |
+ recent_format: Format string to pass to strftime to present dates between |
+ six months ago and yesterday. |
+ old_format: Format string to pass to strftime to present dates older than |
+ six months or more than skew_tolerance in the future. |
+ |
+ Returns: |
+ If timestamp's date is today, "Today". If timestamp's date is yesterday, |
+ "Yesterday". If timestamp is within six months before today, return the |
+ time as formatted by recent_format. Otherwise, return the time as formatted |
+ by old_format. |
+ """ |
+ ts = datetime.datetime.fromtimestamp(timestamp) |
+ now = clock() |
+ month_delta = 12 * now.year + now.month - (12 * ts.year + ts.month) |
+ delta = now - ts |
+ |
+ if ts > now: |
+ # If the time is slightly in the future due to clock skew, treat as today. |
+ skew_tolerance = datetime.timedelta(seconds=MAX_CLOCK_SKEW_SEC) |
+ if -delta <= skew_tolerance: |
+ return 'Today' |
+ # Otherwise treat it like an old date. |
+ else: |
+ fmt = old_format |
+ elif month_delta > 6 or delta.days >= 365: |
+ fmt = old_format |
+ elif delta.days == 1: |
+ return 'Yesterday' |
+ elif delta.days == 0: |
+ return 'Today' |
+ else: |
+ fmt = recent_format |
+ |
+ return time.strftime(fmt, time.localtime(timestamp)).replace(' 0', ' ') |
+ |
+ |
+def FormatRelativeDate(timestamp, recent_only=False, clock=None): |
+ """Return a short string that makes timestamp more meaningful to the user. |
+ |
+ Describe the timestamp relative to the current time, e.g., '4 |
+ hours ago'. In cases where the timestamp is more than 6 days ago, |
+ we simply show the year, so that the combined absolute and |
+ relative parts look like 'Sep 05, 2005'. |
+ |
+ Args: |
+ timestamp: Seconds since the epoch in UTC. |
+ recent_only: If True, only return a description of recent relative |
+ dates. Do not return the year, and do not put results inside parentheses. |
+ clock: optional function to return an int time, like int(time.time()). |
+ |
+ Returns: |
+ String describing relative time. |
+ """ |
+ if clock: |
+ now = clock() |
+ else: |
+ now = int(time.time()) |
+ |
+ # TODO(jrobbins): i18n of date strings |
+ delta = int(now - timestamp) |
+ d_minutes = delta // 60 |
+ d_hours = d_minutes // 60 |
+ d_days = d_hours // 24 |
+ if recent_only: |
+ if d_days > 6: |
+ return '' |
+ if d_days > 1: |
+ return '%s days ago' % d_days # starts at 2 days |
+ if d_hours > 1: |
+ return '%s hours ago' % d_hours # starts at 2 hours |
+ if d_minutes > 1: |
+ return '%s minutes ago' % d_minutes |
+ if d_minutes > 0: |
+ return '1 minute ago' |
+ if delta > -MAX_CLOCK_SKEW_SEC: |
+ return 'moments ago' |
+ return '' |
+ else: |
+ if d_days > 6: |
+ return ', %s' % (time.localtime(timestamp))[0] |
+ if d_days > 1: |
+ return ' (%s days ago)' % d_days # starts at 2 days |
+ if d_hours > 1: |
+ return ' (%s hours ago)' % d_hours # starts at 2 hours |
+ if d_minutes > 1: |
+ return ' (%s minutes ago)' % d_minutes |
+ if d_minutes > 0: |
+ return ' (1 minute ago)' |
+ if delta > -MAX_CLOCK_SKEW_SEC: |
+ return ' (moments ago)' |
+ # Only say something is in the future if it is more than just clock skew. |
+ return ' (in the future)' |
+ |
+ |
+def GetHumanScaleDate(timestamp, now=None): |
+ """Formats a timestamp to a course-grained and fine-grained time phrase. |
+ |
+ Args: |
+ timestamp: Seconds since the epoch in UTC. |
+ now: Current time in seconds since the epoch in UTC. |
+ |
+ Returns: |
+ A pair (course_grain, fine_grain) where course_grain is a string |
+ such as 'Today', 'Yesterday', etc.; and fine_grained is a string describing |
+ relative hours for Today and Yesterday, or an exact date for longer ago. |
+ """ |
+ if now is None: |
+ now = int(time.time()) |
+ |
+ now_year = datetime.datetime.fromtimestamp(now).year |
+ then_year = datetime.datetime.fromtimestamp(timestamp).year |
+ delta = int(now - timestamp) |
+ delta_minutes = delta // 60 |
+ delta_hours = delta_minutes // 60 |
+ delta_days = delta_hours // 24 |
+ |
+ if 0 <= delta_hours < 24: |
+ if delta_hours > 1: |
+ return 'Today', '%s hours ago' % delta_hours |
+ if delta_minutes > 1: |
+ return 'Today', '%s min ago' % delta_minutes |
+ if delta_minutes > 0: |
+ return 'Today', '1 min ago' |
+ if delta > 0: |
+ return 'Today', 'moments ago' |
+ if 0 <= delta_hours < 48: |
+ return 'Yesterday', '%s hours ago' % delta_hours |
+ if 0 <= delta_days < 7: |
+ return 'Last 7 days', time.strftime( |
+ '%b %d, %Y', (time.localtime(timestamp))) |
+ if 0 <= delta_days < 30: |
+ return 'Last 30 days', time.strftime( |
+ '%b %d, %Y', (time.localtime(timestamp))) |
+ if delta > 0: |
+ if now_year == then_year: |
+ return 'Earlier this year', time.strftime( |
+ '%b %d, %Y', (time.localtime(timestamp))) |
+ return 'Older', time.strftime('%b %d, %Y', (time.localtime(timestamp))) |
+ if delta > -MAX_CLOCK_SKEW_SEC: |
+ return 'Today', 'moments ago' |
+ # Only say something is in the future if it is more than just clock skew. |
+ return 'Future', 'Later' |