16

I am using NodaTime in an application, and I need the user to select their timezone from a dropdown list. I have the following soft requirements:

1) The list only contain choices that are reasonably valid for the present and near future for real places. Historical, obscure, and generic timezones should be filtered out.

2) The list should be sorted first by UTC offset, and then by timezone name. This hopefully puts them in an order that is meaningful for the user.

I've written the following code, which does indeed work, but doesn't have exactly what I'm after. The filter probably needs to be adjusted, and I'd rather have the offset represent the base (non-dst) offset, rather than the current offset.

Suggestions? Recommendations?

var now = Instant.FromDateTimeUtc(DateTime.UtcNow);
var tzdb = DateTimeZoneProviders.Tzdb;
var list = from id in tzdb.Ids
           where id.Contains("/") && !id.StartsWith("etc", StringComparison.OrdinalIgnoreCase)
           let tz = tzdb[id]
           let offset = tz.GetOffsetFromUtc(now)
           orderby offset, id
           select new
           {
               Id = id,
               DisplayValue = string.Format("({0}) {1}", offset.ToString("+HH:mm", null), id)
           };

// ultimately we build a dropdown list, but for demo purposes you can just dump the results
foreach (var item in list)
    Console.WriteLine(item.DisplayValue);
Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575

2 Answers2

26

Noda Time 1.1 has the zone.tab data, so you can now do the following:

/// <summary>
/// Returns a list of valid timezones as a dictionary, where the key is
/// the timezone id, and the value can be used for display.
/// </summary>
/// <param name="countryCode">
/// The two-letter country code to get timezones for.
/// Returns all timezones if null or empty.
/// </param>
public IDictionary<string, string> GetTimeZones(string countryCode)
{
    var now = SystemClock.Instance.Now;
    var tzdb = DateTimeZoneProviders.Tzdb;

    var list = 
        from location in TzdbDateTimeZoneSource.Default.ZoneLocations
        where string.IsNullOrEmpty(countryCode) ||
              location.CountryCode.Equals(countryCode, 
                                          StringComparison.OrdinalIgnoreCase)
        let zoneId = location.ZoneId
        let tz = tzdb[zoneId]
        let offset = tz.GetZoneInterval(now).StandardOffset
        orderby offset, zoneId
        select new
        {
            Id = zoneId,
            DisplayValue = string.Format("({0:+HH:mm}) {1}", offset, zoneId)
        };

    return list.ToDictionary(x => x.Id, x => x.DisplayValue);
}

Alternative Approach

Instead of providing a drop down at all, you can use a map-based timezone picker.

enter image description here

Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
3

Getting the standard offset is easy - tz.GetZoneInterval(now).StandardOffset. That will give you the "current" standard offset (it's possible for a zone to change over time).

The filtering may be appropriate for you - I wouldn't like to say for sure. It's certainly not ideal in that the IDs aren't really designed for display. Ideally you'd use the Unicode CLDR "example" places, but we don't have any CLDR integration on that front at the moment.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Is there some clean way to filter out historical timezones? The TZ database does have From and To dates that timezones are valid for. Does Noda expose this in any way? – Matt Johnson-Pint Oct 25 '12 at 14:52
  • @MattJohnson: No - which bit of TZDB exposes that? I must confess I've never fully understood the format, but if it's something we can add, we may do so (though probably not for 1.0). (From and To are usually for particular rules within a time zone, and I thought the final rule always had a To of "max".) – Jon Skeet Oct 25 '12 at 14:55
  • I stumbled upon this page http://69.36.11.139/tzdb/tz-how-to.html which explains things fairly well. The Rule entries seem to have the data I would need. – Matt Johnson-Pint Oct 25 '12 at 14:59
  • For sake of filtering, I guess I would be interested in removing any timezones that did not have a "max" rule, but had a final To date that had already passed. But I guess I am assuming that there is always at least one Rule per Zone, and I don't know if that is the case or not. – Matt Johnson-Pint Oct 25 '12 at 15:04
  • @MattJohnson: Could you raise a feature request citing that web page, and giving Honolulu as an example? Currently we assume (in various places, actually) that time zones are perpetual... it sounds like we shouldn't. That could be, um, interesting. – Jon Skeet Oct 25 '12 at 15:06
  • @MattJohnson: Brilliant, thanks. Hoping to get 1.0 launched soon (genuinely soon) - this may be the first post-v1 feature I look at :) – Jon Skeet Oct 25 '12 at 15:11
  • I just found something a bit more useful. http://timezonedb.com/download I can use it to build a two-list selector, where the first list is the country code and the second is a timezone list filtered to the selected country. Should make a cleaner interface and I can still use Noda for calculations based on the selected timezone. – Matt Johnson-Pint Oct 25 '12 at 15:32
  • @MattJohnson: Cool - thanks for sharing that; will have a close look at it. – Jon Skeet Oct 25 '12 at 16:02
  • I found that the file from timezonedb.com is just a rehash of the zone.tab file in the tzdata. It doesn't look like noda currently includes this file, so I embedded it in my own app. I updated the question with the code I ultimately ended up using. – Matt Johnson-Pint Oct 25 '12 at 17:33