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