by Malcolm Rowe

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:

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!

  1. That seems to me to be one too many, and it’s possible we might revisit this for 2.0. 

  2. The PCL version of TimeZoneInfo doesn’t include the GetAdjustmentRules() method, which provides historical transitions (“adjustment rules”), and which is core to how BclDateTimeZone is currently implemented. 

  3. 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. 

  4. 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 with TimeZoneInfo.Local. An SPI method that’s only called with a single property value seems wrong to me.