40
votes

Hi I need some help to understand why this is happening. I have a method to track 'time remaining' in an event program:

def get_program_time_budget(self):
    return self.estimated_duration-self.get_program_duration() 

All fine when the estimated_duration > self.get_program_duration() but when this goes the other way things get funny.

Results are displayed to the user:

Estimated   11 hours    Allocated       10 hours 55 minutes     Remaining       5 minutes

When the result goes negative it does this:

Estimated   11 hours    Allocated       11 hours 5 minutes  Remaining       -1 day 23 hours 55 minutes

Any ideas how to get the result -5 minutes?

EDIT: Here is the timedelta formatter (Note this is a Django filter, so receives the timedelta value as a str - but it is stored as a timedelta):

def format_duration(value):
  try:
    delim = ':'
    toks = value.split(',')
    hour = minute = ''
    d_string = value.count('day') and toks[0] or ''
    h, m, s = d_string and toks[-1].strip().split(delim) or value.split(delim)
    try:
        hour = int(h)
    except:
        pass
    try:
        minute = int(m)
    except:
        pass  
    h_string = "%s%s%s" % (hour and hour or '', (hour and ' hour' or ''),(hour and hour > 1 and 's' or '')  )
    m_string = "%s%s%s" % (minute and minute or '', (minute and ' minute' or ''),(minute and minute > 1 and 's' or ''))
    return "%s %s %s" % (d_string, h_string, m_string)
  except Exception, e:
    logging.error("Error in format_duration -> %s. Duration value=%s" % (e, value))
    return ''v 
3
This is the way timedelta works for negative values. Results are always normalized so that only the days value is negative. Would you want to negate the other fields if the days value was, say, -5? - Ray Toal
We know how to subtract two timedeltas. What we don't know is what code you used to display the result. For better advice, please divulge. - John Machin
If you want to work with negative timedelta values in a sane way ("-1 minute" is just "-1 minute" and not "-1 day plus 23h59"), you could use the relativetimedelta module present in dateutil. - florisla

3 Answers

59
votes

If you are using Python 2.7 or higher you can use timedelta.total_seconds() to get a float representation of the timedelta as a positive or negative number of seconds.

>>> datetime.timedelta(-1, 86100).total_seconds()
-300.0

You should be able to use this to calculate a number of minutes fairly easily.

If you are not using Python 2.7 you can use the following equivalent formula from the docs:

(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10.0**6

Edit: It looks like you are probably using the default string representation for timedelta to display the result, so my original answer may not be as useful. I would suggest something like this for displaying the result:

def get_program_time_budget(self):
    td = self.estimated_duration-self.get_program_duration()
    if td.days < 0:
        return '-' + str(datetime.timedelta() - td)
    return str(td)

This would now return a string instead of a timedelta, and for negative timedeltas it would prepend a '-' to a positive timedelta.

25
votes

Why?

Possibly as a unintended side effect of the way // and % are defined.

Possibly because it makes it easier to implement the datetime class. Five minutes before the epoch is 23:55, not 0:-5.

It doesn't really matter. Just know that it's how days, seconds, and microseconds get normalized. And that it can easily be worked around.

def format_timedelta(td):
    if td < timedelta(0):
        return '-' + format_timedelta(-td)
    else:
        # Change this to format positive timedeltas the way you want
        return str(td)

 >>> format_timedelta(timedelta(minutes=-5))
 '-0:05:00'
0
votes

Also if you are facing problems with a timedelta object containing negative values even though the values should be positive, you can use pythons builtin abs(td_object)

>>> current_time = datetime.datetime.now()
>>> fifteen_seconds = datetime.timedelta(seconds=15)
>>> time_delta_after_calculations = current_time - (current_time + fifteen_seconds)  # It should give a timedelta with 15 seconds but it does not
>>> time_delta_after_calculations
datetime.timedelta(days=-1, seconds=86385)
>>> # The above is kind of True but not what expected (A day contains 86400 seconds)
>>> abs(time_delta_after_calculations)  # Gives expected output
datetime.timedelta(seconds=15)