64

I have two datetime objects. I need to calculate the timedelta between them and then show the output in a specific format.

Alpha_TimeObj = datetime.datetime(int(AlphaTime.strftime('%Y')), int(AlphaTime.strftime('%m')), int(AlphaTime.strftime('%d')), int(AlphaTime.strftime('%H')), int(AlphaTime.strftime('%M')), int(AlphaTime.strftime('%S')))
Beta_TimeObj = datetime.datetime(int(BetaTime.strftime('%Y')), int(BetaTime.strftime('%m')), int(BetaTime.strftime('%d')), int(BetaTime.strftime('%H')), int(BetaTime.strftime('%M')), int(BetaTime.strftime('%S')))
Turnaround_TimeObj = Beta_TimeObj  - Alpha_TimeObj

An example of this Turnaround_TimeObj time delta is "2 days, 22:13:45". I want to format the output, but I am unable to do so.

print Turnaround_TimeObj.strftime('%H hrs %M mins %S secs')

doesn't work.

I know one way of doing this will be to convert it to seconds and then divmoding to get the required formatting.

As in:

totalSeconds = Turnaround_TimeObj.seconds
hours, remainder = divmod(totalSeconds, 3600)
minutes, seconds = divmod(remainder, 60)
print '%s:%s:%s' % (hours, minutes, seconds)

But I was wondering if I can do it in a single line using any date time function like strftime.

Actually converting to seconds doesn't work either. If I convert the time delta "1 day, 3:42:54" to seconds using:

totalSeconds = Turnaround_TimeObj.seconds

The totalSeconds value is shown as 13374 instead of 99774. i.e. it's ignoring the "day" value.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Rishav Sharan
  • 2,763
  • 8
  • 39
  • 55
  • 1
    @plaes not really an exact duplicate. OP as indirectly acknowledged that he has seen that question by quoting the answer from that Q. This question seems to be about getting an `strftime` equivalent, as well as including the "days" value not mentioned in the other question. – Shawn Chin Jan 18 '12 at 09:46
  • 1
    your code is fine if you use `.total_seconds()` (or its analog on earlier Python versions) instead of `.seconds`: [code example -- notice that `02d` format is used](http://stackoverflow.com/a/30477047/4279) – jfs May 30 '15 at 16:00

7 Answers7

94

But I was wondering if I can do it in a single line using any date time function like strftime.

As far as I can tell, there isn't a built-in method to timedelta that does that. If you're doing it often, you can create your own function, e.g.

def strfdelta(tdelta, fmt):
    d = {"days": tdelta.days}
    d["hours"], rem = divmod(tdelta.seconds, 3600)
    d["minutes"], d["seconds"] = divmod(rem, 60)
    return fmt.format(**d)

Usage:

>>> print strfdelta(delta_obj, "{days} days {hours}:{minutes}:{seconds}")
1 days 20:18:12
>>> print strfdelta(delta_obj, "{hours} hours and {minutes} to go")
20 hours and 18 to go

If you want to use a string format closer to the one used by strftime we can employ string.Template:

from string import Template

class DeltaTemplate(Template):
    delimiter = "%"

def strfdelta(tdelta, fmt):
    d = {"D": tdelta.days}
    d["H"], rem = divmod(tdelta.seconds, 3600)
    d["M"], d["S"] = divmod(rem, 60)
    t = DeltaTemplate(fmt)
    return t.substitute(**d)

Usage:

>>> print strfdelta(delta_obj, "%D days %H:%M:%S")
1 days 20:18:12
>>> print strfdelta(delta_obj, "%H hours and %M to go")
20 hours and 18 to go

The totalSeconds value is shown as 13374 instead of 99774. I.e. it's ignoring the "day" value.

Note in the example above that you can use timedelta.days to get the "day" value.

Alternatively, from Python 2.7 onwards, timedelta has a total_seconds() method which return the total number of seconds contained in the duration.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Shawn Chin
  • 84,080
  • 19
  • 162
  • 191
  • 4
    I like this solution, except for the lack of zero-padding of the hours, minutes and seconds. I've proposed a slight variant - with zero-padding - in another solution. – gumption May 29 '15 at 21:11
  • 1
    @gumption: your solution as well as this answer do not include `.days` attribute into the `hours` value. I don't understand why OP hasn't accepted [unutbu's answer](http://stackoverflow.com/a/8907407/4279) instead. – jfs May 30 '15 at 16:07
  • @J.F.Sebastian: I'm not sure what you mean. Both solutions include the timedelta.days attribute as the value of the "D" key in the dict. I agree that unutbu's solution is also a good one, but preferred the structuring of the solution into a class and method ... and the ability to use this in Python versions preceding 2.7. I generally prefer to show times & elapsed times using zero-padding for %H:%M:%S (consistent with the specification for datetime.strftime) ... which is why I proposed my variant. – gumption May 31 '15 at 17:28
  • 1
    Do you see the difference between *"27 hrs ..."* (unutbu's answer) and *"1 day, 3 hours ..."*? Why do you think OP was not satisfied with `str(timedelta)` and posted the question? – jfs May 31 '15 at 17:39
  • See [my answer](https://stackoverflow.com/questions/8906926/formatting-python-timedelta-objects#49226644) regarding _negative_ timedelta values. – Jens Mar 12 '18 at 00:42
  • I like to have a leading zero for minutes and seconds: `print strfdelta(timedelta(seconds=74), "{days} days {hours}:{minutes:02d}:{seconds:02d}") 0 days 0:01:14` – OLS Apr 10 '18 at 09:12
  • How can I support for months and years? – Razzer Jan 03 '22 at 13:51
  • Adding `d['f'] = tdelta.microseconds` is nice to support `%f` for microsecond formatting – Addison Klinke Jun 16 '22 at 19:21
31

A slight variant on Shawn Chin's answer - that also addresses a subsequent issue raised by mpouncett - pads the hours, minutes and seconds with leading zeros to ensure that all 3 elements use 2 places (more consistent with the specification for these fields in strftime):

from string import Template

class DeltaTemplate(Template):
    delimiter = "%"

def strfdelta(tdelta, fmt):
    d = {"D": tdelta.days}
    hours, rem = divmod(tdelta.seconds, 3600)
    minutes, seconds = divmod(rem, 60)
    d["H"] = '{:02d}'.format(hours)
    d["M"] = '{:02d}'.format(minutes)
    d["S"] = '{:02d}'.format(seconds)
    t = DeltaTemplate(fmt)
    return t.substitute(**d)

Here are is a test for some sample values:

from datetime import timedelta
for seconds in [0, 1, 59, 60, 61, 3599, 3600, 3601]:
    print strfdelta(timedelta(0, seconds), '%H:%M:%S')

And here is the output:

00:00:00
00:00:01
00:00:59
00:01:00
00:01:01
00:59:59
01:00:00
01:00:01
Community
  • 1
  • 1
gumption
  • 1,108
  • 13
  • 10
  • 1
    Perfect ! Thanks – Bruno Duyé Mar 01 '18 at 22:17
  • 1
    See [my answer](https://stackoverflow.com/questions/8906926/formatting-python-timedelta-objects#49226644) regarding _negative_ timedelta values. – Jens Mar 12 '18 at 00:41
  • 1
    Great proposition. I just regret that contrary to strftime the %H needs to be separated from other character by a specificly separating character, it does not work with a letter! "%Hh%M" returns an error – Raphaël Poli Dec 13 '18 at 10:35
22

In Python 2.7 or newer, you could use the total_seconds method:

import datetime as dt

turnaround = dt.timedelta(days = 1, hours = 3, minutes = 42, seconds = 54)

total_seconds = int(turnaround.total_seconds())
hours, remainder = divmod(total_seconds,60*60)
minutes, seconds = divmod(remainder,60)

print('{} hrs {} mins {} secs'.format(hours,minutes,seconds))

yields

27 hrs 42 mins 54 secs

In Python 2.6 or older, you could compute the total_seconds yourself:

total_seconds = turnaround.seconds + turnaround.days*24*60*60

(For the more general formula, including microseconds, see the link above).

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
18

Shawn Chin's answer is a good one, but one problem with it is that if you skip a particular element in your format (as in his second example which only has hours and minutes .. not days or seconds) then that time vanishes from your result. You can modify it to fix that issue and get more accurate results by only processing the keywords that actually appear in the format string.

class DeltaTemplate(Template):
    delimiter = '%'

def strfdelta(tdelta, fmt):
    d = {}
    l = {'D': 86400, 'H': 3600, 'M': 60, 'S': 1}
    rem = int(tdelta.total_seconds())

    for k in ( 'D', 'H', 'M', 'S' ):
        if "%{}".format(k) in fmt:
            d[k], rem = divmod(rem, l[k])

    t = DeltaTemplate(fmt)
    return t.substitute(**d)

Usage:

>>> print strfdelta(delta_obj, "%D days %H:%M:%S")
1 days 20:18:12
>>> print strfdelta(delta_obj, "%H hours and %M to go")
44 hours and 18 to go

That's inflexible about formatting though, as you can't apply any conversion strings and wind up with ugly things like this:

>>> delta_obj = timedelta(minutes=5, seconds=2)
>>> print strfdelta(delta_obj, "%H:%M:%S")
0:5:2

However, you can take the same approach and apply it to string.Formatter instead of string.Template and get something much better.

from string import Formatter

def strfdelta(tdelta, fmt):
    f = Formatter()
    d = {}
    l = {'D': 86400, 'H': 3600, 'M': 60, 'S': 1}
    k = map( lambda x: x[1], list(f.parse(fmt)))
    rem = int(tdelta.total_seconds())

    for i in ('D', 'H', 'M', 'S'):
        if i in k and i in l.keys():
            d[i], rem = divmod(rem, l[i])

    return f.format(fmt, **d)

Usage:

>>> delta_obj = timedelta(days=1, hours=20, minutes=18, seconds=12)
>>> print strfdelta(delta_obj, "{D} days {H}:{M}:{S}")
1 days 20:18:12
>>> print strfdelta(delta_obj, "{H} hours and {M} to go")
44 hours and 18 to go

>>> delta_obj = timedelta(minutes=5, seconds=2)
>>> print strfdelta(delta_obj, "{H:02}h{M:02}m{S:02}s")
00h05m02s
>>> print strfdelta(delta_obj, "{H:02}:{M:02}:{S:02}")
00:05:02
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
mpounsett
  • 1,174
  • 1
  • 10
  • 30
  • Oddly, this did not work for me as written, I had to change `map( lambda x: x[1], list(f.parse(fmt)))` to `list(map( lambda x: x[1], list(f.parse(fmt))))` – ndw Aug 23 '17 at 18:19
  • Given the amount of time that has passed since that was written (2013), it could be a version incompatibility. I don't recall for sure now, but I may not have tested those examples in python 3.x – mpounsett Aug 23 '17 at 22:29
12

You could use the dateutil module which has the friendlier relativedelta object:

import dateutil
import datetime

alpha = datetime.datetime(2012, 1, 16, 6, 0)
beta = datetime.datetime(2012, 1, 18, 10, 42, 57, 230301)
delta = dateutil.relativedelta(beta, alpha)

This gives you an object delta that looks like:

>>> delta
relativedelta(days=+2, hours=+4, minutes=+42, seconds=+57, microseconds=+230301)

You can then do

print('turnaround %i hrs %i mins %i secs' % (delta.days * 24 + delta.hours, delta.minutes, delta.seconds))
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
dabhaid
  • 3,849
  • 22
  • 30
  • 1
    Interesting, but it does not work for me as written: `>>> import dateutil ; print(dateutil.relativedelta)` fails for me with `AttributeError: module 'dateutil' has no attribute 'relativedelta'` (Python 3.5 on Raspbian, and Python 3.8 on Mingw64). Via https://stackoverflow.com/q/23385003 I got that I should do `>>> import dateutil.relativedelta ; print(dateutil.relativedelta.relativedelta)`, which prints `` – sdbbs Sep 21 '20 at 06:36
  • I believe you need to `pip install python-dateutil` – Chad Lowe Oct 13 '20 at 18:46
9
def to_time(seconds):
  delta = datetime.timedelta(seconds=seconds)
  return str(delta.days) + 'd ' + (datetime.datetime.utcfromtimestamp(0) + delta).strftime('%H:%M')
Daniel F
  • 13,684
  • 11
  • 87
  • 116
9

The previous answers don’t seem to handle negative timedelta values (as produced e.g. by pytz) for timezones “left” of UTC). As per documentation, timedelta objects are normalized, and a negative timedelta is represented by a negative day instance attribute. From the documentation:

Note that normalization of negative values may be surprising at first.

and that

String representations of timedelta objects are normalized similarly to their internal representation. This leads to somewhat unusual results for negative timedeltas.

For example:

>>> td = timedelta(seconds=-30)
>>> str(td)
'-1 day, 23:59:30'
>>> repr(td)
'datetime.timedelta(-1, 86370)'

Given this example timedelta, both Shawn Chin’s accepted answer and gumption’s answer return '23:59:30', and mpounsett’s answer '-1:59:30'.

I think in order to print both negative and positive timedeltas in a more readable manner, we need to handle the sign and absolute value of a timezone object explicitly:

def strfdelta(td, fmt):

    # Get the timedelta’s sign and absolute number of seconds.
    sign = "-" if td.days < 0 else "+"
    secs = abs(td).total_seconds()

    # Break the seconds into more readable quantities.
    days, rem = divmod(secs, 86400)  # Seconds per day: 24 * 60 * 60
    hours, rem = divmod(rem, 3600)  # Seconds per hour: 60 * 60
    mins, secs = divmod(rem, 60)

    # Format (as per above answers) and return the result string.
    t = DeltaTemplate(fmt)
    return t.substitute(
        s=sign,
        D="{:d}".format(int(days)),
        H="{:02d}".format(int(hours)),
        M="{:02d}".format(int(mins)),
        S="{:02d}".format(int(secs)),
        )

This function returns a more readable string representation:

>>> strfdelta(td, "%s%H:%M:%S")  # Note that %s refers to the timedelta’s sign.
'-00:00:30'
>>> strfdelta(timedelta(days=-1), "%s%D %H:%M:%S")
'-1 00:00:00'
>>> strfdelta(timedelta(days=-1, minutes=5), "%s%D %H:%M:%S")
'-0 23:55:00'
>>> strfdelta(timedelta(days=-1, minutes=-5), "%s%D %H:%M:%S")
'-1 00:05:00'

…or in the more practical context of timezones:

>>> import pytz
>>> import datetime
>>> td = pytz.timezone("Canada/Newfoundland").utcoffset(datetime.datetime.now())
>>> td
datetime.timedelta(-1, 77400)
>>> strfdelta(td, fmt="%s%H:%M")
'-02:30'
>>> td = pytz.timezone("Australia/Eucla").utcoffset(datetime.datetime.now())
>>> td
datetime.timedelta(0, 31500)
>>> strfdelta(td, fmt="%s%H:%M")
'+08:45'
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Jens
  • 8,423
  • 9
  • 58
  • 78
  • 1
    Thanks for noting the shortcomings - and offering an alternative - in formatting negative timedeltas. I have never encountered a scenario involving negative timedeltas, but I agree it would be best to accommodate them correctly. – gumption Mar 13 '18 at 01:01
  • 1
    @gumption, for example the timezone implementation of the pytz module creates negative timedeltas for timezones “left” of UTC . I’ve added two examples. – Jens Mar 13 '18 at 07:05