From 621818b31803324101b4ce5dde60ed425bf5a443 Mon Sep 17 00:00:00 2001 From: Tim Peters Date: Sun, 29 Dec 2002 23:44:49 +0000 Subject: 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 ). --- Lib/test/test_datetime.py | 152 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/Lib/test/test_datetime.py b/Lib/test/test_datetime.py index dc58972..9663217 100644 --- a/Lib/test/test_datetime.py +++ b/Lib/test/test_datetime.py @@ -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) -- cgit v0.12