52

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?

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 
Neuron
  • 5,141
  • 5
  • 38
  • 59
Drew Nichols
  • 735
  • 1
  • 6
  • 13
  • 4
    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 Dec 06 '11 at 23:37
  • 3
    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 Dec 06 '11 at 23:40
  • 3
    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](http://labix.org/python-dateutil). – florisla Aug 29 '14 at 07:52

3 Answers3

74

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.

Mark E. Haase
  • 25,965
  • 11
  • 66
  • 72
Andrew Clark
  • 202,379
  • 35
  • 273
  • 306
  • Thanks. Great answer but I don't use the default str format. I have my own formatter (to strip seconds etc) so need a timedelta, not a str. Will try the formula from the docs to do total_seconds. – Drew Nichols Dec 07 '11 at 00:45
  • In that case you should change your formatter to convert to a positive timedelta and just at a '-' to the front of it. – Andrew Clark Dec 07 '11 at 00:46
  • Unfortunately the formatter code is a Django filter, so takes the timedelta.__str__ (or __unicode__) as the value - see question for code. – Drew Nichols Dec 07 '11 at 00:54
28

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'
dan04
  • 87,747
  • 23
  • 163
  • 198
2

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)
AmaanK
  • 1,032
  • 5
  • 25