Zohar 基本上是正确的。关键是DateTime.ToFileTimeUtc,就像许多在DateTime 上工作的方法一样,依赖于与值关联的Kind。当DateTimeKind.Unspecified 被传递时,这个特定的方法假定输入已经是UTC。但是,在您的代码中,您正在创建这些值,就好像它们是根据给定的时区一样。
让我们把罪魁祸首归零:
var reminderStartTimeToUtc = (ulong)reminderstarttime.ToFileTimeUtc();
var referenceTimeToUtc = (ulong)referencetime.ToFileTimeUtc();
由于reminderstarttime 和referencetime 都有Kind == DateTimeKind.Unspecified,因此它们生成的文件时间值不正确。具体来说:
reminderStartTimeToUtc: 131651928000000000
we wanted: 131652216000000000
difference: -288000000000 = -8 hours
referenceTimeToUtc: 131652288000000000
we wanted: 131652540000000000
difference: -252000000000 = -7 hours
如您所见,它们的值因每个日期与 UTC 的差异而不同。
使用DateTime.FromFileTimeUtc 将它们转换回您的代码中会返回确实 具有DateTimeKind.Utc 的值,这会引发后续的夏令时检查:
reminderStartTimeFromUtc: 2018-03-10 22:00:00 UTC
which is equivalent to: 2018-03-10 14:00:00 PST (UTC-8)
we wanted: 2018-03-10 22:00:00 PST (UTC-8)
referencetimeFromUtc: 2018-03-11 08:00:00 UTC
which is equivalent to: 2018-03-11 00:00:00 PST (UTC-8)
we wanted: 2018-03-11 08:00:00 PDT (UTC-8)
请注意,从 PST 到 PDT 的切换发生在 02:00 PST,因此这两个值仍然是标准时间。
那么我们如何在没有 hack 的情况下获得正确的 this?只需确保我们的输入值是DateTimeKind.Utc,然后再将其转换为 Windows 文件时间。 (DateTimeKind.Local 也可以,但这里不需要涉及本地时区)
// First convert the DateTime values from their unspecified zone-specific times to UTC
var reminderStartTimeUtc = TimeZoneInfo.ConvertTimeToUtc(reminderstarttime, tzInfo);
var referenceTimeUtc = TimeZoneInfo.ConvertTimeToUtc(referencetime, tzInfo);
// Then convert THOSE values to file-times.
var reminderStartTimeToUtc = (ulong)reminderStartTimeUtc.ToFileTimeUtc();
var referenceTimeToUtc = (ulong)referenceTimeUtc.ToFileTimeUtc();
其余代码将按原样正确执行,您将获得预期的结果。
请注意,这些方法的措辞有些混乱。 DateTime.ToFileTimeUtc 表示您正在转换为文件时间,输入DateTime 和.Kind == DateTimeKind.Unspecified 将被视为DateTimeKind.Utc。另一种方法DateTime.ToFileTime 将Unspecified 种类视为Local。但它们都对待Utc 和Local 种类相同,并且它们都产生Windows 文件时间,它本质上是基于UTC 的。
作为上述方法的替代方案,您可以改用DateTimeOffset.ToFileTime。在转换为文件时间期间将正确考虑偏移量。
// construct a DateTimeOffset for each value
var reminderStartTimeDto = new DateTimeOffset(reminderstarttime, tzInfo.GetUtcOffset(reminderstarttime));
var referencetimeDto = new DateTimeOffset(referencetime, tzInfo.GetUtcOffset(referencetime));
// then just convert them to file times
var reminderStartTimeAsFileTime = reminderStartTimeDto.ToFileTime();
var referenceTimeAsFileTime = referencetimeDto.ToFileTime();
注意这里没有ToFileTimeUtc,因为DateTimeOffset上没有Kind,所以只有一种方法可以转换它。
最后一件事。请注意,DateTime.AddHours(10) 不遵守 DST 间隔。因此,当您谈论太平洋夏令时间上午 8 点时,实际上只过去了 9 个小时,这是由于弹簧向前间隙。 10 个实际经过的小时将是太平洋夏令时间上午 9 点。如果您在添加 10 小时之前 保留 DateTimeOffset 类型的值,则可以轻松纠正此问题。