arrow-py / arrow

🏹 Better dates & times for Python
https://arrow.readthedocs.io
Apache License 2.0
8.73k stars 684 forks source link

incorrect behavior WRT daylight savings for shift/adding timedelta #1170

Open eigenvariable opened 10 months ago

eigenvariable commented 10 months ago

Issue Description

Arrow's shift function appears to handle daylight savings time incorrectly, or at least inconsistently. In particular, on spring-forward days, shifting forward by 2 hours from midnight is treated as being the same time as shifting 3 from midnight:

Python 3.10.12 | packaged by conda-forge | (main, Jun 23 2023, 22:41:52) [Clang 15.0.7 ] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import arrow
>>> from datetime import datetime
>>> x = arrow.get(datetime(2023, 3, 12), 'America/Los_Angeles')
>>> x.shift(hours=3) == x.shift(hours=2)
True
>>> print(x.shift(hours=2))
2023-03-12T03:00:00-07:00
>>> print(x.shift(hours=3))
2023-03-12T03:00:00-07:00

It looks to me like shifting by 2 hours is correct, but that shifting 3 is not. Perhaps this is the intended semantics of shift, but if that's the case then the following example seems inconsistent (shifting by 1 hour 3 times returns the result I expected for shifting 3 hours once):

>>> x.shift(hours=1).shift(hours=1).shift(hours=1)
<Arrow [2023-03-12T04:00:00-07:00]>
>>> x.shift(hours=1).shift(hours=1).shift(hours=1) == x.shift(hours=3) # I'd expect this to be true for any value of x
False

When I try adding timedelta(hours=1) I get different unintuitive behavior, this time when it comes to adding 1 hour or 2 hours:

>>> (x + timedelta(hours=1)).timestamp() == (x + timedelta(hours=2)).timestamp() # I'd expect this to be false
True
>>> x + timedelta(hours=1)
<Arrow [2023-03-12T01:00:00-08:00]> # this looks right to me
>>> x + timedelta(hours=2)
<Arrow [2023-03-12T02:00:00-07:00]> # I think this should be 2023-03-12T03:00:00-07:00

On fall-behind days, there are also problems. In this case using both shift and adding a timedelta do the same thing : the gap between midnight offsetby 1 hour and offset by 2 hours is 7200 seconds (2 hours):

>>> y = arrow.get(datetime(2023, 11, 5), 'America/Los_Angeles')
>>> y.shift(hours=1)
<Arrow [2023-11-05T01:00:00-07:00]>
>>> y.shift(hours=2)
<Arrow [2023-11-05T02:00:00-08:00]>
>>> y.shift(hours=2).timestamp() - y.shift(hours=1).timestamp() # I'd expect 3600
7200.0
>>> (y + timedelta(hours=2)).timestamp() - (y + timedelta(hours=1)).timestamp() # I'd expect 3600
7200.0

System Info