从通用或本地 DateTime 中添加/减去的最佳实践

Posted

技术标签:

【中文标题】从通用或本地 DateTime 中添加/减去的最佳实践【英文标题】:Best practice for adding/subtracting from universal or local DateTime 【发布时间】:2017-12-15 07:47:48 【问题描述】:

我正在尝试在 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 在较早的问题中提供的答案。

由于夏令时问题,我正在努力加/减时间。根据以下最佳做法是添加/减去通用时间:

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

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

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

我会在 27/10/2003 00:00:00 回来,时间会保留。这在我看来是正确的,但它违反了添加到通用时间的最佳做法。

如果有人可以帮助澄清执行此操作的正确方法,我将不胜感激。请注意,我查看了 Noda Time,目前要转换成它需要做很多工作,我也想更好地理解这个问题。

【问题讨论】:

【参考方案1】:

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

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

    时序计算。

    历法计算。

时序 计算涉及以相对于物理时间有规律的单位的时间算术。例如添加秒、纳秒、小时或天。

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

当您想要添加一个不一定具有固定秒数的粗略单位,但您仍想在日期中保留更精细的字段单位(例如天、小时、分和秒。

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

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

这两种计算都很有用。两者都是常用的。知道您需要什么,并知道如何使用 API 来计算您需要的任何内容。

【讨论】:

谢谢,我已经修改了我的代码,将天、月和年添加到本地时间(日历计算)和秒、分钟和小时到通用时间(时间计算)。我现在很欣赏两者之间的区别,但我希望我的代码能够在不深入了解其背后的所有复杂性的情况下工作。【参考方案2】:

只是为了补充霍华德的好答案,请理解您所指的“最佳实践”是关于按经过时间递增。实际上,如果您想添加 24 小时,您可以在 UTC 中添加,您会发现由于当天多出一个小时,您会在 23:00 结束。

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

然后您必须验证结果是否是当天的有效时间,因为它很可能使您在前向转换的“间隙”中处于无效值。你必须决定如何调整。同样,当您转换为 UTC 时,您应该测试不明确的时间并进行相应的调整。

请理解,如果您不自行进行任何调整,您将依赖于 TimeZoneInfo 方法的默认行为,该方法会在不明确的时间向后调整(即使通常需要的行为是调整forward),那ConvertTimeFromUtc会在无效时间抛出异常。

这就是为什么野田时代的ZonedDateTime 有“解析器”的概念来让你更具体地控制这种行为的原因。您的代码缺少任何类似的概念。

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

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

【讨论】:

感谢您提供的信息。我已经添加了案例来处理您提到的场景。

以上是关于从通用或本地 DateTime 中添加/减去的最佳实践的主要内容,如果未能解决你的问题,请参考以下文章

为啥减去两个本地 DateTime 值似乎不能说明夏令时?

从日期时间减去 1 年

如何从日期中减去一天?

在飞镖中添加/减去迄今为止的月/年?

C# DateTime 每个月的最后 1 天 [关闭]

DateTime 的通用端点还是三个不同的端点?