TimeZoneInfo and DST issues in .Net

I am working in a simple application to convert some Unix Timestamp dates to localtime. I print both UTC time and "E. South American Standard Time" β†’ (GMT-03: 00) Brasilia. The code below works fine, but it seems to be a mess 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 tested this code on three different machines and got 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 application (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 for something I'm missing?

+3


source to share


2 answers


Well, you probably just didn't notice that the Windows timezone data doesn't match the IANA data that Java uses, and that your two Windows 7 windows may have a different set of Windows updates. I wouldn't want to guess what exactly Mono uses, I'm afraid.

One option you might want to consider is to use my Noda Time library , which uses IANA data (and you can use any version of the data you want) and also as a 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)

      

+3


source


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

However, to explain your results:

  • On the last line, you format the variable dt

    as a string. This variable is a type DateTime

    , and hers .Kind

    is DateTimeKind.Unspecified

    .

  • The format formatLocal

    contains a tag z

    that returns the time zone offset.

  • When the format specifier is applied, z

    c DateTime

    is evaluated Kind

    . For the type, Utc

    it emits "+0"

    . For a type, Local

    it emits an offset for the local time zone where the computer is running. For the type, Unspecified

    it is considered local.

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

MSDN says about this specifierz

:



When set to values, DateTime

the custom format specifier "z" is the signed time zone offset of the local operating system from Coordinated Universal Time (UTC), measured in hours. It does not reflect the value of the instance property DateTime.Kind

. For this reason, the "z" format specifier is not recommended for use with DateTime

values
.

With DateTimeOffset values

this format specifier represents the offset of the value DateTimeOffset

from UTC in hours.

This wording is a bit flawed as it DateTimeKind.Utc

does return "+0"

, but I think you get the point. You must use 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

      

+1


source







All Articles