[Python-checkins] python/nondist/sandbox/datetime test_datetime.py,1.84,1.85

tim_one@users.sourceforge.net tim_one@users.sourceforge.net
Sun, 29 Dec 2002 15:31:01 -0800


Update of /cvsroot/python/python/nondist/sandbox/datetime
In directory sc8-pr-cvs1:/tmp/cvs-serv27412

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>).


Index: test_datetime.py
===================================================================
RCS file: /cvsroot/python/python/nondist/sandbox/datetime/test_datetime.py,v
retrieving revision 1.84
retrieving revision 1.85
diff -C2 -d -r1.84 -r1.85
*** test_datetime.py	27 Dec 2002 00:13:56 -0000	1.84
--- test_datetime.py	29 Dec 2002 23:30:59 -0000	1.85
***************
*** 2512,2515 ****
--- 2512,2666 ----
          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')
***************
*** 2523,2526 ****
--- 2674,2678 ----
                                 TestTimeTZ,
                                 TestDateTimeTZ,
+                                TestTimezoneConversions,
                                )
                  ]