[Zope3-checkins] CVS: Zope3/src/datetime/tests - test_datetime.py:1.5
Tim Peters
tim.one@comcast.net
Sun, 29 Dec 2002 18:33:25 -0500
Update of /cvs-repository/Zope3/src/datetime/tests
In directory cvs.zope.org:/tmp/cvs-serv8304/src/datetime/tests
Modified Files:
test_datetime.py
Log Message:
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>).
=== Zope3/src/datetime/tests/test_datetime.py 1.4 => 1.5 ===
--- Zope3/src/datetime/tests/test_datetime.py:1.4 Thu Dec 26 19:15:50 2002
+++ Zope3/src/datetime/tests/test_datetime.py Sun Dec 29 18:33:24 2002
@@ -2507,6 +2507,157 @@
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,
@@ -2518,6 +2669,7 @@
TestTime,
TestTimeTZ,
TestDateTimeTZ,
+ TestTimezoneConversions,
)
]
return unittest.TestSuite(allsuites)