.Net 中 TimeZoneInfo 和 DST 的问题

Problems with TimeZoneInfo and DST in .Net

我正在使用一个简单的应用程序将一些 Unix 时间戳日期转换为本地时间。我正在打印 UTC 时间和 "E. South America Standard Time" -> (GMT-03:00) Brasilia。下面的代码运行良好,但似乎与 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));
        }
    }

我在三台不同的机器上测试了这段代码,得到了以下结果:

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

另一个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(单声道):

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

来自 Java 应用的预期结果(BRT 表示 -3,BRST 表示 -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

对我遗漏的东西有什么建议吗?

好吧,您可能只是忽略了 Windows 时区数据与 Java 使用的 IANA 数据不同的事实,而且您的两个 Windows 7 个盒子可能应用了一组不同的 Windows 更新。恐怕我不想猜测 Mono 到底在用什么。

您可能要考虑的一个选项是使用我的 Noda Time 库,它使用 IANA 数据(并允许您使用您想要的任何版本的数据),并且通常更好 API, 国际海事组织。这是等效的代码:

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));
        }
    }
}

输出:

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)

我同意 Jon 的观点,Noda Time 更适合这种情况。我强烈建议您使用他的实现。

但是,只是为了解释你的结果:

  • 在最后一行中,将 dt 变量格式化为字符串。这个变量是DateTime类型,它的.KindDateTimeKind.Unspecified.

  • 您的 formatLocal 格式化程序包含 z 标记到 return 时区偏移量。

  • 当您将 z 格式说明符与 DateTime 一起应用时,将计算 Kind。对于 Utc 种类,它会发出 "+0"。对于 Local 种类,它发出计算机运行所在的本地时区的偏移量。对于 Unspecified 种类,它被视为 local.

所以偏移量不一定来自您转换到的时区,而是来自您本地计算机的时区!

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.

这个措辞有点不正确,因为 DateTimeKind.Utc 确实 return "+0",但我想你明白了。你应该使用 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