import calendar import datetime from django.utils.html import avoid_wrapping from django.utils.timezone import is_aware, utc from django.utils.translation import gettext, ngettext_lazy TIME_STRINGS = { 'year': ngettext_lazy('%(num)d year', '%(num)d years', 'num'), 'month': ngettext_lazy('%(num)d month', '%(num)d months', 'num'), 'week': ngettext_lazy('%(num)d week', '%(num)d weeks', 'num'), 'day': ngettext_lazy('%(num)d day', '%(num)d days', 'num'), 'hour': ngettext_lazy('%(num)d hour', '%(num)d hours', 'num'), 'minute': ngettext_lazy('%(num)d minute', '%(num)d minutes', 'num'), } TIMESINCE_CHUNKS = ( (60 * 60 * 24 * 365, 'year'), (60 * 60 * 24 * 30, 'month'), (60 * 60 * 24 * 7, 'week'), (60 * 60 * 24, 'day'), (60 * 60, 'hour'), (60, 'minute'), ) def timesince(d, now=None, reversed=False, time_strings=None, depth=2): """ Take two datetime objects and return the time between d and now as a nicely formatted string, e.g. "10 minutes". If d occurs after now, return "0 minutes". Units used are years, months, weeks, days, hours, and minutes. Seconds and microseconds are ignored. Up to `depth` adjacent units will be displayed. For example, "2 weeks, 3 days" and "1 year, 3 months" are possible outputs, but "2 weeks, 3 hours" and "1 year, 5 days" are not. `time_strings` is an optional dict of strings to replace the default TIME_STRINGS dict. `depth` is an optional integer to control the number of adjacent time units returned. Adapted from https://web.archive.org/web/20060617175230/http://blog.natbat.co.uk/archive/2003/Jun/14/time_since """ if time_strings is None: time_strings = TIME_STRINGS if depth <= 0: raise ValueError('depth must be greater than 0.') # Convert datetime.date to datetime.datetime for comparison. if not isinstance(d, datetime.datetime): d = datetime.datetime(d.year, d.month, d.day) if now and not isinstance(now, datetime.datetime): now = datetime.datetime(now.year, now.month, now.day) now = now or datetime.datetime.now(utc if is_aware(d) else None) if reversed: d, now = now, d delta = now - d # Deal with leapyears by subtracing the number of leapdays leapdays = calendar.leapdays(d.year, now.year) if leapdays != 0: if calendar.isleap(d.year): leapdays -= 1 elif calendar.isleap(now.year): leapdays += 1 delta -= datetime.timedelta(leapdays) # ignore microseconds since = delta.days * 24 * 60 * 60 + delta.seconds if since <= 0: # d is in the future compared to now, stop processing. return avoid_wrapping(time_strings['minute'] % {'num': 0}) for i, (seconds, name) in enumerate(TIMESINCE_CHUNKS): count = since // seconds if count != 0: break else: return avoid_wrapping(time_strings['minute'] % {'num': 0}) result = [] current_depth = 0 while i < len(TIMESINCE_CHUNKS) and current_depth < depth: seconds, name = TIMESINCE_CHUNKS[i] count = since // seconds if count == 0: break result.append(avoid_wrapping(time_strings[name] % {'num': count})) since -= seconds * count current_depth += 1 i += 1 return gettext(', ').join(result) def timeuntil(d, now=None, time_strings=None, depth=2): """ Like timesince, but return a string measuring the time until the given time. """ return timesince(d, now, reversed=True, time_strings=time_strings, depth=depth)