# -*- test-case-name: omnipresence.test.test_humanize
"""Functions for presenting data in human-readable forms."""
from datetime import datetime, timedelta
import re
[docs]def ago(then, now=None):
"""Given a `~datetime.datetime` object, return an English string
giving an approximate relative time, such as "5 days ago"."""
if not now: # then when?
now = datetime.now()
delta = now - then
if delta.days == 0:
if delta.seconds < 10:
return 'just now'
if delta.seconds < 60:
return '{} seconds ago'.format(delta.seconds)
if delta.seconds < 120:
return 'a minute ago'
if delta.seconds < 3600:
return '{} minutes ago'.format(delta.seconds / 60)
if delta.seconds < 7200:
return 'an hour ago'
return '{} hours ago'.format(delta.seconds / 3600)
if delta.days == 1:
return 'yesterday'
if delta.days < 7:
return '{} days ago'.format(delta.days)
if delta.days < 14:
return 'a week ago'
return '{} weeks ago'.format(delta.days / 7)
[docs]def andify(seq, two_comma=False):
"""Join the elements of a sequence and return a string of the form
"*x* and *y*" for a two-element list, or "*x*, *y*, and *z*" for
three or more elements. If *two_comma* is True, insert a comma
before "and" even if the list is only two elements long ("*x*, and
*y*")."""
if len(seq) > 2:
return ', '.join(seq[:-2] + [', and '.join(seq[-2:])])
if two_comma:
return ', and '.join(seq)
return ' and '.join(seq)
DURATION_RE = re.compile(
r'^(?:(\d+)w)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$',
re.IGNORECASE)
DURATION_GROUPS = ['weeks', 'days', 'hours', 'minutes', 'seconds']
[docs]def duration_to_timedelta(duration):
"""Convert a duration string of the form "_w_d_h_m_s" into a
`~datetime.timedelta` object."""
match = DURATION_RE.match(duration)
if match is None:
return timedelta()
kwargs = dict(((DURATION_GROUPS[i], int(value, 10))
for (i, value) in enumerate(match.groups('0'))))
return timedelta(**kwargs)
[docs]def readable_duration(duration):
"""Convert a duration string of the form "_w_d_h_m_s" to a plain
English representation such as "2 weeks, 5 days, and 20 hours"."""
match = DURATION_RE.match(duration)
if match is None:
return 'instant'
components = []
for i, value in enumerate(match.groups()):
if not value:
continue
unit = DURATION_GROUPS[i]
value = int(value, 10)
if value == 1:
# Thankfully, all of these words are simple plurals.
if components:
# Add the value: "2 weeks, 1 day, 3 hours".
component = '1 {}'.format(unit[:-1])
else:
# Skip the value: "past day" instead of "past 1 day".
component = unit[:-1]
else:
component = '{} {}'.format(value, unit)
components.append(component)
return andify(components)