72

How can I convert the following into a System.TimeZone or System.TimeZoneInfo?

{
  "timeZone": "America/Los_Angeles", 
  "currentOffsetMs": -25200000
}

This is data I'm getting back from a 3rd party web service.

I'm assuming the offset is the difference from UTC, and I'm told that the "America/Los_Angeles" is an Olson time zone. Java has no problems parsing this into a Java TimeZone, but I need to parse this into a C# TimeZoneInfo object.

Judah Gabriel Himango
  • 58,906
  • 38
  • 158
  • 212
  • 1
    That TZ database list is also available as a .NET library: codeplex.com/zoneinfo However, that doesn't help either; it doesn't return anything that can be mapped back to a standard .NET TimeZoneInfo. Grrrr. – Judah Gabriel Himango May 16 '11 at 17:08
  • 5
    @HansPassant - America/Los_Angeles is a timezone identifier from the IANA time zone database https://en.wikipedia.org/wiki/America/Los_Angeles. – Matthew Oct 03 '14 at 14:10

6 Answers6

112

This Unicode.org page has a "Olson time zone to Win32 time zone" table. From there, I created a nice little C# helper function to map from Olson time zone string into a .NET TimeZoneInfo:

/// <summary>
/// Converts an Olson time zone ID to a Windows time zone ID.
/// </summary>
/// <param name="olsonTimeZoneId">An Olson time zone ID. See http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/zone_tzid.html. </param>
/// <returns>
/// The TimeZoneInfo corresponding to the Olson time zone ID, 
/// or null if you passed in an invalid Olson time zone ID.
/// </returns>
/// <remarks>
/// See http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/zone_tzid.html
/// </remarks>
public static TimeZoneInfo OlsonTimeZoneToTimeZoneInfo(string olsonTimeZoneId)
{
    var olsonWindowsTimes = new Dictionary<string, string>()
    {
        { "Africa/Bangui", "W. Central Africa Standard Time" },
        { "Africa/Cairo", "Egypt Standard Time" },
        { "Africa/Casablanca", "Morocco Standard Time" },
        { "Africa/Harare", "South Africa Standard Time" },
        { "Africa/Johannesburg", "South Africa Standard Time" },
        { "Africa/Lagos", "W. Central Africa Standard Time" },
        { "Africa/Monrovia", "Greenwich Standard Time" },
        { "Africa/Nairobi", "E. Africa Standard Time" },
        { "Africa/Windhoek", "Namibia Standard Time" },
        { "America/Anchorage", "Alaskan Standard Time" },
        { "America/Argentina/San_Juan", "Argentina Standard Time" },
        { "America/Asuncion", "Paraguay Standard Time" },
        { "America/Bahia", "Bahia Standard Time" },
        { "America/Bogota", "SA Pacific Standard Time" },
        { "America/Buenos_Aires", "Argentina Standard Time" },
        { "America/Caracas", "Venezuela Standard Time" },
        { "America/Cayenne", "SA Eastern Standard Time" },
        { "America/Chicago", "Central Standard Time" },
        { "America/Chihuahua", "Mountain Standard Time (Mexico)" },
        { "America/Cuiaba", "Central Brazilian Standard Time" },
        { "America/Denver", "Mountain Standard Time" },
        { "America/Fortaleza", "SA Eastern Standard Time" },
        { "America/Godthab", "Greenland Standard Time" },
        { "America/Guatemala", "Central America Standard Time" },
        { "America/Halifax", "Atlantic Standard Time" },
        { "America/Indianapolis", "US Eastern Standard Time" },
        { "America/Indiana/Indianapolis", "US Eastern Standard Time" },
        { "America/La_Paz", "SA Western Standard Time" },
        { "America/Los_Angeles", "Pacific Standard Time" },
        { "America/Mexico_City", "Mexico Standard Time" },
        { "America/Montevideo", "Montevideo Standard Time" },
        { "America/New_York", "Eastern Standard Time" },
        { "America/Noronha", "UTC-02" },
        { "America/Phoenix", "US Mountain Standard Time" },
        { "America/Regina", "Canada Central Standard Time" },
        { "America/Santa_Isabel", "Pacific Standard Time (Mexico)" },
        { "America/Santiago", "Pacific SA Standard Time" },
        { "America/Sao_Paulo", "E. South America Standard Time" },
        { "America/St_Johns", "Newfoundland Standard Time" },
        { "America/Tijuana", "Pacific Standard Time" },
        { "Antarctica/McMurdo", "New Zealand Standard Time" },
        { "Atlantic/South_Georgia", "UTC-02" },
        { "Asia/Almaty", "Central Asia Standard Time" },
        { "Asia/Amman", "Jordan Standard Time" },
        { "Asia/Baghdad", "Arabic Standard Time" },
        { "Asia/Baku", "Azerbaijan Standard Time" },
        { "Asia/Bangkok", "SE Asia Standard Time" },
        { "Asia/Beirut", "Middle East Standard Time" },
        { "Asia/Calcutta", "India Standard Time" },
        { "Asia/Colombo", "Sri Lanka Standard Time" },
        { "Asia/Damascus", "Syria Standard Time" },
        { "Asia/Dhaka", "Bangladesh Standard Time" },
        { "Asia/Dubai", "Arabian Standard Time" },
        { "Asia/Irkutsk", "North Asia East Standard Time" },
        { "Asia/Jerusalem", "Israel Standard Time" },
        { "Asia/Kabul", "Afghanistan Standard Time" },
        { "Asia/Kamchatka", "Kamchatka Standard Time" },
        { "Asia/Karachi", "Pakistan Standard Time" },
        { "Asia/Katmandu", "Nepal Standard Time" },
        { "Asia/Kolkata", "India Standard Time" },
        { "Asia/Krasnoyarsk", "North Asia Standard Time" },
        { "Asia/Kuala_Lumpur", "Singapore Standard Time" },
        { "Asia/Kuwait", "Arab Standard Time" },
        { "Asia/Magadan", "Magadan Standard Time" },
        { "Asia/Muscat", "Arabian Standard Time" },
        { "Asia/Novosibirsk", "N. Central Asia Standard Time" },
        { "Asia/Oral", "West Asia Standard Time" },
        { "Asia/Rangoon", "Myanmar Standard Time" },
        { "Asia/Riyadh", "Arab Standard Time" },
        { "Asia/Seoul", "Korea Standard Time" },
        { "Asia/Shanghai", "China Standard Time" },
        { "Asia/Singapore", "Singapore Standard Time" },
        { "Asia/Taipei", "Taipei Standard Time" },
        { "Asia/Tashkent", "West Asia Standard Time" },
        { "Asia/Tbilisi", "Georgian Standard Time" },
        { "Asia/Tehran", "Iran Standard Time" },
        { "Asia/Tokyo", "Tokyo Standard Time" },
        { "Asia/Ulaanbaatar", "Ulaanbaatar Standard Time" },
        { "Asia/Vladivostok", "Vladivostok Standard Time" },
        { "Asia/Yakutsk", "Yakutsk Standard Time" },
        { "Asia/Yekaterinburg", "Ekaterinburg Standard Time" },
        { "Asia/Yerevan", "Armenian Standard Time" },
        { "Atlantic/Azores", "Azores Standard Time" },
        { "Atlantic/Cape_Verde", "Cape Verde Standard Time" },
        { "Atlantic/Reykjavik", "Greenwich Standard Time" },
        { "Australia/Adelaide", "Cen. Australia Standard Time" },
        { "Australia/Brisbane", "E. Australia Standard Time" },
        { "Australia/Darwin", "AUS Central Standard Time" },
        { "Australia/Hobart", "Tasmania Standard Time" },
        { "Australia/Perth", "W. Australia Standard Time" },
        { "Australia/Sydney", "AUS Eastern Standard Time" },
        { "Etc/GMT", "UTC" },
        { "Etc/GMT+11", "UTC-11" },
        { "Etc/GMT+12", "Dateline Standard Time" },
        { "Etc/GMT+2", "UTC-02" },
        { "Etc/GMT-12", "UTC+12" },
        { "Europe/Amsterdam", "W. Europe Standard Time" },
        { "Europe/Athens", "GTB Standard Time" },
        { "Europe/Belgrade", "Central Europe Standard Time" },
        { "Europe/Berlin", "W. Europe Standard Time" },
        { "Europe/Brussels", "Romance Standard Time" },
        { "Europe/Budapest", "Central Europe Standard Time" },
        { "Europe/Dublin", "GMT Standard Time" },
        { "Europe/Helsinki", "FLE Standard Time" },
        { "Europe/Istanbul", "GTB Standard Time" },
        { "Europe/Kiev", "FLE Standard Time" },
        { "Europe/London", "GMT Standard Time" },
        { "Europe/Minsk", "E. Europe Standard Time" },
        { "Europe/Moscow", "Russian Standard Time" },
        { "Europe/Paris", "Romance Standard Time" },
        { "Europe/Sarajevo", "Central European Standard Time" },
        { "Europe/Warsaw", "Central European Standard Time" },
        { "Indian/Mauritius", "Mauritius Standard Time" },
        { "Pacific/Apia", "Samoa Standard Time" },
        { "Pacific/Auckland", "New Zealand Standard Time" },
        { "Pacific/Fiji", "Fiji Standard Time" },
        { "Pacific/Guadalcanal", "Central Pacific Standard Time" },
        { "Pacific/Guam", "West Pacific Standard Time" },
        { "Pacific/Honolulu", "Hawaiian Standard Time" },
        { "Pacific/Pago_Pago", "UTC-11" },
        { "Pacific/Port_Moresby", "West Pacific Standard Time" },
        { "Pacific/Tongatapu", "Tonga Standard Time" }
    };

    var windowsTimeZoneId = default(string);
    var windowsTimeZone = default(TimeZoneInfo);
    if (olsonWindowsTimes.TryGetValue(olsonTimeZoneId, out windowsTimeZoneId))
    {
        try { windowsTimeZone = TimeZoneInfo.FindSystemTimeZoneById(windowsTimeZoneId); }
        catch (TimeZoneNotFoundException) { }
        catch (InvalidTimeZoneException) { }
    }
    return windowsTimeZone;
}
Judah Gabriel Himango
  • 58,906
  • 38
  • 158
  • 212
32

Here is a reverse mapping (tzdb -> windows) function using NodaTime:

using NodaTime;
using NodaTime.TimeZones;

...

public TimeZoneInfo GetTimeZoneInfoForTzdbId(string tzdbId)
{
  var mappings = TzdbDateTimeZoneSource.Default.WindowsMapping.MapZones;
  var map = mappings.FirstOrDefault(x =>
      x.TzdbIds.Any(z => z.Equals(tzdbId, StringComparison.OrdinalIgnoreCase)));
  return map == null ? null : TimeZoneInfo.FindSystemTimeZoneById(map.WindowsId);
}

Note that it is possible for there to be more than one mapping (in which case this just uses the first one found), or no mapping at all (where this returns null).

In most commonly used time zones, this should work well enough. But the better solution would be to skip using TimeZoneInfo at all, and just use NodaTime throughout the application, directly with the TZDB zone you have.

See also: How to translate between Windows and IANA time zones?

Community
  • 1
  • 1
Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
12

You might want to look into Jon Skeet's Noda-Time and abandon TimeZoneInfo altogether. Noda-Time uses Olson timezones so your mapping would be a piece of cake. There are other reasons why you might want to use it :

What's wrong with DateTime anyway?

SO question of mine the outcome of which was to use Noda-Time

Community
  • 1
  • 1
Stephen Kennedy
  • 20,585
  • 22
  • 95
  • 108
11

A little snippet I came up with to get a list of olson to windows time zone mappings from the xml at http://unicode.org/repos/cldr/trunk/common/supplemental/windowsZones.xml

private static void LoadMappingsO()
{
    var file = new FileInfo("windowsZones.xml");
    if (!file.Exists)
    {
        return;
    }

    var map = new Dictionary<string, string>();
    using (var reader = file.OpenText())
    {
        var readerSettings = new XmlReaderSettings { XmlResolver = null, ProhibitDtd = false };

        using (var xmlReader = XmlReader.Create(reader, readerSettings))
        {
            var document = new XPathDocument(xmlReader);
            var navigator = document.CreateNavigator();

            var nodes = navigator.Select("/supplementalData/windowsZones/mapTimezones/mapZone");

            while (nodes.MoveNext())
            {
                var node = nodes.Current;
                if (node == null) continue;

                var olsonNames = node.GetAttribute("type", "").Split(' ');
                var windowsName = node.GetAttribute("other", "");
                foreach (var olsonName in olsonNames)
                {
                    if (!map.ContainsKey(olsonName))
                    {
                        map.Add(olsonName, windowsName);
                    }
                }
            }
        }
    }

    using (TextWriter tw = new StreamWriter("dict.txt", false))
    {
        foreach (var key in map.Keys)
        {
            tw.WriteLine(string.Format("{{\"{0}\", \"{1}\"}},", key, map[key]));
        }
    }
}

UPDATE (Using Linq Xml):

private static void LoadMappings()
{
    var map = new Dictionary<string, string>();
    var xdoc = XDocument.Load("windowsZones.xml");

    var zones = xdoc.XPathSelectElements("/supplementalData/windowsZones/mapTimezones/mapZone");
    foreach (var zone in zones)
    {
        var olsonNames = zone.Attribute("type")?.Value.Split(' ');
        if (olsonNames == null)
            continue;

        var windowsName = zone.Attribute("other")?.Value;
        if (string.IsNullOrWhiteSpace(windowsName))
            continue;

        foreach (var olsonName in olsonNames)
        {
            map[olsonName] = windowsName;
        }
    }

    using (TextWriter tw = new StreamWriter("dict.txt", false))
    {
        foreach (var key in map.Keys)
        {
            tw.WriteLine($"{{\"{key}\", \"{map[key]}\"}},");
        }
    }
}
H77
  • 5,859
  • 2
  • 26
  • 39
  • Nice. I wonder if it could be cleaner via LINQ-to-XML. But thanks. – Judah Gabriel Himango Mar 02 '12 at 16:31
  • 2
    @Hosney yes but please do NOT write code that automatically fetches files from unicode.org. Download a CLDR release and include the file in your app. This URL is getting abusive downloads now. – Steven R. Loomis Jul 02 '14 at 23:16
  • The code does NOT fetch files from unicode.org. As you can see the code uses FileInfo which reads the xml file stored locally in the app's path. – H77 Jul 03 '14 at 00:45
  • @Hosney Yes - it was more a warning to those who might do something different than the code here. thanks though! – Steven R. Loomis Jul 03 '14 at 01:47
4

UPDATE: I've removed the URL from the script. Please source the file manually. This script wasn't intended to run constantly putting unnecessary load on unicode.org. See the comments below.

This Powershell script can be used to generate a case statement using the current XML file from unicode.org. It generates mappings from IANA names to TimeZoneInfoId's.

    # Download the xml file.
    $xml = [Xml] /// Load the XML content here

    # Parse the fields we want from the XML.
    $mappings1 = $xml.supplementalData.windowsZones.mapTimezones.mapZone | select Type,Other 

    # Extrapolate extra rows for entries that contain more than one IANA name seperated by spaces.
    # Example: |<mapZone other="Alaskan Standard Time" territory="US" type="America/Anchorage America/Juneau America/Nome America/Sitka America/Yakutat"/>
    $mappings2 = $mappings1 | %{
        $mapping = $_
        $_.Type.Split(" ") | %{
            New-Object PSObject -Property @{type = $_; other = $mapping.other}
        }  
    }

    # Remove dup's
    $mappings3 = $mappings2 | sort type -Unique

    # Generate the case statements.
    $mappings3 | %{ [String]::Format("case @""{0}"": return @""{1}"";", $_.Type, $_.Other)}
Jared Kells
  • 6,771
  • 4
  • 38
  • 43
  • 3
    Just a note to you and any readers - please do not unconditionally download this XML file. It is getting abusive downloads. Really, download it once manually and use it. Thanks. – Steven R. Loomis Jul 03 '14 at 01:48
  • 2
    @Jared;Would you mind please changing this code snippet not to fetch the `windowsZones.xml` directly from unicode.org SVN? (either read it from the disk or assume that the user downloads it manually). Currently,direct fetch of this file sucks a very large portion of their network bandwidth. The request just spiked into millions of hits few days ago. This shows that someone's code is iterating on this many, many times. Although it doesn't solve the problem of those hits (since whoever is doing that probably won't see this message),but it would prevent future cases of this from happening.Thanks. – Shervin Jul 03 '14 at 01:49
  • 2
    Hey, I've removed the URL from the script. It was never intended to be run constantly. Personally I only run it once every 6 months or so. – Jared Kells Jul 03 '14 at 05:52
  • thanks for this, was able to convert it into a function that worked for me. – ATek Jan 17 '19 at 06:28
2

After converting the currentOffsetMs into hours and leftover minutes, you can enumerate the defined TimeZoneInfo objects:

foreach (TimeZoneInfo nextZone in TimeZoneInfo.GetSystemTimeZones())
{
    int nextHours = nextZone.BaseUtcOffset.Hours + 24;     // To prevent negative numbers
    int nextMinutes = nextZone.BaseUtcOffset.Minutes;
    if (tzHours == nextHours && tzMinutes == nextMinutes)
    {
        myTimeZoneInfo = nextZone;
        break;
    }
}
Ed Bayiates
  • 11,060
  • 4
  • 43
  • 62
  • 1
    Helpful, but not precise enough. Multiple time zones have the same offset "UTC-07:00 Chihuahua, La Paz, Mazatlan" and "UTC-07:00 Arizona" share the same offset. – Judah Gabriel Himango May 13 '11 at 19:21