blob: b768c733fc972391f86052b99d50bfbc0405d546 [file] [log] [blame]
# Copyright 2016 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Time-to-string and time-from-string routines."""
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
import calendar
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'
HTML_DATE_WIDGET_FORMAT = '%Y-%m-%d'
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 TimestampToDateWidgetStr(when):
"""Format a timestamp int for use by HTML <input type="date">."""
return time.strftime(HTML_DATE_WIDGET_FORMAT, time.gmtime(when))
def DateWidgetStrToTimestamp(val_str):
"""Parse the HTML <input type="date"> string into a timestamp int."""
return int(calendar.timegm(time.strptime(val_str, HTML_DATE_WIDGET_FORMAT)))
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.utcfromtimestamp(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.gmtime(timestamp)).replace(' 0', ' ')
def FormatRelativeDate(timestamp, days_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 return '' so that an alternative display can be used instead.
Args:
timestamp: Seconds since the epoch in UTC.
days_only: If True, return 'N days ago' even for more than 6 days.
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 days_only:
if d_days > 1:
return '%s days ago' % d_days
else:
return ''
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 ''
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 ('Before this year',
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'