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?

Upvotes: 3

Views: 294

Answers (2)

Matt Johnson-Pint
Matt Johnson-Pint

Reputation: 241758

I agree with Jon that Noda Time is much better for this scenario. I highly recommend you go with his implementation.

However, just to explain your results:

  • In the last line, you format the dt variable as a string. This variable is a DateTime type, and its .Kind is DateTimeKind.Unspecified.

  • Your formatLocal formatter contains the z token to return the time zone offset.

  • When you apply the z format specifier with a DateTime, the Kind is evaluated. For Utc kind, it emits "+0". For Local kind, it emits the offset for the local time zone where the computer runs. For Unspecified kind, it is treated as local.

So the offsets are not necessarily from the time zone you converted to, but from your local computer's time zone!

MSDN says this about the z specifier:

With DateTime values, the "z" custom format specifier represents the signed offset of the local operating system's time zone from Coordinated Universal Time (UTC), measured in hours. It does not reflect the value of an instance's DateTime.Kind property. For this reason, the "z" format specifier is not recommended for use with DateTime values.

With DateTimeOffset values, this format specifier represents the DateTimeOffset value's offset from UTC in hours.

That wording is slightly incorrect, since DateTimeKind.Utc does indeed return "+0", but I think you get the point. You should be using DateTimeOffset.

DateTimeOffset epoch = new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero);

foreach (long ts in timestamps)
{
    DateTimeOffset dto = epoch.AddSeconds(ts);

    Console.Write(formatUtc, dto);

    dto = TimeZoneInfo.ConvertTime(dto, tzBr);
    Console.WriteLine("\t\t" + formatLocal, dto);
}
UTC                             America/Sao_Paulo
---------------------------------------------------------
19 Oct 2014 02:30:00            18 Oct 2014 23:30:00 -3
19 Oct 2014 03:30:00            19 Oct 2014 01:30:00 -2
22 Feb 2015 01:30:00            21 Feb 2015 23:30:00 -2
22 Feb 2015 02:30:00            21 Feb 2015 23:30:00 -3
22 Feb 2015 03:30:00            22 Feb 2015 00:30:00 -3

Upvotes: 1

Jon Skeet
Jon Skeet

Reputation: 1502696

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)

Upvotes: 3

Related Questions