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?

+3


source to share


2 answers


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.

0


source


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

      

+1


source







All Articles