Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save pablogsal/38095b4f0aacb8a692f28ae8316c84dd to your computer and use it in GitHub Desktop.
Save pablogsal/38095b4f0aacb8a692f28ae8316c84dd to your computer and use it in GitHub Desktop.
timezones
#!/usr/bin/env python3
import pytz
from zoneinfo import ZoneInfo
from datetime import datetime, timedelta, timezone
from dateutil import tz
from zoneinfo import ZoneInfo
def time_travel():
tzlondon = ZoneInfo("Europe/London")
start_date = datetime(2019, 3, 31, 0, 59, tzinfo=tzlondon)
# according to:
# - https://stackoverflow.com/questions/64440016/python-3-9-construct-dst-valid-timestamp-using-standard-library/64452687#64452687
# - https://blog.ganssle.io/articles/2018/02/aware-datetime-arithmetic.html
# a timedelta should be considered as moving the clock forward by X amount of time, in absolute terms
# which means it may return a local time that does not exist
#
# furthermore, the second link mentions timedelta addition should not modify the timezone,
# but it will do so in this example
print(f"Starting from {start_date.isoformat()}")
end_date = start_date + timedelta(minutes=1)
# https://www.python.org/dev/peps/pep-0495
# as per the documentation, if (on a short day):
# - fold=0 (default), the timeline from *before* the transition is brought forward
# - fold=1, the timeline from *after* the transition is brought back
#
# as per docs
assert end_date.fold == 0
# meaning that
assert end_date.utcoffset() == timedelta(seconds=0)
# prints 2019-03-31T01:00:00+00:00
# this time does not exist in local time! As mentioned above, this is "by design"
# but we are still linear. We have increased by 60 absolute seconds
print(f"Add 1 minute, and we get {end_date.isoformat()}")
# however, if we add 61 minutes instead...
new_end_date = start_date + timedelta(minutes=61)
# prints 2019-03-31T02:00:00+01:00
# this makes more sense in local time...
# but contradicts the statement above. We have added 3660 seconds (61 minutes), but we
# get a time back that is only 60 seconds past the start date in absolute terms
# (02:00:00+01:00 is the same as 01:00:00+00:00)
print(f"Add 61 minutes, and we get {new_end_date.isoformat()}")
# which has the right UTC offset now
assert new_end_date.utcoffset() == timedelta(seconds=3600)
# and since we're now fully on the new timeline, fold is still zero
assert new_end_date.fold == 0
# At this point we have:
# end_date - built by adding 1 minute:
# =01:00:00+00:00, which does NOT exist as a local time
#
# new_end_date - built by adding 61 minutes:
# =02:00:00+01:00, which DOES exist as a local time
# ... but is the same as the 01:00:00+00:00 we got by adding just 1 minute
#
# If we compare their absolute time differences, by adding 61 minutes, we went up by....
# 60 seconds
assert (new_end_date.timestamp() - start_date.timestamp()) == 60
# and in fact, the timestamps we get by adding 1 minute or 61 minutes are identical
assert (new_end_date.timestamp() == end_date.timestamp())
def no_time_travel_pytz():
tzlondon = pytz.timezone("Europe/London")
start_date = tzlondon.localize(datetime(2019, 3, 31, 0, 59))
print(f"Starting from {start_date.isoformat()}")
end_date = start_date + timedelta(minutes=1)
# prints 2019-03-31T01:00:00+00:00
# which does not exist in local time, just like the other library
print(f"Add 1 minute and we get {end_date.isoformat()}")
new_end_date = start_date + timedelta(minutes=61)
# prints 2019-03-31T02:00:00+00:00
# which is not the right time, but does exist
print(f"Add 61 minutes and we get {new_end_date.isoformat()}")
# we could use tzlondon.normalize, but that's not the point here
#
# if we compare their times, we always get something that makes sense:
assert (end_date.timestamp() - start_date.timestamp()) == 60
assert (new_end_date.timestamp() - start_date.timestamp()) == 3660
print("Standard lib time travels...")
time_travel()
print("pytz does not - I think?")
no_time_travel_pytz()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment