Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Surprising behaviour of Python date and timedelta subtraction

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
        ...
like image 856
gweis Avatar asked Jul 05 '17 23:07

gweis


1 Answers

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)

like image 114
Jon Kiparsky Avatar answered Nov 04 '22 10:11

Jon Kiparsky