Last Updated: February 25, 2016
·
3.007K
· deontologician

Displaying Useful Relative Dates in Python

When creating interfaces, a lot of times an iso formatted date isn't what your users are really looking for. They want something to jog their memory so they can go "Oh, that's the file I uploaded yesterday" rather than "That's the file I uploaded October 3rd... hmm, was that before or after I went on vacation..."

Anyway, using Python's wonderful datetime library (and timedelta), here's some code to do really nice relative dates for you. (Also as a gist here: https://gist.github.com/3503910)

def reltime(date, compare_to=None, at='@'):
 r'''Takes a datetime and returns a relative representation of the
 time.
 :param date: The date to render relatively
 :param compare_to: what to compare the date to. Defaults to datetime.now()
 :param at: date/time separator. defaults to "@". "at" is also reasonable.

 >>> from datetime import datetime, timedelta
 >>> today = datetime(2050, 9, 2, 15, 00)
 >>> earlier = datetime(2050, 9, 2, 12)
 >>> reltime(earlier, today)
 'today @ 12pm'
 >>> yesterday = today - timedelta(1)
 >>> reltime(yesterday, compare_to=today)
 'yesterday @ 3pm'
 >>> reltime(datetime(2050, 9, 1, 15, 32), today)
 'yesterday @ 3:32pm'
 >>> reltime(datetime(2050, 8, 31, 16), today)
 'Wednesday @ 4pm (2 days ago)'
 >>> reltime(datetime(2050, 8, 26, 14), today)
 'last Friday @ 2pm (7 days ago)'
 >>> reltime(datetime(2049, 9, 2, 12, 00), today)
 'September 2nd, 2049 @ 12pm (last year)'
 >>> today = datetime(2012, 8, 29, 13, 52)
 >>> last_mon = datetime(2012, 8, 20, 15, 40, 55)
 >>> reltime(last_mon, today)
 'last Monday @ 3:40pm (9 days ago)'
 '''
 def ordinal(n):
 r'''Returns a string ordinal representation of a number
 Taken from: http://stackoverflow.com/a/739301/180718
 '''
 if 10 <= n % 100 < 20:
 return str(n) + 'th'
 else:
 return str(n) + {1 : 'st', 2 : 'nd', 3 : 'rd'}.get(n % 10, "th")

 compare_to = compare_to or datetime.now()
 if date > compare_to:
 return NotImplementedError('reltime only handles dates in the past')
 #get timediff values
 diff = compare_to - date
 if diff.seconds < 60 * 60 * 8: #less than a business day?
 days_ago = diff.days
 else:
 days_ago = diff.days + 1
 months_ago = compare_to.month - date.month
 years_ago = compare_to.year - date.year
 weeks_ago = int(math.ceil(days_ago / 7.0))
 #get a non-zero padded 12-hour hour
 hr = date.strftime('%I')
 if hr.startswith('0'):
 hr = hr[1:]
 wd = compare_to.weekday()
 #calculate the time string
 if date.minute == 0:
 time = '{0}{1}'.format(hr, date.strftime('%p').lower())
 else:
 time = '{0}:{1}'.format(hr, date.strftime('%M%p').lower())
 #calculate the date string
 if days_ago == 0:
 datestr = 'today {at} {time}'
 elif days_ago == 1:
 datestr = 'yesterday {at} {time}'
 elif (wd in (5, 6) and days_ago in (wd+1, wd+2)) or \
 wd + 3 <= days_ago <= wd + 8:
 #this was determined by making a table of wd versus days_ago and
 #divining a relationship based on everyday speech. This is somewhat
 #subjective I guess!
 datestr = 'last {weekday} {at} {time} ({days_ago} days ago)'
 elif days_ago <= wd + 2:
 datestr = '{weekday} {at} {time} ({days_ago} days ago)'
 elif years_ago == 1:
 datestr = '{month} {day}, {year} {at} {time} (last year)'
 elif years_ago > 1:
 datestr = '{month} {day}, {year} {at} {time} ({years_ago} years ago)'
 elif months_ago == 1:
 datestr = '{month} {day} {at} {time} (last month)'
 elif months_ago > 1:
 datestr = '{month} {day} {at} {time} ({months_ago} months ago)'
 else: 
 #not last week, but not last month either
 datestr = '{month} {day} {at} {time} ({days_ago} days ago)'
 return datestr.format(time=time,
 weekday=date.strftime('%A'),
 day=ordinal(date.day),
 days=diff.days,
 days_ago=days_ago,
 month=date.strftime('%B'),
 years_ago=years_ago,
 months_ago=months_ago,
 weeks_ago=weeks_ago,
 year=date.year,
 at=at)