[Zope3-dev] Holes in time

Tim Peters tim@zope.com
Wed, 1 Jan 2003 19:46:00 -0500


The datetime module underwent a number of last-minute changes to make
writing tzinfo classes a pleasure instead of an expert-level nightmare
(which latter is what it turned to be -- it's not anymore).

Realistic examples can be found in the Python CVS datetime sandbox, at

    http://tinyurl.com/3zr7

    US.py        Some US time zones
    EU.py        Some European time zones
    PSF.py       Essential if you're a PSF Director <wink>
    dateutil.py  Examples of using the datetime module to compute
                 other kinds of practical stuff (like the 2nd-last
                 Tuesday in August)

We still have two time zone conversion problems, and always will (they come
with the territory -- they're inherent problems, not artifacts of the
implementation).  They only arise in a tzinfo class that's trying to model
both standard and daylight time.  In effect, that's one class trying to
pretend it's two different time zones, and at the transition points there
are "impossible problems".

For concreteness, I'll use US Eastern here, UTC-0500.  On some day in April,
DST starts at the minute following the local wall clock's 1:59.  On some day
in October, DST ends at the minute following the local wall clock's 1:59.

Here's a picture:

       UTC  3:MM  4:MM  5:MM  6:MM  7:MM  8:MM
  standard 22:MM 23:MM  0:MM  1:MM  2:MM  3:MM
  daylight 23:MM  0:MM  1:MM  2:MM  3:MM  4:MM

wall start 22:MM 23:MM  0:MM  1:MM  3:MM  4:MM

  wall end 23:MM  0:MM  1:MM  1:MM  2:MM  3:MM

UTC, EST and EDT are all self-consistent and trivial.  It's only wall time
that's a problem, and only at the transition points:

1. When DST starts (the "wall start" line), the wall clock leaps from 1:59
to 3:00.  A wall time of the form 2:MM doesn't really make sense on that
day.  The example classes do what I believe is the best that can be done:
since 2:MM is "after 2" on that day, it's taken as daylight time, and so as
an alias for 1:MM standard == 1:MM wall on that day, which is a time that
does make sense on the wall clock that day.  The astimezone() function
ensures that the "impossible hour" on that day is never the result of a
conversion (you'll get the standard-time spelling instead).  If you don't
think that's the best that can be done, speak up now.

2. When DST ends (the "wall end" line), we have a worse problem:  there'a an
hour that can't be spelled *at all* in wall time.  It's the hour beginning
at the moment DST ends; in the example, that's times of the form 6:MM UTC on
the day daylight time ends.  The local wall clock leaps from 1:59 (daylight
time) back to 1:00 again (but the second time as a standard time).  The hour
6:MM UTC looks like 1:MM, but so does the hour 5:MM UTC on that day.  A
reasonable tzinfo class should take 1:MM as being daylight time on that day,
since it's "before 2".  As a consequence, the hour 6:MM UTC has no
wall-clock spelling at all.

This can't be glossed over.  If you code a tzinfo class to take 1:MM as
being standard time on that day instead, then the UTC hour 5:MM becomes
unspellable in wall time instead.  No matter how you cut it, the redundant
spellings of an hour on the day DST starts means there's an hour that can't
be spelled at all on the day DST ends (so in that sense, they're the two
sides of a single problem).

What to do?  The current implementation of dt.astimezone(tz) raises
ValueError if dt can't be expressed as a local time in tz.  That's the
"errors should never pass silently" school, which I briefly attended in
college <wink>.  If you don't like that, what would you rather see happen?
Try to be precise, and remember that getting a "correct" time in tz is
flatly impossible in this case.

Two other debatable edge cases in the implementation of dt.astimezone(tz):

3. If dt.tzinfo.dst(dt) returns None, the current implementation takes that
as a synonym for 0.  Perhaps it should raise an exception instead.

4. If dt.tzinfo.utcoffset(dt) first returns an offset, and on a subsequent
call (while still trying to figure out the same conversion)returns None, an
exception is raised.  Those who don't want unspellable hours to raise an
exception may also want inconsistent tzinfo implementations to go without
complaint.  If so, what do you want it to do instead?