mirror of
https://github.com/python/cpython.git
synced 2025-08-04 08:59:19 +00:00
A start at non-trivial (== DST-aware) tests of timezone conversion.
Guido has in mind an easier way for users to code this stuff, but the only tests we have now are for fixed-offset tzinfo classes, and this stuff is extremely delicate in the endcases (read the new test code for why: there are holes in time <wink>).
This commit is contained in:
parent
40f6217092
commit
621818b318
1 changed files with 152 additions and 0 deletions
|
@ -2497,6 +2497,157 @@ class TestDateTimeTZ(TestDateTime, TZInfoBase):
|
|||
t2 = t2.replace(tzinfo=Varies())
|
||||
self.failUnless(t1 < t2) # t1's offset counter still going up
|
||||
|
||||
# Pain to set up DST-aware tzinfo classes.
|
||||
|
||||
def first_sunday_on_or_after(dt):
|
||||
days_to_go = 6 - dt.weekday()
|
||||
if days_to_go:
|
||||
dt += timedelta(days_to_go)
|
||||
return dt
|
||||
|
||||
ZERO = timedelta(0)
|
||||
HOUR = timedelta(hours=1)
|
||||
DAY = timedelta(days=1)
|
||||
# In the US, DST starts at 2am (standard time) on the first Sunday in April.
|
||||
DSTSTART = datetime(1, 4, 1, 2)
|
||||
# and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct,
|
||||
# which is the first Sunday on or after Oct 25.
|
||||
DSTEND = datetime(1, 10, 25, 2)
|
||||
|
||||
class USTimeZone(tzinfo):
|
||||
|
||||
def __init__(self, hours, reprname, stdname, dstname):
|
||||
self.stdoffset = timedelta(hours=hours)
|
||||
self.reprname = reprname
|
||||
self.stdname = stdname
|
||||
self.dstname = dstname
|
||||
|
||||
def __repr__(self):
|
||||
return self.reprname
|
||||
|
||||
def tzname(self, dt):
|
||||
if self.dst(dt):
|
||||
return self.dstname
|
||||
else:
|
||||
return self.stdname
|
||||
|
||||
def utcoffset(self, dt):
|
||||
return self.stdoffset + self.dst(dt)
|
||||
|
||||
def dst(self, dt):
|
||||
if dt is None or isinstance(dt, time) or dt.tzinfo is None:
|
||||
# An exception instead may be sensible here, in one or more of
|
||||
# the cases.
|
||||
return ZERO
|
||||
|
||||
convert_endpoints_to_utc = False
|
||||
if dt.tzinfo is not self:
|
||||
# Convert dt to UTC.
|
||||
offset = dt.utcoffset()
|
||||
if offset is None:
|
||||
# Again, an exception instead may be sensible.
|
||||
return ZERO
|
||||
convert_endpoints_to_utc = True
|
||||
dt -= offset
|
||||
|
||||
# Find first Sunday in April.
|
||||
start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year))
|
||||
assert start.weekday() == 6 and start.month == 4 and start.day <= 7
|
||||
|
||||
# Find last Sunday in October.
|
||||
end = first_sunday_on_or_after(DSTEND.replace(year=dt.year))
|
||||
assert end.weekday() == 6 and end.month == 10 and end.day >= 25
|
||||
|
||||
if convert_endpoints_to_utc:
|
||||
start -= self.stdoffset # start is in std time
|
||||
end -= self.stdoffset + HOUR # end is in DST time
|
||||
|
||||
# Can't compare naive to aware objects, so strip the timezone from
|
||||
# dt first.
|
||||
if start <= dt.astimezone(None) < end:
|
||||
return HOUR
|
||||
else:
|
||||
return ZERO
|
||||
|
||||
Eastern = USTimeZone(-5, "Eastern", "EST", "EDT")
|
||||
Pacific = USTimeZone(-8, "Pacific", "PST", "PDT")
|
||||
UTC = FixedOffset(0, "UTC", 0)
|
||||
|
||||
class TestTimezoneConversions(unittest.TestCase):
|
||||
# The DST switch times for 2002, in local time.
|
||||
dston = datetimetz(2002, 4, 7, 2)
|
||||
dstoff = datetimetz(2002, 10, 27, 2)
|
||||
|
||||
def test_easy(self):
|
||||
# Despite the name of this test, the endcases are excruciating.
|
||||
for tz in Eastern, Pacific:
|
||||
dston = self.dston.replace(tzinfo=tz)
|
||||
dstoff = self.dstoff.replace(tzinfo=tz)
|
||||
for delta in (timedelta(weeks=13),
|
||||
DAY,
|
||||
HOUR,
|
||||
timedelta(minutes=1),
|
||||
timedelta(microseconds=1)):
|
||||
for during in dston, dston + delta, dstoff - delta:
|
||||
self.assertEqual(during.dst(), HOUR)
|
||||
asutc = during.astimezone(UTC)
|
||||
there_and_back = asutc.astimezone(tz)
|
||||
|
||||
# Conversion to UTC and back isn't always an identity here,
|
||||
# because there are redundant spellings (in local time) of
|
||||
# UTC time when DST begins: the clock jumps from 1:59:59
|
||||
# to 3:00:00, and a local time of 2:MM:SS doesn't really
|
||||
# make sense then. The classes above treat 2:MM:SS as
|
||||
# daylight time then (it's "after 2am"), really an alias
|
||||
# for 1:MM:SS standard time. The latter form is what
|
||||
# conversion back from UTC produces.
|
||||
if during.date() == dston.date() and during.hour == 2:
|
||||
# We're in the redundant hour, and coming back from
|
||||
# UTC gives the 1:MM:SS standard-time spelling.
|
||||
self.assertEqual(there_and_back + HOUR, during)
|
||||
# Although during was considered to be in daylight
|
||||
# time, there_and_back is not.
|
||||
self.assertEqual(there_and_back.dst(), ZERO)
|
||||
# They're the same times in UTC.
|
||||
self.assertEqual(there_and_back.astimezone(UTC),
|
||||
during.astimezone(UTC))
|
||||
else:
|
||||
# We're not in the redundant hour.
|
||||
self.assertEqual(during, there_and_back)
|
||||
|
||||
# Because we have a redundant spelling when DST begins,
|
||||
# there is (unforunately) an hour when DST ends that can't
|
||||
# be spelled at all in local time. When DST ends, the
|
||||
# clock jumps from 1:59:59 back to 1:00:00 again. The
|
||||
# hour beginning then has no spelling in local time:
|
||||
# 1:MM:SS is taken to be daylight time, and 2:MM:SS as
|
||||
# standard time. The hour 1:MM:SS standard time ==
|
||||
# 2:MM:SS daylight time can't be expressed in local time.
|
||||
nexthour_utc = asutc + HOUR
|
||||
nexthour_tz = nexthour_utc.astimezone(tz)
|
||||
if during.date() == dstoff.date() and during.hour == 1:
|
||||
# We're in the hour before DST ends. The hour after
|
||||
# is ineffable.
|
||||
# For concreteness, picture Eastern. during is of
|
||||
# the form 1:MM:SS, it's daylight time, so that's
|
||||
# 5:MM:SS UTC. Adding an hour gives 6:MM:SS UTC.
|
||||
# Daylight time ended at 2+4 == 6:00:00 UTC, so
|
||||
# 6:MM:SS is (correctly) taken to be standard time.
|
||||
# But standard time is at offset -5, and that maps
|
||||
# right back to the 1:MM:SS Eastern we started with.
|
||||
# That's correct, too, *if* 1:MM:SS were taken as
|
||||
# being standard time. But it's not -- on this day
|
||||
# it's taken as daylight time.
|
||||
self.assertEqual(during, nexthour_tz)
|
||||
else:
|
||||
self.assertEqual(nexthour_tz - during, HOUR)
|
||||
|
||||
for outside in dston - delta, dstoff, dstoff + delta:
|
||||
self.assertEqual(outside.dst(), ZERO)
|
||||
there_and_back = outside.astimezone(UTC).astimezone(tz)
|
||||
self.assertEqual(outside, there_and_back)
|
||||
|
||||
|
||||
def test_suite():
|
||||
allsuites = [unittest.makeSuite(klass, 'test')
|
||||
for klass in (TestModule,
|
||||
|
@ -2508,6 +2659,7 @@ def test_suite():
|
|||
TestTime,
|
||||
TestTimeTZ,
|
||||
TestDateTimeTZ,
|
||||
TestTimezoneConversions,
|
||||
)
|
||||
]
|
||||
return unittest.TestSuite(allsuites)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue