Noda Time 1.1.1
Noda Time 1.2 is coming along nicely, and hopefully I’ll find time to write more about that closer to release. Today, though, I thought I’d talk about 1.1.1, which we released today.
First off, if you’re not using the Portable Class Library version of Noda Time or running under Mono, there’s really no need to upgrade from 1.1.0. 1.1.1 includes just two bug fixes over 1.1.0, both of which revolve around getting the system default time zone in those two runtime environments.
I thought it might be interesting to walk through what’s going on here in a little (okay, a lot) more detail.
Noda Time has the concept of an IDateTimeZoneProvider
, which is a
factory for DateTimeZone
instances. For example, you can write:
DateTimeZone zone = DateTimeZoneProviders.Tzdb["Europe/London"];
Instant now = SystemClock.Instance.Now;
Console.WriteLine("It is now {0}.", now);
Console.WriteLine("In London, that's {0}.", now.InZone(zone));
That will print something like this:
It is now 2013-08-30T19:18:23Z.
In London, that's Local: 30/08/2013 20:18:23 Offset: +01 Zone: Europe/London.
This is fine, but sometimes you need the system default time zone instead. In that case, Noda Time offers two different ways to get hold of the system default time zone1:
- You can ask either the TZDB or BCL time zone provider for the
DateTimeZone
that it considers to be equivalent to the system default time zone. - You can obtain a
BclDateTimeZone
wrapping the BCL’s default system time zone viaBclDateTimeZone.ForSystemDefault()
(or viaBclDateTimeZone.FromTimeZoneInfo(TimeZoneInfo.Local)
, which is equivalent, though slightly less efficient).
In the PCL build of Noda Time, only the first of this is available:
BclDateTimeZone
and the corresponding BCL time zone provider are only
included in the desktop build of Noda Time2. Under the PCL, the only
way to get a Noda Time version of the system default time zone is something
like:
DateTimeZone zone = DateTimeZoneProviders.Tzdb.GetSystemDefault();
Instant now = SystemClock.Instance.Now;
Console.WriteLine("It is now {0}.", now);
Console.WriteLine("Locally, that's {0}.", now.InZone(zone));
which should work as above3.
However, one of the things we did just before Noda Time 1.0 was to introduce a service provider interface for time zones in the form of a time zone source (in comparison to the time zone provider that user code calls).
One of the differences between the two interfaces is the way that the
default system time zone is obtained: user code calls the above
GetSystemDefault()
method on the provider, while the source implements a
MapTimeZone()
method that converts a TimeZoneInfo
into a
provider-specific time zone ID for the source’s equivalent zone4.
How does our provider implementation implement GetSystemDefault()
? We
call MapTimeZone()
to identify the equivalent zone from the source, then
look up that time zone by ID. The TZDB source implements MapTimeZone()
by
mapping the Windows time zone ID to a TZDB time zone ID using information
from the CLDR windowsZones
data.
However, annoyingly (and rather unreasonably, given that the data is still
available internally) the PCL version of TimeZoneInfo
doesn’t support the
Id
property, and so the PCL build of Noda Time uses the StandardName
property instead, which almost always has the same value (we have a small
hard-coded map to fix up the cases where the ID and display name differ).
Bug #1 (or #221 if we’re counting): we found that the
StandardName
property is, not entirely unsurprisingly, locale-sensitive.
It actually varies based on the system locale (which, on Windows, you need
to reboot to change). Faced with that, and having already released 1.1.0,
we implemented something of a hack: if the display name/ID match fails, the
TZDB source actually uses heuristics to find a time zone that’s correct at
the majority of the time zone’s transitions over the next year (in practice,
this either produces a decent result, or we throw
DateTimeZoneNotFoundException
).
Obviously, we only need to use this hack on the PCL build (and even then
only really when run on a non-English system locale): the desktop build just
maps the Id
property directly.
From a bug entirely of our making, to one mostly of Mono’s. Bug #2
(#235) is caused because Mono’s implementation of
TimeZoneInfo.Local
on non-Windows platforms typically returns a time zone
with an ID of “Local”, which isn’t returned by Mono’s
TimeZoneInfo.GetSystemTimeZones()
. This breaks the way we implemented
caching in our provider, which assumes that the source
(here, BclDateTimeZoneSource
) will never return a time zone with an ID
that it hasn’t already told us about.
We could argue about whether Mono’s behaviour is reasonable, or talk about
the UNIX heritage that shaped this behaviour (hint: /etc/localtime
), but
the fix here is simple: we now always include the local BCL time zone name
in the list of time zones returned by BclDateTimeZoneSource.GetIds()
.
So that’s the two bug fixes, in rather more detail that I was originally planning to write! Have fun with 1.1.1, and we’ll keep going with 1.2!
-
That seems to me to be one too many, and it’s possible we might revisit this for 2.0. ↩
-
The PCL version of
TimeZoneInfo
doesn’t include theGetAdjustmentRules()
method, which provides historical transitions (“adjustment rules”), and which is core to howBclDateTimeZone
is currently implemented. ↩ -
Annoyingly, I can’t actually test this, since it only works if the runtime returns a local time zone with a Windows time zone name, and (as discussed a little later) Mono/Linux doesn’t. ↩
-
On reflection, I think this is probably a bad design: it doesn’t isolate the source from the BCL’s
TimeZoneInfo
, and we only ever call the method withTimeZoneInfo.Local
. An SPI method that’s only called with a single property value seems wrong to me. ↩