Dates and time zone codes around DST change
I am testing how dates are calculated and displayed (with time zone codes) around a change in daily savings.
In the UK, at 1 hour. On March 30, 2014, we enter Daylight Saving Time and go from GMT to BST. Time jumps from 2014-03-30 00:59:59 GMT
to 2014-03-30 02:00:00 BST
.
I hit a weird problem by copying it with the following code:
import pytz
from datetime import datetime, time, timedelta
def is_dst(d, tz):
assert d.tzinfo is None # we want a naive datetime to localize
return tz.localize(d).dst() != timedelta(0)
start_datetime = datetime(2014, 03, 30, 0, 0, 0)
tz = pytz.timezone('Europe/London')
# Increment using timedelta
print 'This doesn\'t work:'
d = start_datetime
for i in range(5):
print str(d) + ' ' + tz.tzname(d, is_dst=is_dst(d, tz))
d += timedelta(minutes=30) # Add 30 minutes
# Increment by adding seconds to epoch
print 'This works:'
epoch = datetime.utcfromtimestamp(0)
timestamp = (start_datetime - epoch).total_seconds()
for i in range(5):
d = datetime.fromtimestamp(timestamp)
print str(d) + ' ' + tz.tzname(d, is_dst=is_dst(d, tz))
timestamp += 30 * 60 # Add 30 minutes
Output:
This doesn't work:
2014-03-30 00:00:00 GMT
2014-03-30 00:30:00 GMT
2014-03-30 01:00:00 GMT <- invalid time
2014-03-30 01:30:00 GMT <- invalid time
2014-03-30 02:00:00 BST
This works:
2014-03-30 00:00:00 GMT
2014-03-30 00:30:00 GMT
2014-03-30 02:00:00 BST
2014-03-30 02:30:00 BST
2014-03-30 03:00:00 BST
I have noted in the output where invalid values āāare specified. Those times don't exist on the booth, but March 30, 2014 wasn't 1:00 or 1:30, so I'm not sure why it is displayed.
The same process, but in a slightly different way, gives the correct results. Why is this?
source to share
Actually, both sections of the code are wrong. Running your exact code on my machine (in the US Pacific area), the bottom part returns:
2014-03-29 17:00:00 GMT 2014-03-29 17:30:00 GMT 2014-03-29 18:00:00 GMT 2014-03-29 18:30:00 GMT 2014-03-29 19:00:00 GMT
This is because it fromtimestamp
uses the local time zone of the computer when none is specified. If I just switch fromtimestamp
to utcfromtimestamp
, it uses the naive value. The results then fall into the correct timezone, but it gives the same results as the first section - showing two invalid times.
The problem is straightened out with a method normalize
from the pytz instance timezone
. Unfortunately, you are saving d
as a naive datetime, so you cannot normalize it without localizing it in the first place, and when that is done, you have to make it naive again. This works, but creates some messy code:
# Increment using timedelta
print 'This works:'
d = start_datetime
for i in range(5):
print str(d) + ' ' + tz.tzname(d, is_dst=is_dst(d, tz))
d += timedelta(minutes=30) # Add 30 minutes
d = tz.normalize(tz.localize(d)).replace(tzinfo=None)
# Increment by adding seconds to epoch
print 'This works too:'
epoch = datetime.utcfromtimestamp(0)
timestamp = (start_datetime - epoch).total_seconds()
for i in range(5):
d = tz.normalize(datetime.fromtimestamp(timestamp, pytz.utc).astimezone(tz)).replace(tzinfo=None)
print str(d) + ' ' + tz.tzname(d, is_dst=is_dst(d, tz))
timestamp += 30 * 60 # Add 30 minutes
Output:
This works: 2014-03-30 00:00:00 GMT 2014-03-30 00:30:00 GMT 2014-03-30 02:00:00 BST 2014-03-30 02:30:00 BST 2014-03-30 03:00:00 BST This works too: 2014-03-30 00:00:00 GMT 2014-03-30 00:30:00 GMT 2014-03-30 02:00:00 BST 2014-03-30 02:30:00 BST 2014-03-30 03:00:00 BST
Of course, all of this could be simplified using early alerts:
import pytz
from datetime import datetime, time, timedelta
tz = pytz.timezone('Europe/London')
start_datetime = tz.localize(datetime(2014, 03, 30, 0, 0, 0))
# Increment using timedelta
print 'This works:'
d = start_datetime
for i in range(5):
print str(d) + ' ' + d.tzname()
d = tz.normalize(d + timedelta(minutes=30)) # Add 30 minutes
# Increment by adding seconds to epoch
print 'This works too:'
epoch = datetime.fromtimestamp(0, pytz.utc)
timestamp = (start_datetime - epoch).total_seconds()
for i in range(5):
d = datetime.fromtimestamp(timestamp, pytz.utc).astimezone(tz)
print str(d) + ' ' + d.tzname()
timestamp += 30 * 60 # Add 30 minutes
Output:
This works: 2014-03-30 00:00:00+00:00 GMT 2014-03-30 00:30:00+00:00 GMT 2014-03-30 02:00:00+01:00 BST 2014-03-30 02:30:00+01:00 BST 2014-03-30 03:00:00+01:00 BST This works too: 2014-03-30 00:00:00+00:00 GMT 2014-03-30 00:30:00+00:00 GMT 2014-03-30 02:00:00+01:00 BST 2014-03-30 02:30:00+01:00 BST 2014-03-30 03:00:00+01:00 BST
Note that you no longer need the function is_dst
, as you can now get tzname
directly from the knowledgeable datetime
instance.
source to share
Here's @Matt Johnson's answer modified according to my comments :
from datetime import datetime, timedelta
import pytz
tz = pytz.timezone('Europe/London')
#NOTE: is_dst=None asserts that the local time exists and unambiguous
start_datetime = tz.localize(datetime(2014, 03, 30, 0, 0, 0), is_dst=None)
# increment using timedelta
print 'This works:'
d = start_datetime.astimezone(pytz.utc) # use UTC to do arithmetic
for _ in range(5):
local = d.astimezone(tz) # use local timezone for display only
print("\t{:%F %T %Z%z}".format(local))
d += timedelta(minutes=30) # works in UTC
# increment by adding seconds to epoch
print 'This works too:'
epoch = datetime(1970, 1, 1, tzinfo=pytz.utc)
timestamp = (start_datetime - epoch).total_seconds()
for i in range(5):
local = datetime.fromtimestamp(timestamp, tz)
print("\t{:%F %T %Z%z}".format(local))
timestamp += 30 * 60 # add 30 minutes
source to share