8

I am trying to do some Python date and timedelta maths and stumbled upon this.

>>> import datetime
>>> dt = datetime.date(2000, 4, 20)
>>> td = datetime.timedelta(days=1)
>>> dt - td
datetime.date(2000, 4, 19)
>>> -(td) + dt
datetime.date(2000, 4, 19)
>>> dt - td == dt + (-td)
True

So far so good, but when the timedelta also includes some hours it gets interesting.

>>> td = datetime.timedelta(days=1, hours=1)
>>> dt - td
datetime.date(2000, 4, 19)
>>> -(td) + dt
datetime.date(2000, 4, 18)

or in a comparison:

>>> dt - td == dt + (-td)
False

I would have expected that a - b == a + (-b), but this doesn't seem to work for date and timedelta. As far as I was able to track that down, this happens because adding/subtracting date and timedelta only considers the days field of timedelta, which is probably correct. However negating a timedelta considers all fields and may change the days field as well.

>>> -datetime.timedelta(days=1)
datetime.timedelta(-1)
>>> -datetime.timedelta(days=1, hours=1)
datetime.timedelta(-2, 82800)

As can be seen in the second example, days=-2 after the negation, and therefore date + timedelta will actually subtract 2 days.

Should this be considered a bug in the python datetime module? Or is this rather some 'normal' behaviour which needs to be taken into account when doing things like that?

Internally the datetime module creates a new timedelta, with just the days field of the original timedelta object passed in, when subtracting a timedelta to a date object. Which equates to following, code that seems to be quite odd.

>>> dt + datetime.timedelta(-(-(-dt).days))
datetime.date(2000, 4, 18)

I can't really sea a reason for just using the negated days field when doing date - timedelta subtractions.

Edit:

Here is the relevant code path in python datetime module:

class date:

    ...

    def __sub__(self, other):
        """Subtract two dates, or a date and a timedelta."""
        if isinstance(other, timedelta):
           return self + timedelta(-other.days)
        ...

If it would just pass on -other then the condition a - b == a + (-b) would hold true. (It would change current behaviour though).

class date:

    ...

    def __sub__(self, other):
        """Subtract two dates, or a date and a timedelta."""
        if isinstance(other, timedelta):
           return self - other  # timedelta.__rsub__ would take care of negating other
        ...
gweis
  • 116
  • 7
  • 1
    I think it's worth asking what it means to add/subtract _hours_ to a `date`. If the hours are 24, then it's pretty easy to decide that you're supposed to add 1 day. If it's less than 24 hours, do you upgrade the result to a `datetime` when you started with a `date`, or do you keep working with `date` and truncate? – mgilson Jul 05 '17 at 23:37
  • Yes, that is a good question, and one that can't be answered easily. But what I wanted to highlight is, that normal maths operation like *a - b == a + (-b)* do not work as expected. – gweis Jul 05 '17 at 23:42
  • Yes, fixed that. thanks. – gweis Jul 05 '17 at 23:57

2 Answers2

2

Should this be considered a bug in the python datetime module? Or is this rather some 'normal' behaviour which needs to be taken into account when doing things like that?

No, this should not be considered a bug. A date does not track its state in terms of hours, minutes, and seconds, which is what would be needed for it to behave in the way you suggest it ought to.

I would consider the code you've presented to be a bug: the programmer is using the wrong datatype for the work they're trying to accomplish. If you want to keep track of time in days, hours, minutes and seconds, then you need a datetime object. (which will happily provide you with a date once you've done all of the arithmetic you care to do)

Jon Kiparsky
  • 7,499
  • 2
  • 23
  • 38
  • Agreed. However using datetime will be a hard option. Which time of the day should you pick? (There are many discussions about this here on Stack Overflow). Maybe date/timedelta operations should throw an error if anything but days (in timedalta) is non zero? It is using a wrong type after all. – gweis Jul 06 '17 at 19:12
  • I agree that it's hard, but hard in the sense of "you have to figure out what you really mean", not hard in the sense of "it's behaving incorrectly". If you're subtracting times from dates, you really do need to tell me what time you're starting from on that date. – Jon Kiparsky Jul 06 '17 at 19:13
0

This is because of the way how negative timedeltas are represented.

import datetime
td = datetime.timedelta(days=1, hours=1)
print (td.days, td.seconds)
# prints 1 3600
minus_td = -td
print (minus_td.days, minus_td.seconds)
# prints -2 82800

I hope you now better understand why days were affected.

Seconds in a timedelta are always normalized to a positive amount between 0 and 86399:

>>> print (datetime.timedelta(seconds=-10).seconds)
86390
9000
  • 39,899
  • 9
  • 66
  • 104
  • Maybe I'm missing something, but the issue doesn't seem to be about whether the `timedelta` is negative or positive. Note that when the minuend is a `datetime` instead of a `date`, `timedelta` arithmetic works as expected with both positive and negative values, and the unexpected behavior is observed when `timedelta`s of positive and negative value are added to or subtracted from `date`s. – Jon Kiparsky Jul 06 '17 at 02:16
  • This answer adds some clarity why it happens, but it also suggests that the datetime.timedelta implementation is the cause for this unforeseen behaviour. – gweis Jul 06 '17 at 03:17
  • @JonKiparsky: as you rightly mentioned, a semantic type mismatch, together with duck typing and (likely) backwards compatibility considerations leads to `date` objects accepting `timedelta` with non-zero time, and unexpected results. By the same token, `1 < '1'` in Python 2. Python 3 somehow alleviates this, and `1 < '1'` becomes a `TypeError`; the `date` and `timedelta` behavior remains the same, though. – 9000 Jul 06 '17 at 04:44