I'm testing how dates are calculated and displayed (with time zone codes) around a daylight savings change.
In the UK, at 1am on the 30th March 2014, we enter DST, and go from GMT to BST. The time jumps from 2014-03-30 00:59:59 GMT
to 2014-03-30 02:00:00 BST
.
I've hit a strange issue replicating this 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
The output is:
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 marked on the output where the invalid times are. Those times do not exist on the wallclock, there is no 1am or 1:30am on the 30th March 2014, so I'm not sure why it is being displayed.
The same process but done in a slightly different way yields the correct results. Why is this?
Actually, both sections of code are incorrect. Running your exact code on my machine (in US Pacific Time zone), the bottom section 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 fromtimestamp
uses the computer's local time zone when none is specified. If I just switch the fromtimestamp
to utcfromtimestamp
, it will use a naive value - The results then get put in the correct time zone, but it gives the same results as the first section - showing the two invalid times.
The problem is rectified by using the normalize
method from the pytz timezone
instance. Unfortunately, you're keeping d
as a naive datetime, so you can't normalize it without localizing it first, and when done you'll have to make it naive again. This works, but makes for 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, the whole thing could be simplified by using aware datetimes early:
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 is_dst
function, as you can now get the tzname
directly from the aware datetime
instance.