69

This question is similar to this question about subtracting dates with Python, but not identical. I'm not dealing with strings, I have to figure out the difference between two epoch time stamps and produce the difference in a human readable format.

For instance:

32 Seconds
17 Minutes
22.3 Hours
1.25 Days
3.5 Weeks
2 Months
4.25 Years

Alternately, I'd like to express the difference like this:

4 years, 6 months, 3 weeks, 4 days, 6 hours 21 minutes and 15 seconds

I don't think I can use strptime, since I'm working with the difference of two epoch dates. I could write something to do this, but I'm quite sure that there's something already written that I could use.

What module would be appropriate? Am I just missing something in time? My journey into Python is just really beginning, if this is indeed a duplicate it's because I failed to figure out what to search for.

Addendum

For accuracy, I really care most about the current year's calendar.

Community
  • 1
  • 1
Tim Post
  • 33,371
  • 15
  • 110
  • 174
  • By UNIX epoch date you mean the usual number of seconds since X? – Cat Plus Plus Jul 04 '11 at 17:16
  • 1
    How accurate do you want the month / year calculation to be? It can get complicated since the number of days per month and per year can vary. – GWW Jul 04 '11 at 17:20

8 Answers8

62

You can use the wonderful dateutil module and its relativedelta class:

import datetime
import dateutil.relativedelta

dt1 = datetime.datetime.fromtimestamp(123456789) # 1973-11-29 22:33:09
dt2 = datetime.datetime.fromtimestamp(234567890) # 1977-06-07 23:44:50
rd = dateutil.relativedelta.relativedelta (dt2, dt1)

print "%d years, %d months, %d days, %d hours, %d minutes and %d seconds" % (rd.years, rd.months, rd.days, rd.hours, rd.minutes, rd.seconds)
# 3 years, 6 months, 9 days, 1 hours, 11 minutes and 41 seconds

It doesn't count weeks, but that shouldn't be too hard to add.

Schnouki
  • 7,527
  • 3
  • 33
  • 38
  • 1
    Thank you! This works perfectly. Expressing weeks as days isn't a problem. – Tim Post Jul 04 '11 at 17:44
  • 4
    Is there a simple way to display only the non 0 units and to pluralize automatically ? like "1 year, 2 minutes and 1 second" – Pierre de LESPINAY Aug 10 '12 at 08:43
  • @PierredeLESPINAY You can ternary the `rd.second` as a final printf argument and and replace the `s` in `seconds` with `%s`, it's either 's' or '' depending on plurality. – Tim Post Jun 26 '14 at 07:36
56

A little improvement over @Schnouki's solution with a single line list comprehension. Also displays the plural in case of plural entities (like hours)

Import relativedelta

>>> from dateutil.relativedelta import relativedelta

A lambda function

>>> attrs = ['years', 'months', 'days', 'hours', 'minutes', 'seconds']
>>> human_readable = lambda delta: ['%d %s' % (getattr(delta, attr), attr if getattr(delta, attr) > 1 else attr[:-1]) 
...     for attr in attrs if getattr(delta, attr)]

Example usage:

>>> human_readable(relativedelta(minutes=125))
['2 hours', '5 minutes']
>>> human_readable(relativedelta(hours=(24 * 365) + 1))
['365 days', '1 hour']
APerson
  • 8,140
  • 8
  • 35
  • 49
Sharoon Thomas
  • 1,816
  • 15
  • 12
  • had to add 'microseconds' to `attrs` for really short times to avoid errors, but great method. – arturomp Jun 30 '20 at 19:21
  • This also helps avoid creating datetime objects from timestamps when you can just store the epoch timings directly. So then you can say `relativedelta(seconds=(t1 - t0))`. – Noein Sep 11 '20 at 22:31
23

I had that exact same problem earlier today and I couldn't find anything in the standard libraries that I could use, so this is what I wrote:

humanize_time.py

    #!/usr/bin/env python

    INTERVALS = [1, 60, 3600, 86400, 604800, 2419200, 29030400]
    NAMES = [('second', 'seconds'),
             ('minute', 'minutes'),
             ('hour', 'hours'),
             ('day', 'days'),
             ('week', 'weeks'),
             ('month', 'months'),
             ('year', 'years')]

    def humanize_time(amount, units):
    """
    Divide `amount` in time periods.
    Useful for making time intervals more human readable.

    >>> humanize_time(173, 'hours')
    [(1, 'week'), (5, 'hours')]
    >>> humanize_time(17313, 'seconds')
    [(4, 'hours'), (48, 'minutes'), (33, 'seconds')]
    >>> humanize_time(90, 'weeks')
    [(1, 'year'), (10, 'months'), (2, 'weeks')]
    >>> humanize_time(42, 'months')
    [(3, 'years'), (6, 'months')]
    >>> humanize_time(500, 'days')
    [(1, 'year'), (5, 'months'), (3, 'weeks'), (3, 'days')]
    """
       result = []

       unit = map(lambda a: a[1], NAMES).index(units)
       # Convert to seconds
       amount = amount * INTERVALS[unit]

       for i in range(len(NAMES)-1, -1, -1):
          a = amount / INTERVALS[i]
          if a > 0:
             result.append( (a, NAMES[i][1 % a]) )
             amount -= a * INTERVALS[i]

       return result

    if __name__ == "__main__":
        import doctest
        doctest.testmod()

You can use dateutil.relativedelta() to calculate the accurate time delta and humanize it with this script.

Graeme Stuart
  • 5,837
  • 2
  • 26
  • 46
Liudmil Mitev
  • 464
  • 2
  • 6
  • I'm curious as to where you are getting your numbers for seconds in Weeks, Months, and Years. – QA Automator Jul 11 '11 at 16:34
  • I wrote it as `[1, 60, 60*60, 24*60*60, 7*24*60*60 ... ]` originally to be more obvious but I thought that it looked long and annoying, so I changed it to this. The current code is merely the result from the multiplications. – Liudmil Mitev Jul 14 '11 at 13:21
  • How can we handle `amount` values that have decimal places? Eg: `humanize_time(60.5, 'seconds')` should give `1 minute, 0.5 seconds` – Nyxynyx Mar 13 '14 at 11:37
14
def humanize_time(amount, units = 'seconds'):    

    def process_time(amount, units):

        INTERVALS = [   1, 60, 
                        60*60, 
                        60*60*24, 
                        60*60*24*7, 
                        60*60*24*7*4, 
                        60*60*24*7*4*12, 
                        60*60*24*7*4*12*100,
                        60*60*24*7*4*12*100*10]
        NAMES = [('second', 'seconds'),
                 ('minute', 'minutes'),
                 ('hour', 'hours'),
                 ('day', 'days'),
                 ('week', 'weeks'),
                 ('month', 'months'),
                 ('year', 'years'),
                 ('century', 'centuries'),
                 ('millennium', 'millennia')]

        result = []

        unit = map(lambda a: a[1], NAMES).index(units)
        # Convert to seconds
        amount = amount * INTERVALS[unit]

        for i in range(len(NAMES)-1, -1, -1):
            a = amount // INTERVALS[i]
            if a > 0: 
                result.append( (a, NAMES[i][1 % a]) )
                amount -= a * INTERVALS[i]

        return result

    rd = process_time(int(amount), units)
    cont = 0
    for u in rd:
        if u[0] > 0:
            cont += 1

    buf = ''
    i = 0
    for u in rd:
        if u[0] > 0:
            buf += "%d %s" % (u[0], u[1])
            cont -= 1

        if i < (len(rd)-1):
            if cont > 1:
                buf += ", "
            else:
                buf += " and "

        i += 1

    return buf

Example of use:

>>> print humanize_time(234567890 - 123456789)
3 years, 9 months, 3 weeks, 5 days, 11 minutes and 41 seconds
>>> humanize_time(9, 'weeks')
2 months and 1 week

Advantage (You don't need third parties!).

Improved from "Liudmil Mitev" algorithm. (Thanks!)

makiolo
  • 176
  • 1
  • 4
  • I like no new modules must be installed. – Barny Jun 11 '19 at 13:16
  • till week it is good, but the moment it starts calculating month and year, It became higly inaccurate, As per interval logic, your month is going to be 28 days and year is going to be 336 days. – Vikash Singh Nov 22 '20 at 03:25
12

Check out the humanize package

https://github.com/jmoiron/humanize

import datetime

humanize.naturaltime(datetime.datetime.now() - datetime.timedelta(seconds=1))

'a second ago'

humanize.naturaltime(datetime.datetime.now() - datetime.timedelta(seconds=3600))

'an hour ago'
Joshua
  • 5,336
  • 1
  • 28
  • 42
9

Old question, but I personally like this approach most:

import datetime
import math

def human_time(*args, **kwargs):
    secs  = float(datetime.timedelta(*args, **kwargs).total_seconds())
    units = [("day", 86400), ("hour", 3600), ("minute", 60), ("second", 1)]
    parts = []
    for unit, mul in units:
        if secs / mul >= 1 or mul == 1:
            if mul > 1:
                n = int(math.floor(secs / mul))
                secs -= n * mul
            else:
                n = secs if secs != int(secs) else int(secs)
            parts.append("%s %s%s" % (n, unit, "" if n == 1 else "s"))
    return ", ".join(parts)

human_time(seconds=3721)
# -> "1 hour, 2 minutes, 1 second"

If you want to separate the seconds part with an "and" do:

"%s and %s" % tuple(human_time(seconds=3721).rsplit(", ", 1))
# -> "1 hour, 2 minutes and 1 second"
Riga
  • 1,049
  • 10
  • 6
7

Here's a shorter one for interval in seconds and within a day (t<86400). Useful if you work with unix timestamps (seconds since epoch, UTC).

t = 45678
print('%d hours, %d minutes, %d seconds' % (t//3600, t%3600//60, t%60))

May be extended further (t//86400, ...).

Andor
  • 5,523
  • 5
  • 26
  • 24
5

A very old question but I found this solution which seems to be very simple in Python3:

print(datetime.timedelta(seconds=3600))
# output: 1:00:00
print(datetime.timedelta(hours=360.1245))
# output: 15 days, 0:07:28.200000
Elendil
  • 120
  • 1
  • 7