[Python-checkins] python/nondist/sandbox/datetime US.py,1.2,1.3

tim_one@users.sourceforge.net tim_one@users.sourceforge.net
Wed, 25 Dec 2002 15:36:50 -0800


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

Modified Files:
	US.py 
Log Message:
I doubt it's possible to do better than this rework if trying to use a
single tzinfo subclass to represent both standard and daylight times.
It ends up with ambiguities (redundant representations) on one end,
times that can't be represented at all on the other end, and arithmetic
surprises.  That's all explained in great detail in the comments, and
illustrated concretely in a new "literate doctest".  Turns out that
.astimezone() also has an unbounded recursion trap awaiting the unwary.


Index: US.py
===================================================================
RCS file: /cvsroot/python/python/nondist/sandbox/datetime/US.py,v
retrieving revision 1.2
retrieving revision 1.3
diff -C2 -d -r1.2 -r1.3
*** US.py	25 Dec 2002 19:52:25 -0000	1.2
--- US.py	25 Dec 2002 23:36:47 -0000	1.3
***************
*** 1,6 ****
  from datetime import *
  
  class USTimeZone(tzinfo):
!     "A class capturing the current rules for United States time zones."
  
      dstoff = timedelta(hours=1)
--- 1,30 ----
  from datetime import *
  
+ def _first_sunday_at_or_after(dt):
+     days_to_go = 6 - dt.weekday()
+     if days_to_go:
+         dt += timedelta(days_to_go)
+     return dt
+ 
  class USTimeZone(tzinfo):
!     "A class capturing the current (2002) rules for United States time zones."
! 
!     # A seemingly intractable problem:  when DST ends, there's a one-hour
!     # slice that repeats in "naive time".  That is, when the naive clock
!     # hits 2am on the last Sunday in October, it magically goes back an
!     # hour and starts over at 1am.  A naive time simply can't know whether
!     # range(1:00:00, 2:00:00) on that day *intends* to refer to standard
!     # or daylight time, and adding a tzinfo object modeling both DST and
!     # standard time doesn't improve that.  We arbitrarily decide it intends
!     # DST then.  A consequence is that there's no way to spell a time in
!     # the 1-hour span starting when DST ends:  2:HH:MM on that day is
!     # taken as being in standard time, while 1:HH:MM is taken as DST, and
!     # in UTC there's a one-hour gap between 1:59:59 and 2:00:00.
!     #
!     # On the other end, when DST starts at 2am on the first Sunday in April,
!     # the naive clock magically jumps from 1:59:59 to 3:00:00.  A naive time
!     # of 2:HH:MM on that day doesn't make sense.  We arbitrarily decide it
!     # intends DST then, making it a redundant spelling of 1:HH:MM in standard
!     # time on that day.
  
      dstoff = timedelta(hours=1)
***************
*** 8,13 ****
      # DST starts at 2am (standard time) on the first Sunday in April.
      start = datetime(1, 4, 1, 2)
!     # and ends at 1am (standard time; 2am DST) on the last Sunday of Oct.
!     end = datetime(1, 10, 31, 1)
  
      def __init__(self, stdoffset, stdname, dstname):
--- 32,37 ----
      # DST starts at 2am (standard time) on the first Sunday in April.
      start = datetime(1, 4, 1, 2)
!     # and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct.
!     end = datetime(1, 10, 25, 2)
  
      def __init__(self, stdoffset, stdname, dstname):
***************
*** 18,31 ****
          self.dstname = dstname
  
-     def _hard_to_say(self, dt):
-         # Subtle:  the "dt.tzinfo is not self" guard is needed to prevent
-         # unbounded recursion due to the dt.utcoffset() call -- without
-         # that guard, dt.utcoffset() calls self.utcoffset(dt), which
-         # calls self.dst(dt), which calls self._hard_to_say(dt) again.
-         return (dt is None or
-                 isinstance(dt, time) or
-                 dt.tzinfo is None or
-                 (dt.tzinfo is not self and dt.utcoffset() is None))
- 
      def utcoffset(self, dt):
          return self.stdoff + self.dst(dt)
--- 42,45 ----
***************
*** 38,77 ****
  
      def dst(self, dt):
!         if self._hard_to_say(dt):
!             return self.zero # an exception may also be sensible here
  
!         # Convert to local time.  Again we need to guard against
!         # calling dt.utcoffset() when dt.tzinfo is self.  We also need
!         # to make the comparands naive. because comparing aware objects
!         # also tries to subtract off UTC offsets (again leading to
!         # unbounded recursion).
          if dt.tzinfo is not self:
!             dt = dt - (dt.utcoffset() - self.stdoff)
!         dt = dt.replace(tzinfo=None)
          # Find first Sunday in April.
!         start = self.start.replace(year=dt.year)
!         start += timedelta(days=6-start.weekday())
          assert start.weekday() == 6 and start.month == 4 and start.day <= 7
!         if dt < start:
!             return self.zero
          # Find last Sunday in October.
!         end = self.end.replace(year=dt.year)
!         end -= timedelta(days=(end.weekday()+1) % 7)
          assert end.weekday() == 6 and end.month == 10 and end.day >= 25
!         if dt < end:
              return self.dstoff
!         return self.zero
  
! Eastern = USTimeZone(timedelta(hours=-5), "EST", "EDT")
  
! def printstuff(d):
!     print
!     print d
!     print d.tzname()
!     print d.timetuple()
!     print d.ctime()
  
! d = datetimetz(2002, 4, 7, 1, 59, 59, tzinfo=Eastern)
! printstuff(d)
! d += timedelta(seconds=1)
! printstuff(d)
--- 52,172 ----
  
      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 self.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 self.zero
!             convert_endpoints_to_utc = True
!             dt -= offset
! 
          # Find first Sunday in April.
!         start = _first_sunday_at_or_after(self.start.replace(year=dt.year))
          assert start.weekday() == 6 and start.month == 4 and start.day <= 7
! 
          # Find last Sunday in October.
!         end = _first_sunday_at_or_after(self.end.replace(year=dt.year))
          assert end.weekday() == 6 and end.month == 10 and end.day >= 25
! 
!         if convert_endpoints_to_utc:
!             start -= self.stdoff    # start is in std time
!             end -= (self.stdoff + self.dstoff) # end is in DST time
! 
!         # Can't compare naive to aware objects, so strip the timezone from
!         # dt first.  Caution:  dt.astimezone(None) instead can lead to
!         # unbounded recursion, because astimezone() has to call utcoffset()
!         # to determine whether it returns None, and utcoffset() ends up
!         # calling this (dst()) method again.
!         dt = dt.replace(tzinfo=None)
!         if start <= dt < end:
              return self.dstoff
!         else:
!             return self.zero
  
! brainbuster_test = """
! >>> Eastern = USTimeZone(timedelta(hours=-5), "EST", "EDT")
  
! >>> def printstuff(d):
! ...     print d
! ...     print d.tzname()
! ...     print d.timetuple()
! ...     print d.ctime()
  
! Right before DST starts.
! >>> before = datetimetz(2002, 4, 7, 1, 59, 59, tzinfo=Eastern)
! >>> printstuff(before)
! 2002-04-07 01:59:59-05:00
! EST
! (2002, 4, 7, 1, 59, 59, 6, 97, 0)
! Sun Apr  7 01:59:59 2002
! 
! Right when DST starts.
! >>> after = before + timedelta(seconds=1)
! >>> printstuff(after)
! 2002-04-07 02:00:00-04:00
! EDT
! (2002, 4, 7, 2, 0, 0, 6, 97, 1)
! Sun Apr  7 02:00:00 2002
! 
! The difference is confusing.  2:00:00 doesn't exist on the naive clock (the
! naive clock leaps from 1:59:59 to 3:00:00), and is taken to be in DST, as a
! redundant spelling of 1:00:00 standard time.  So we actually expect b to be
! about an hour *before* a.  But subtraction doesn't do that:  because the
! tzinfo objects are identical, subtraction ignores them, and the difference
! comes out as positive one second.
! 
! >>> print after - before
! 0:00:01
! 
! OTOH, if we convert them to UTC and subtract, the difference is about
! an hour.
! 
! >>> class UTC(tzinfo):
! ...     def utcoffset(self, dt):
! ...         return 0
! >>> utc = UTC()
! >>> utcdiff = after.astimezone(utc) - before.astimezone(utc)
! >>> print -utcdiff
! 0:59:59
! 
! Now right before DST ends.
! >>> before = datetimetz(2002, 10, 27, 1, 59, 59, tzinfo=Eastern)
! >>> printstuff(before)
! 2002-10-27 01:59:59-04:00
! EDT
! (2002, 10, 27, 1, 59, 59, 6, 300, 1)
! Sun Oct 27 01:59:59 2002
! 
! And right when DST ends.
! >>> after = before + timedelta(seconds=1)
! >>> printstuff(after)
! 2002-10-27 02:00:00-05:00
! EST
! (2002, 10, 27, 2, 0, 0, 6, 300, 0)
! Sun Oct 27 02:00:00 2002
! 
! The naive clock repeats the times in 1:HH:MM, so 1:59:59 was actually
! ambiguous, and resolved arbitrarily as being in DST.  2:00:00 is in standard
! time, and is actually about an hour later, but the tzinfo objects are the same
! so the offsets are again ignored by subtraction.
! 
! >>> print after - before
! 0:00:01
! >>> print after.astimezone(utc) - before.astimezone(utc)
! 1:00:01
! """
! 
! __test__ = {'brainbuster': brainbuster_test}
! 
! def _test():
!     import doctest, US
!     return doctest.testmod(US)
! 
! if __name__ == "__main__":
!     _test()