[Zope3-dev] Re: [Python-Dev] Holes in time
Tim Peters
tim.one@comcast.net
Sat, 04 Jan 2003 01:44:27 -0500
[Shane Hathaway]
> ...
> That sounds perfectly reasonable, but may I suggest moving the
> assumption by changing the interface of the tzinfo class. The
> utcoffset() method leads one to naively assume that functions f and g
> can both depend reliably on utcoffset(). Instead, tzinfo might
> have two methods, to_local(utc_date) and to_utc(local_date). That
> way, the tzinfo object encapsulates the madness.
I think we may need from_utc() before this is over, but that most people
won't have any need for it. In the other direction, it's already the tzinfo
subclass author's responsibility to ensure that the current:
d - d.utcoffset()
yields exactly the same date and time members as would the hypothesized:
d.to_utc()
> One downside is that then you can't expect normal programmers to
> write a correct tzinfo based on the C libraries. They'll never get
> it right. :-) It would have to be supplied with Python.
I doubt the latter will happen, and it certainly won't happen for 2.3.
The current scheme has actually become about as easy as it can become. From
the next iteration of the docs, here's a full implementation of a class for
DST-aware major US time zones (using the rules that have been in effect for
more than a decade):
"""
from datetime import tzinfo, timedelta, datetime
ZERO = timedelta(0)
HOUR = timedelta(hours=1)
def first_sunday_on_or_after(dt):
days_to_go = 6 - dt.weekday()
if days_to_go:
dt += timedelta(days_to_go)
return dt
# 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 October, 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 dt.tzinfo is None:
# An exception may be sensible here, in one or both cases.
# It depends on how you want to treat them. The astimezone()
# implementation always passes a datetimetz with
# dt.tzinfo == self.
return ZERO
assert dt.tzinfo is self
# Find first Sunday in April & the last in October.
start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year))
end = first_sunday_on_or_after(DSTEND.replace(year=dt.year))
# Can't compare naive to aware objects, so strip the timezone
$ from dt first.
if start <= dt.replace(tzinfo=None) < end:
return HOUR
else:
return ZERO
Eastern = USTimeZone(-5, "Eastern", "EST", "EDT")
Central = USTimeZone(-6, "Central", "CST", "CDT")
Mountain = USTimeZone(-7, "Mountain", "MST", "MDT")
Pacific = USTimeZone(-8, "Pacific", "PST", "PDT")
"""
The test suite beats the snot out of this class, and .astimezone() behaves
exactly as we've talked about here in all cases now, whether Eastern or
Pacific (etc) are source zones or target zones or both. But the coding is
really quite simple, doing nothing more nor less than implementing "the
plain rules". (BTW, note that no use is made of the platform C time
functions here)
A similar class for European rules can be found in EU.py in the Python
datetime sandbox, and is just as straightforward (relative to the complexity
inherent in those rules).
Because the only strong assumption astimezone() makes is that
tz.utcoffset(d) - tz.dst(d) # tz's "standard offset"
is invariant wrt d, it should work fine for tzinfo subclasses that want to
use different switch points in different years, or have multiple DST periods
in a year (including none at all in some years), etc. So long as a time
zone's "standard offset" depends only on a location's longitude,
astimezone() is very likely to do the right thing no matter how goofy the
rest of the zone is.
So, at the moment, I don't have an actual use case in hand anymore that
requires a from_utc() method. astimezone() could be written in terms of it,
though:
def astimezone(self, tz):
self -= self.utcoffset() # as UTC
other = self.replace(tzinfo=tz)
return other.from_utc()
and the tzinfo base class could supply a default from_utc() method capturing
the current astimezone() implementation. Then we'd have a powerful hook
tzinfo subclasses could override -- but I'm not sure anyone will find a need
to!