I've been going through Chandler, making various parts of its workings timezone-aware. Here are some of the issues I've run into. For some background, have a look at the 0.6 Chandler Timezone spec.
UI Issues
- The default timezone dropdown is too big: Mimi has suggested it move to a menu (under File, probably).
- In week view, if you're not running on a massively wide display, we'll clip the timezone when displaying text like "3:00 PM PST".
Localization of timezone names
PyICU currently has localizations for strings of the form "PST" or "Pacific Standard Time". If, as the UI spec says, we want to display timezones
as "US/Pacific", we will have to create localizable strings for these. (This is the approach taken by Apple iCal, for example).
Equivalent timezones
When running on Linux, I've noticed that my default timezone appears as "PDT", while it's (the technically more correct) "US/Pacific" on the Mac. It would be
good to map this correctly, so there aren't duplications in the timezone drop-downs.
"Floating" timezones
At one point, I had put in a "floating" entry into the detail view timezone dropdown. For the most part, this worked, although it was causing weird UI errors
when switching to/from floating in recurring events. So, for now, I've disabled this.
Unsafe datetime comparisons
Python's
datetime
API doesn't allow you to compare naïve and non-naïve datetimes (it raises
a
TypeError
if you do so). Note that "compare" here covers the usual builtin operators like
==
,
<
,
<=
,
>
,
but also builtin functions like
min
,
max
and
cmp
.
Currently, all
datetime
objects are naïve in Chandler: This includes instances from imported events, as well as objects used internally (e.g. to figure out where to display events in the calendar, or when the event reminder dialog should be displayed). Once events with timezone information are present, then, Chandler has a dangerous mixture of naïve and non-naïve
datetimes
, and we need a strategy for porting over code that does comparisons.
Let's say we're trying to compare
naif
, a naïve
datetime
with
sophisticate
,
a non-naïve instance. There are a couple of possibilities (using
<=
as an example):
-
naif <= sophisticate.replace(tzinfo=None)
-
naif.replace(tzinfo=PyICU.ICUtzinfo.getDefault()) <= sophisticate
It really depends on the situation as to which is correct. For instance, if the calendar UI is trying to determine whether or an event with a floating timezone overlaps
another (non-floating) event, 2. above is correct, since "floating" events are always interpreted as occurring in the user's current time zone. However, there are cases where the first comparison is what we want.
For now, for minimizing code changes (or making them more obvious), I've added API on the Calendar module that uses the regular Python comparison API if it's safe, and falls back to 1. otherwise. Where now you have
>>> naive <= sophisticate
you would use the API via:
>>> Calendar.datetimeOp(naive, '<=', sophisticate)
This is somewhat of a stopgap measure, partly because all these usages have to be reviewed to check whether #2. should be used instead.
An abundance of naïveté
There are cases in our code where calculations are performed incorrectly when
datetimes
have differing timezones. For example, the calendar UI assumes that you can figure out what day a given event starts on by using the
datetime
class's
toordinal()
method.
Along similar lines, code that changes event information (e.g., editing of start and end times in the detail view, or drag-and-drop of events in the calendar UI) needed
to be changed to make sure that timezone information was preserved. In many cases, the old code would drop all time zone info from the newly created
datetime
instances.
dateutil
JeffreyHarris pointed out that the python
dateutil
library, our underlying implementation for recurrence, uses unsafe
datetime
comparisons everywhere. As a result, we need to make sure that
datetimes
accessible to a given
rruleset
are either all naïve or all non-naïve. The current implementation attempts to do more than that by making sure that all
these
datetimes
have the same
tzinfo
; there may be cases that have been missed, though. (For example, this
would have to apply to any arguments to the
before
or
after
methods. Maybe subclassing
dateutil.rrule.rrule{,set}
would be a better way to deal with this problem than the current approach of tweaking all the call sites.)
PyICU DateFormat usage
There is somewhat of a mismatch between Python
datetimes
and PyICU's
DateFormat
(or
MessageFormat
). In ICU, times contain no timezone information (they're represented as a POSIX timestamp).
Timezone is owned by
DateFormat
objects: Unless specified otherwise, Format objects are created with the user's current timezone. So, when
getting a displayable string from a
datetime
with a non-default timezone, you have to call
setTimeZone()
before
using any
DateFormat
instances. (For
MessageFormat
instances, the situation is more
complex because you have to do this for any date sub-formats).
Note:
DateFormat.parse()
has similar issues.
Possibly we should make PyICU itself could deal with this (since it already has code to coerce
datetime
objects to ICU's
UDate
). This
would probably not be hard to change for
DateFormat
, but
MessageFormat
would be more difficult. In general, the ICU formatting API isn't very Pythonic (e.g.,
Formattable
objects would probably not exist if ICU had originated in a dynamic language). Whether it's worth addressing this general concern in PyICU is open to question.
For the moment, I've worked around this in AttributeEditors by adding a
DateFormatter
class that wraps a
PyICU.DateFormat
instance, but
deals with changing the timezone when you call its
format()
method. Similarly, it has a
parse()
method that takes a
ICUtzinfo
argument and returns a
datetime
in the correct timezone.
-- GrantBaillie - 11 Aug 2005