[Zope3-checkins] CVS: Zope3/src/datetime - _datetime.py:1.8

Tim Peters tim.one@comcast.net
Tue, 31 Dec 2002 01:07:26 -0500


Update of /cvs-repository/Zope3/src/datetime
In directory cvs.zope.org:/tmp/cvs-serv18503/src/datetime

Modified Files:
	_datetime.py 
Log Message:
A new, and much hairier, implementation of astimezone(), building on
an idea from Guido.  This restores that the datetime implementation
never passes a datetime d to a tzinfo method unless d.tzinfo is the
tzinfo instance whose method is being called.  That in turn allows
enormous simplifications in user-written tzinfo classes (see the Python
sandbox US.py and EU.py for fully fleshed-out examples).

d.astimezone(tz) also raises ValueError now if d lands in the one hour
of the year that can't be expressed in tz (this can happen iff tz models
both standard and daylight time).  That it used to return a nonsense
result always ate at me, and it turned out that it seemed impossible to
force a consistent nonsense result under the new implementation (which
doesn't know anything about how tzinfo classes implement their methods --
it can only infer properties indirectly).

Doc changes will have to wait for tomorrow.  Ditto getting the C
implementation back in synch.


=== Zope3/src/datetime/_datetime.py 1.7 => 1.8 ===
--- Zope3/src/datetime/_datetime.py:1.7	Mon Dec 30 14:45:53 2002
+++ Zope3/src/datetime/_datetime.py	Tue Dec 31 01:06:55 2002
@@ -1522,6 +1522,7 @@
 datetime.max = datetime(9999, 12, 31, 23, 59, 59, 999999)
 datetime.resolution = timedelta(microseconds=1)
 
+_HOUR = timedelta(hours=1)
 
 class datetimetz(datetime):
 
@@ -1612,19 +1613,72 @@
         return datetimetz(year, month, day, hour, minute, second,
                           microsecond, tzinfo)
 
+    def _inconsistent_utcoffset_error(self):
+        raise ValueError("astimezone():  tz.utcoffset() gave "
+                         "inconsistent results; cannot convert")
+
+    def _finish_astimezone(self, other, otoff):
+        # If this is the first hour of DST, it may be a local time that
+        # doesn't make sense on the local clock, in which case the naive
+        # hour before it (in standard time) is equivalent and does make
+        # sense on the local clock.  So force that.
+        alt = other - _HOUR
+        altoff = alt.utcoffset()
+        if altoff is None:
+            self._inconsistent_utcoffset_error()
+        # Are alt and other really the same time?  alt == other iff
+        # alt - altoff == other - otoff, iff
+        # (other - _HOUR) - altoff = other - otoff, iff
+        # otoff - altoff == _HOUR
+        diff = otoff - altoff
+        if diff == _HOUR:
+            return alt      # use the local time that makes sense
+
+        # There's still a problem with the unspellable (in local time)
+        # hour after DST ends.
+        if self == other:
+            return other
+        # Else there's no way to spell self in zone other.tz.
+        raise ValueError("astimezone():  the source datetimetz can't be "
+                         "expressed in the target timezone's local time")
+
     def astimezone(self, tz):
         _check_tzinfo_arg(tz)
-        # Don't call utcoffset unless it's necessary.
-        if tz is not None:
-            offset = self.utcoffset()
-            if offset is not None:
-                newoffset = tz.utcoffset(self)
-                if newoffset is not None:
-                    if not isinstance(newoffset, timedelta):
-                        newoffset = timedelta(minutes=newoffset)
-                    diff = offset - newoffset
-                    self -= diff # this can overflow; can't be helped
-        return self.replace(tzinfo=tz)
+        # This is somewhat convoluted because we can only call
+        # tzinfo.utcoffset(dt) when dt.tzinfo is tzinfo.  It's more
+        # convoluted due to DST headaches (redundant spellings and
+        # "missing" hours in local time -- see the tests for details).
+        other = self.replace(tzinfo=tz) # this does no conversion
+
+        # Don't call utcoffset unless necessary.  First check trivial cases.
+        if tz is None or self._tzinfo is None or self._tzinfo is tz:
+            return other
+
+        # Get the offsets.  If either object turns out to be naive, again
+        # there's no conversion of date or time fields.
+        myoff = self.utcoffset()
+        if myoff is None:
+            return other
+        otoff = other.utcoffset()
+        if otoff is None:
+            return other
+
+        other += otoff - myoff
+        # If tz is a fixed-offset class, we're done, but we can't know
+        # whether it is.  If it's a DST-aware class, and we're not near a
+        # DST boundary, we're also done.  If we crossed a DST boundary,
+        # the offset will be different now, and that's our only clue.
+        # Unfortunately, we can be in trouble even if we didn't cross a
+        # DST boundary, if we landed on one of the DST "problem hours".
+        newoff = other.utcoffset()
+        if newoff is None:
+            self._inconsistent_utcoffset_error()
+        if newoff != otoff:
+            other += newoff - otoff
+            otoff = other.utcoffset()
+            if otoff is None:
+                self._inconsistent_utcoffset_error()
+        return self._finish_astimezone(other, otoff)
 
     def isoformat(self, sep='T'):
         s = super(datetimetz, self).isoformat(sep)