13

I'm using NodaTime because of its nice support for zoneinfo data, however I have a case where I need to convert the DateTimeZone into TimeZoneInfo for use in Quartz.NET.

What's the recommended approach here? IANA has a mapping file between Windows time zones and zoneinfo time zones, can I create an extension method that utilises this information?

Dean Ward
  • 4,793
  • 1
  • 29
  • 36
  • Which IANA file do you mean, by the way? I know there's a CLDR one, but if IANA provides this in a different format, that would be interesting... – Jon Skeet Feb 23 '13 at 08:33

5 Answers5

14

I would avoid using reflection if possible. I wouldn't like to bet on your approach working with future versions :)

Feel free to file a feature request for this functionality for future versions, but for the moment I'd build up your reverse dictionary in a more stable way:

// Note: this version lets you work with any IDateTimeZoneSource, although as the only
// other built-in source is BclDateTimeZoneSource, that may be less useful :)
private static IDictionary<string, string> LoadTimeZoneMap(IDateTimeZoneSource source)
{
    var nodaToWindowsMap = new Dictionary<string, string>();
    foreach (var bclZone in TimeZoneInfo.GetSystemTimeZones())
    {
        var nodaId = source.MapTimeZoneId(bclZone);
        if (nodaId != null)
        {
            nodaToWindowsMap[nodaId] = bclZone.Id;
        }
    }
    return nodaToWindowsMap;
}

Of course, this won't cover all the time zones in TZDB. In fact, it won't even give all the information we could give based on the CLDR information we use... CLDR gives multiple mappings for each Windows ID, and we only store the first one at the moment. We've been trying to work out how to expose more of that, but haven't managed yet. Thoughts welcome on the Noda Time mailing list :)

Also note that just because there's a mapping between the BCL and TZDB zones doesn't mean they'll actually give the same results for everything - it's just the closest mapping available.

SethO
  • 2,703
  • 5
  • 28
  • 38
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Thanks Jon, this approach is much better. Taken on board your comments about the possibility of differing data between TZDB and CLDR. We've taken the list of time zones needed by our end-user devices and they have a one-to-one mapping to valid Windows time zones at the moment. It's possible down the line we need to think up a different approach but this works for now. – Dean Ward Feb 23 '13 at 15:33
  • 1
    @DeanWard: If it's any help, I've started implementing [issue 82](https://code.google.com/p/noda-time/issues/detail?id=82) today to provide more information about the mappings. I don't know for sure whether it'll make it into the 1.1 release though. – Jon Skeet Feb 23 '13 at 16:38
  • 1
    If anyone is looking for the class that implements `IDateTimeZoneSource`: `TzdbDateTimeZoneSource.Default.MapTimeZoneId(myTimeZoneInfo)` – Muhammad Rehan Saeed Sep 07 '16 at 15:33
5

Aha, I found it - TzdbDateTimeZoneSource has a MapTimeZoneId method that I can pop into TimeZoneInfo.FindSystemTimeZoneById.

Edit: MapTimeZoneId does the mapping from Windows time zone into zoneinfo... I ended up resorting to reflection to do the mapping in the opposite direction:

using System;
using System.Collections.Generic;
using System.Reflection;

using NodaTime;
using NodaTime.TimeZones;

/// <summary>
/// Extension methods for <see cref="DateTimeZone" />.
/// </summary>
internal static class DateTimeZoneExtensions
{
    private static readonly Lazy<IDictionary<string, string>> map = new Lazy<IDictionary<string, string>>(LoadTimeZoneMap, true);

    public static TimeZoneInfo ToTimeZoneInfo(this DateTimeZone timeZone)
    {
        string id;
        if (!map.Value.TryGetValue(timeZone.Id, out id))
        {
            throw new TimeZoneNotFoundException(string.Format("Could not locate time zone with identifier {0}", timeZone.Id));
        }

        TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(id);
        if (timeZoneInfo == null)
        {
            throw new TimeZoneNotFoundException(string.Format("Could not locate time zone with identifier {0}", timeZone.Id));
        }

        return timeZoneInfo;
    }

    private static IDictionary<string, string> LoadTimeZoneMap()
    {
        TzdbDateTimeZoneSource source = new TzdbDateTimeZoneSource("NodaTime.TimeZones.Tzdb");
        FieldInfo field = source.GetType().GetField("windowsIdMap", BindingFlags.Instance | BindingFlags.NonPublic);
        IDictionary<string, string> map = (IDictionary<string, string>)field.GetValue(source);

        // reverse the mappings
        Dictionary<string, string> reverseMap = new Dictionary<string, string>();
        foreach (KeyValuePair<string, string> kvp in map)
        {
            reverseMap.Add(kvp.Value, kvp.Key);
        }

        return reverseMap;
    }
}
Dean Ward
  • 4,793
  • 1
  • 29
  • 36
  • 2
    This will help if you don't want to new up the source: NodaTime.TimeZones.TzdbDateTimeZoneSource.Default – jsgoupil Nov 12 '15 at 01:59
3

You can use TimeZoneConverter library by Matt Johnson.

ZoneId used by NodeTime TzdbZoneLocation is IANA time zone, so you can get TimeZoneInfo like this:

string windowsTimeZoneName = TZConvert.IanaToWindows(tzdbZoneLocation.ZoneId);
var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(windowsTimeZoneName);

Don't forget to wrap it with try-catch with some kind of fallback just in case.

Also look at original Matt Johnson solution for converting between IANA time zone and Windows time zone.

tseshevsky
  • 761
  • 8
  • 10
  • This is the only solution here that worked for me. Thank you! Hint: You can convert the timezone names manually using this solution: https://stackoverflow.com/questions/5996320/net-timezoneinfo-from-olson-time-zone/6144229#6144229 But I found just using the TimeZoneConverter NuGet most convinient. – MovGP0 Aug 23 '18 at 09:35
2

However none of this works in a PCL because most of the work is done by .NET in the .GetSystemTImeZones() and .FindSystemTIemZoneById() methods - which don't exist in PCL.

I'm stunned that for all the info you can get out of NodaTime, getting something as simple as "EST" abbreviation when you already have the zone name of "US/Eastern" seems to have stopped me in my tracks.

Clint StLaurent
  • 1,238
  • 12
  • 11
1

With NodaTime assembly version 3.0.9.0 the mapping of tzdb time zone ids (NodaTime.DateTimeZone.Id) to dotnet time zone info ids (System.TimeZoneInfo.Id) and the reverse process is achievable via these two string key and value dictionaries: NodaTime.TimeZones.TzdbDateTimeZoneSource.Default.TzdbToWindowsIds, NodaTime.TimeZones.TzdbDateTimeZoneSource.Default.WindowsToTzdbIds.

andrei.ciprian
  • 2,895
  • 1
  • 19
  • 29