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?
source to share
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)
source to share
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 typeDateTime
, and hers.Kind
isDateTimeKind.Unspecified
. -
The format
formatLocal
contains a tagz
that returns the time zone offset. -
When the format specifier is applied,
z
cDateTime
is evaluatedKind
. 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 propertyDateTime.Kind
. For this reason, the "z" format specifier is not recommended for use withDateTime
values .With
DateTimeOffset values
this format specifier represents the offset of the valueDateTimeOffset
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
source to share