来自通用或本地 DateTime 的 adding/subtracting 的最佳实践

Best practice for adding/subtracting from universal or local DateTime

我试图在 DateTime 周围添加一个包装器以包含时区信息。这是我目前拥有的:

public struct DateTimeWithZone {
    private readonly DateTime _utcDateTime;
    private readonly TimeZoneInfo _timeZone;

    public DateTimeWithZone(DateTime dateTime, TimeZoneInfo timeZone) {
        _utcDateTime = TimeZoneInfo.ConvertTimeToUtc(DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified), timeZone);
        _timeZone = timeZone;
    }

    public DateTime UniversalTime { get { return _utcDateTime; } }

    public TimeZoneInfo TimeZone { get { return _timeZone; } }

    public DateTime LocalTime { get { return TimeZoneInfo.ConvertTimeFromUtc(_utcDateTime, _timeZone); } }

    public DateTimeWithZone AddDays(int numDays) {
        return new DateTimeWithZone(TimeZoneInfo.ConvertTimeFromUtc(UniversalTime.AddDays(numDays), _timeZone), _timeZone);
    }

    public DateTimeWithZone AddDaysToLocal(int numDays) {
        return new DateTimeWithZone(LocalTime.AddDays(numDays), _timeZone);
    }
}

这改编自@Jon Skeet 在之前的问题中提供的答案。

由于夏令时的问题,我正在与 adding/subtracting 时间作斗争。根据以下内容,最佳做法是 add/subtract 世界时间:

https://msdn.microsoft.com/en-us/library/ms973825.aspx#datetime_topic3b

我遇到的问题是,如果我说:

var timeZone = TimeZoneInfo.FindSystemTimeZoneById("Romance Standard Time");            
var date = new DateTimeWithZone(new DateTime(2003, 10, 26, 00, 00, 00), timeZone);
date.AddDays(1).LocalTime.ToString();

这将 return 26/10/2003 23:00:00。正如您所看到的,当地时间已经失去了一个小时(由于夏令时结束)所以如果我要显示它,它会说它与刚刚添加一天的那一天是同一天。但是,如果我要说:

date.AddDaysToLocal(1).LocalTime.ToString();

我会在 27/10/2003 00:00:00 回来,时间会保留。这对我来说看起来是正确的,但它违背了添加到世界时的最佳实践。

如果有人可以帮助阐明执行此操作的正确方法,我将不胜感激。请注意,我已经查看了 Noda Time,目前要转换成它需要太多工作,而且我希望更好地理解这个问题。

这两种方式都是正确的(或不正确的)取决于你需要做什么。

我喜欢将这些视为不同类型的计算:

  1. 时间顺序计算。

  2. 日历计算。

按时间顺序 计算涉及以物理时间为单位的时间算术。例如添加秒、纳秒、小时或天。

日历 计算涉及以人类认为方便的单位进行的时间算术,但物理时间的长度并不总是相同。例如,添加月份或年份(每个月份的天数不同)。

当你想添加一个不一定有固定秒数的粗略单位时,日历计算很方便,但你仍然想在日期中保留更精细的字段单位,例如天,时、分、秒。

在本地时间计算中,您添加了一天,并且假定日历计算是您想要的,您保留了一天中的本地时间,尽管在本地日历中 1 天并不总是 24 小时。请注意,本地时间的算术有可能导致本地时间具有 two 映射到 UTC,甚至 zero 映射到 UTC。因此,您的代码应该构造成您知道这永远不会发生,或者能够检测到它何时发生并以适合您的应用程序的任何方式做出反应(例如,消除歧义映射)。

在您的 UTC 时间计算(按时间顺序计算)中,您总是 添加 86400 秒,并且本地日历可以做出反应,但是它可能由于 UTC 偏移变化(与夏令时相关)或其他)。 UTC 偏移量变化可能大到 24 小时,因此添加一个按时间顺序排列的日期可能甚至不会使该月的本地日历日增加一个。按时间顺序计算的结果始终具有唯一的 UTC <-> 本地映射(假设输入具有唯一的映射)。

两种计算都有用。两者都是通常需要的。知道你需要什么,并且知道如何使用 API 来计算你需要的任何一个。

只是为了补充霍华德的好答案,请理解您所指的 "best practice" 是关于增加 elapsed 时间。事实上,如果你想增加 24 小时,你会在 UTC 中这样做,你会发现你最终会在 23:00 因为那天有一个额外的小时。

我通常考虑将一天添加为日历计算(使用霍华德的术语),因此那天有多少小时并不重要 - 您可以在当地时间增加一天。

然后您必须验证结果是否是当天的有效时间,因为它很可能使您在向前转换的 "gap" 中到达无效值。你必须决定如何调整。同样,当您转换为 UTC 时,您应该测试模糊时间并相应地进行调整。

了解如果不自行进行任何调整,您将依赖 TimeZoneInfo 方法的默认行为,该方法会在不明确的时间内调整 backward (尽管通常期望的行为是调整 forward),并且 ConvertTimeFromUtc 会在无效时间内抛出异常。

这就是为什么ZonedDateTime在Noda Time中有"resolvers"的概念,让你更具体地控制这个行为。您的代码缺少任何类似的概念。

我还要补充一点,虽然你说你看过 Noda Time 并且转换成它的工作量太大 - 我鼓励你再看一遍。人们不一定需要改造他们的整个应用程序才能使用它。你可以,但你也可以只在需要的地方引入它。例如,您可能想在此 DateTimeWithZone class 内部使用它,以迫使您走上正确的道路。

还有一件事 - 当您在输入中使用 SpecifyKind 时,您基本上是在说忽略任何输入类型。由于您正在设计用于重用的通用代码,因此您会招致潜在的错误。例如,我可能会传入 DateTime.UtcNow,您会假设它是基于时区的时间。 Noda Time 通过使用单独的类型而不是 "kind" 来避免这个问题。如果您打算继续使用 DateTime,那么您应该评估类型以应用适当的操作。只是忽略它肯定会给你带来麻烦。