arrow-py / arrow

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

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

Open eigenvariable opened 5 months ago

eigenvariable commented 5 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