Problems with TimeZoneInfo and DST in .Net

I'm working in a simple application to convert some Unix Timestamp dates to localtime. I'm printing both, UTC time and "E. South America Standard Time" -> (GMT-03:00) Brasilia. The code below runs fine, but seems to mess things with DST:

    public static void Main (string[] args)
    {
        long[] timestamps = {1413685800L, 1413689400L, 1424568600L, 1424572200L, 1424575800L};
        string formatUtc = "{0:dd MMM yyyy HH:mm:ss}";
        string formatLocal = "{0:dd MMM yyyy HH:mm:ss z}";
        TimeZoneInfo tzBr = null;

        tzBr = TimeZoneInfo.FindSystemTimeZoneById("E. South America Standard Time");

        DateTime dt;

        Console.WriteLine("UTC\t\t\t\tAmerica/Sao_Paulo");                     
        Console.WriteLine("---------------------------------------------------------");


        foreach (long ts in timestamps) {
            dt = new DateTime(1970,1,1,0,0,0,0,System.DateTimeKind.Utc).AddSeconds(ts);

            Console.Write(string.Format(formatUtc, dt));

            dt = TimeZoneInfo.ConvertTime(dt, TimeZoneInfo.Utc, tzBr);
            Console.WriteLine("\t\t" + string.Format(formatLocal, dt));
        }
    }

I've tested this code in three different machines getting the following results:

Windows 7 (.Net):

    UTC                         America/Sao_Paulo
---------------------------------------------------------
19 out 2014 02:30:00            18 out 2014 23:30:00 -3
19 out 2014 03:30:00            19 out 2014 01:30:00 -2
22 fev 2015 01:30:00            21 fev 2015 23:30:00 -3 <- Wrong!
22 fev 2015 02:30:00            21 fev 2015 23:30:00 -3
22 fev 2015 03:30:00            22 fev 2015 00:30:00 -3

Another Windows 7 box (.Net):

UTC                             America/Sao_Paulo
---------------------------------------------------------
19 out 2014 02:30:00 -3         18 out 2014 23:30:00 -3
19 out 2014 03:30:00 -3         19 out 2014 01:30:00 -3 <- Wrong!
22 fev 2015 01:30:00 -3         21 fev 2015 23:30:00 -3 <- Wrong!
22 fev 2015 02:30:00 -3         21 fev 2015 23:30:00 -3
22 fev 2015 03:30:00 -3         22 fev 2015 00:30:00 -3

Linux Fedora 22 (Mono):

UTC                             America/Sao_Paulo
---------------------------------------------------------
19 out 2014 02:30:00            18 out 2014 23:30:00 -3
19 out 2014 03:30:00            19 out 2014 01:30:00 -2
22 fev 2015 01:30:00            21 fev 2015 22:30:00 -2 <- Wrong!
22 fev 2015 02:30:00            21 fev 2015 23:30:00 -2 <- Wrong!
22 fev 2015 03:30:00            22 fev 2015 00:30:00 -3

Expected results, from Java app (BRT means -3 and BRST means -2):

UTC                             America/Sao_Paulo
---------------------------------------------------------
19 Out 2014 02:30:00 UTC        18 Out 2014 23:30:00 BRT
19 Out 2014 03:30:00 UTC        19 Out 2014 01:30:00 BRST
22 Fev 2015 01:30:00 UTC        21 Fev 2015 23:30:00 BRST
22 Fev 2015 02:30:00 UTC        21 Fev 2015 23:30:00 BRT
22 Fev 2015 03:30:00 UTC        22 Fev 2015 00:30:00 BRT

Any suggestions on something I'm missing?

Jon Skeet
people
quotationmark

Well, you're probably just missing the fact that the Windows time zone data is not the same as the IANA data that Java is using, and that your two Windows 7 boxes probably have a different set of Windows Updates applied. I wouldn't like to guess at exactly what Mono's using, I'm afraid.

One option you might want to consider is using my Noda Time library, which uses the IANA data (and allows you to use whichever version of that data you want), as well as being a generally better API, IMO. Here's the equivalent code:

using System;

using NodaTime;
using NodaTime.Text;

class Test
{

    public static void Main (string[] args)
    {
        long[] timestamps = {1413685800L, 1413689400L, 1424568600L, 1424572200L, 1424575800L};

        var zone = DateTimeZoneProviders.Tzdb["America/Sao_Paulo"];
        var instantPattern = InstantPattern.CreateWithInvariantCulture("dd MMM yyyy HH:mm:ss");
        var zonedPattern = ZonedDateTimePattern.CreateWithInvariantCulture
            ("dd MMM yyyy HH:mm:ss o<g> (x)", null);

        foreach (long ts in timestamps) {
            var instant = Instant.FromSecondsSinceUnixEpoch(ts);
            var zonedDateTime = instant.InZone(zone);            

            Console.WriteLine("{0} UTC - {1}",                              
                instantPattern.Format(instant),
                zonedPattern.Format(zonedDateTime));
        }
    }
}

Output:

19 Oct 2014 02:30:00 UTC - 18 Oct 2014 23:30:00 -03 (BRT)
19 Oct 2014 03:30:00 UTC - 19 Oct 2014 01:30:00 -02 (BRST)
22 Feb 2015 01:30:00 UTC - 21 Feb 2015 23:30:00 -02 (BRST)
22 Feb 2015 02:30:00 UTC - 21 Feb 2015 23:30:00 -03 (BRT)
22 Feb 2015 03:30:00 UTC - 22 Feb 2015 00:30:00 -03 (BRT)

people

See more on this question at Stackoverflow