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