【问题标题】:DateTimeOffset adding TimeSpan returns invalid UTC offset for its TimeZoneInfoDateTimeOffset 添加 TimeSpan 为其 TimeZoneInfo 返回无效的 UTC 偏移量
【发布时间】:2011-01-02 02:32:18
【问题描述】:

我正在构建一个时间表达式库,该库必须正确全球化,因此可以在所有可用的时区工作。

现在我似乎被卡住了,因为我不确定如何在正确的夏令时 (DST) 中从 DateTimeOffset 对象中检索调整后的日期,当使用各种 .Add 移动天、小时、等等

有趣的是,我想出了一个解决本地系统时区的方法,但没有找到任何方法将相同的策略应用于任意时区。

我能够找到一个 sn-p(没有保留源代码,抱歉!),它试图通过偏移量反向查找时区信息,但由于有多个潜在结果,每个结果都可能具有不同的 DST 规则不管用。 (可能有一些优化可用,但我认为基本前提是有缺陷的)

public TimeZoneInfo GetTimeZoneInfo(DateTimeOffset Value)
{
    // Search available sytem time zones for a matching one
    foreach (var tzi in TimeZoneInfo.GetSystemTimeZones())
    {
        // Compare value offset with time zone offset
        if (tzi.GetUtcOffset(Value).Equals(Value.Offset))
        {
            return tzi;
        }
    }
}

要证明这些东西可能有点乏味,所以我将核心问题提取到几个方法和单元测试中,希望能证明我面临的问题。

public DateTimeOffset GetNextDay_Wrong(DateTimeOffset FromDateTimeOffset)
{
    // Cannot create a new DateTimeOffset using simply the supplied value's UtcOffset
    // because in PST, for example, it could be -7 or -8 depending on DST
    return new DateTimeOffset(FromDateTimeOffset.Date.AddDays(1), FromDateTimeOffset.Offset);
}

[TestMethod]
public void GetNextDay_WrongTest()
{
    var tz = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");

    var workingDate = new DateTime(2009, 11, 2, 0, 0, 0);
    var failingDate = new DateTime(2009, 11, 1, 0, 0, 0);

    var workingDate_tz = new DateTimeOffset(workingDate, tz.GetUtcOffset(workingDate));
    var failingDate_tz = new DateTimeOffset(failingDate, tz.GetUtcOffset(failingDate));

    var actual_workingDate_tz = GetNextDay_Wrong(workingDate_tz);
    var actual_failingDate_tz = GetNextDay_Wrong(failingDate_tz);

    var expected_workingDate = new DateTime(2009, 11, 3, 0, 0, 0);
    var expected_failingDate = new DateTime(2009, 11, 2, 0, 0, 0);

    var expected_workingDate_tz = new DateTimeOffset(expected_workingDate, tz.GetUtcOffset(expected_workingDate));
    var expected_failingDate_tz = new DateTimeOffset(expected_failingDate, tz.GetUtcOffset(expected_failingDate));

    Assert.AreEqual(expected_workingDate_tz, actual_workingDate_tz, "Should have found the following day's midnight");
    Assert.AreEqual(expected_failingDate_tz, actual_failingDate_tz, "Failing date does not have the correct offset for it's DST");
}

public DateTimeOffset GetNextDay_LooksRight(DateTimeOffset FromDateTimeOffset)
{
    // Because we cannot create a new DateTimeOffset we simply adjust the one provided!
    var temp = FromDateTimeOffset;
    // Move back to midnight of the current day
    temp = temp.Subtract(new TimeSpan(temp.Hour, temp.Minute, temp.Second));
    // Now move to the next day
    temp = temp.AddDays(1);
    // Let the DateTimeOffset class do it's magic
    temp = temp.ToLocalTime();
    // Check if the time zone has changed
    if (FromDateTimeOffset.Offset != temp.Offset)
    {
        // Calculate the change amount (could be 30 mins or even stranger)
        var delta = FromDateTimeOffset.Offset - temp.Offset;
        // Adjust the temp value by the delta
        temp = temp.Add(delta);
    }
    return temp.ToLocalTime();
}

[TestMethod]
public void GetNextDay_LooksRightTest()
{
    // Everything is looking good and the test passes now, so we're home free yeah?

    // { To work this needs to match your system's configured Local Time Zone, I'm in PST }
    var tz = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");

    var workingDate = new DateTime(2009, 11, 2, 0, 0, 0);
    var failingDate = new DateTime(2009, 11, 1, 0, 0, 0);

    var workingDate_tz = new DateTimeOffset(workingDate, tz.GetUtcOffset(workingDate));
    var failingDate_tz = new DateTimeOffset(failingDate, tz.GetUtcOffset(failingDate));

    var actual_workingDate_tz = GetNextDay_LooksRight(workingDate_tz);
    var actual_failingDate_tz = GetNextDay_LooksRight(failingDate_tz);

    var expected_workingDate = new DateTime(2009, 11, 3, 0, 0, 0);
    var expected_failingDate = new DateTime(2009, 11, 2, 0, 0, 0);

    var expected_workingDate_tz = new DateTimeOffset(expected_workingDate, tz.GetUtcOffset(expected_workingDate));
    var expected_failingDate_tz = new DateTimeOffset(expected_failingDate, tz.GetUtcOffset(expected_failingDate));

    Assert.AreEqual(expected_workingDate_tz, actual_workingDate_tz, "Should have found the following day's midnight");
    Assert.AreEqual(expected_failingDate_tz, actual_failingDate_tz, "Failing date does not have the correct offset for it's DST");
}

[TestMethod]
public void GetNextDay_LooksRight_FAILTest()
{
    // Here is where the frustrating part is... aparantly the "magic" that DateTimeOffset provides only works for your systems Local Time Zone...

    // { To properly fail this cannot match your system's configured Local Time Zone, I'm in PST so I use EST }
    var tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");

    var workingDate = new DateTime(2009, 11, 2, 0, 0, 0);
    var failingDate = new DateTime(2009, 11, 1, 0, 0, 0);

    var workingDate_tz = new DateTimeOffset(workingDate, tz.GetUtcOffset(workingDate));
    var failingDate_tz = new DateTimeOffset(failingDate, tz.GetUtcOffset(failingDate));

    var actual_workingDate_tz = GetNextDay_LooksRight(workingDate_tz);
    var actual_failingDate_tz = GetNextDay_LooksRight(failingDate_tz);

    var expected_workingDate = new DateTime(2009, 11, 3, 0, 0, 0);
    var expected_failingDate = new DateTime(2009, 11, 2, 0, 0, 0);

    var expected_workingDate_tz = new DateTimeOffset(expected_workingDate, tz.GetUtcOffset(expected_workingDate));
    var expected_failingDate_tz = new DateTimeOffset(expected_failingDate, tz.GetUtcOffset(expected_failingDate));

    Assert.AreEqual(expected_workingDate_tz, actual_workingDate_tz, "Should have found the following day's midnight");
    Assert.AreEqual(expected_failingDate_tz, actual_failingDate_tz, "Failing date does not have the correct offset for it's DST");
}

【问题讨论】:

  • 你无法完成这项工作。使用 UTC。
  • 我不能使用 UTC,因为例如,当表示诸如星期几(允许选择 1-N 天)之类的重复模式时,我需要能够在下一次解决重复发生的位置,并且从发出查询的用户的角度来看,它必须是有效的。鉴于模式 MTWTh,它可能是 UTC 的星期五,而仍然是星期四 PST,如果纯粹使用 UTC 计算将返回 {date} 00:00:00.000 -00:00 当它需要是 {date} 00:00:00.000 -08 :00 休息 8 小时。
  • 顺便说一句,我希望有一个更优雅的解决方案,但与此同时,我正在研究一个包装类,它同时跟踪 DateTimeOffset 和创建它的 TimeZoneInfo.Id 以便我将具有正确生成新 DateTimeOffset 所需的信息,但它 a) 似乎重复了 DateTimeOffset 类的值,并且 b) 使 API 变得不那么友好(也许我可以添加一些帮助程序/扩展方法来清理它。 ..)

标签: offset dst datetimeoffset timezone


【解决方案1】:

这种情况的根本问题是没有足够的信息来执行适当的时区转换。简单的偏移量不足以推断时区,因为对于特定时刻的给定偏移量,可能存在多个潜在的时区。因为 DateTimeOffset 对象不捕获和维护有关创建它的时区的信息,所以必须由该类外部的东西来维护这种关系。

让我失去嗅觉的是调用 ToLocalTime(),我后来意识到它实际上是在计算中引入了一个隐含的 TimeZoneInfo,即为机器配置的本地时间,我相信内部的 DateTimeOffset 必须是通过简单地删除配置的偏移量然后使用构造函数创建一个新的 DateTimeOffset 类来转换为 UTC,该构造函数采用 DateTime [in UTC] 和 TimeZoneInfo [from local system] 以生成正确的 dst 感知结果日期。

鉴于此限制,我在 DateTimeOffset 类中不再看到任何值,而不是 DateTime 和 TimeZoneInfo 的同样准确和更有价值的组合。

【讨论】:

  • DateTimeOffset 中有值。见here。您在上一段中谈论的内容没有 .Net BCL 实现,但 NodaTime 将其称为 ZonedDateTime,它也具有价值 - 只是不同。
猜你喜欢
  • 2012-12-13
  • 2015-03-13
  • 2013-09-29
  • 2020-05-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-03-16
  • 1970-01-01
相关资源
最近更新 更多