Formatting timedelta objects
Question:
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.
Answers:
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.
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).
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))
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
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
def to_time(seconds):
delta = datetime.timedelta(seconds=seconds)
return str(delta.days) + 'd ' + (datetime.datetime.utcfromtimestamp(0) + delta).strftime('%H:%M')
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'
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.
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.
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).
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))
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
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
def to_time(seconds):
delta = datetime.timedelta(seconds=seconds)
return str(delta.days) + 'd ' + (datetime.datetime.utcfromtimestamp(0) + delta).strftime('%H:%M')
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'