Formatting timedelta objects [duplicate]
Asked Answered
A

7

66

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.

Archilochus answered 18/1, 2012 at 8:3 Comment(2)
@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.Catching
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 usedHedgehop
C
97

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.

Catching answered 18/1, 2012 at 8:39 Comment(8)
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.Stigmatism
@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 instead.Hedgehop
@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.Stigmatism
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?Hedgehop
See my answer regarding negative timedelta values.Edison
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:14Canvas
How can I support for months and years?Morley
Adding d['f'] = tdelta.microseconds is nice to support %f for microsecond formattingExegesis
S
32

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
Stigmatism answered 29/5, 2015 at 18:11 Comment(3)
Perfect ! ThanksPrejudicial
See my answer regarding negative timedelta values.Edison
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 errorEstablishment
B
23

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).

Balladry answered 18/1, 2012 at 8:53 Comment(0)
P
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
Poplar answered 25/7, 2013 at 0:20 Comment(2)
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))))Georginegeorglana
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.xPoplar
S
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))
Seale answered 18/1, 2012 at 8:53 Comment(2)
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://mcmap.net/q/66616/-attributeerror-when-using-quot-import-dateutil-quot-and-quot-dateutil-parser-parse-quot-but-no-problems-when-using-quot-from-dateutil-import-parser-quot I got that I should do >>> import dateutil.relativedelta ; print(dateutil.relativedelta.relativedelta), which prints <class 'dateutil.relativedelta.relativedelta'>Pyramidon
I believe you need to pip install python-dateutilDiscrete
W
9
def to_time(seconds):
  delta = datetime.timedelta(seconds=seconds)
  return str(delta.days) + 'd ' + (datetime.datetime.utcfromtimestamp(0) + delta).strftime('%H:%M')
Walley answered 7/12, 2016 at 6:5 Comment(0)
E
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'
Edison answered 12/3, 2018 at 0:40 Comment(2)
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.Stigmatism
@gumption, for example the timezone implementation of the pytz module creates negative timedeltas for timezones “left” of UTC 🤓. I’ve added two examples.Edison

© 2022 - 2024 — McMap. All rights reserved.