.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
类型,它的.Kind
是DateTimeKind.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
我正在使用一个简单的应用程序将一些 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
类型,它的.Kind
是DateTimeKind.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'sDateTime.Kind
property. For this reason, the "z" format specifier is not recommended for use withDateTime
values.With
DateTimeOffset values
, this format specifier represents theDateTimeOffset
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