在C#中计算重复日期的正确方法

在我的项目中,我需要计算重复事件的日期。 一开始我只有一个开始日期/时间以及该事件必须重复的信息:

Every Day Every Week Every 2 Weeks Every 3 Weeks Every Month Every 2 Months ... 

这样做的正确方法是什么? 它应该可以正常使用不同的时区和日节省时间设置。 我想我应该将日/周/月添加到本地DateTime,然后将其转换为UTC。 但我不确定这一点。 如果我添加几天会发生什么,这将是我们需要向前调整我们的时钟一小时的时间。 在这种情况下,此时间不存在。

下面是我写的代码,但我不确定它在每种情况下都能正常工作:

 private static List FindOccurrences(DateTime localStart, Repeat repeat, int occurrences) { var result = new List { localStart }; switch (repeat) { case Repeat.Daily: for (int i = 1; i <= occurrences; i++) result.Add(localStart.AddDays(i)); break; case Repeat.Every2Weeks: for (int i = 1; i <= occurrences; i++) result.Add(localStart.AddDays((7 * 2) * i)); break; ... } return result; } public List CreateRepeating(string timeZone, Repeat repeat, int repeatEnds, DateTime localStart, int eventDuration) { var events = new List(); var occurrences = FindOccurrences(localStart, repeat, repeatEnds); foreach (var occurrence in occurrences) { var item = new Event(); item.Start = occurrence.ToUtcTime(timeZone); item.End = occurrence.ToUtcTime(timeZone).AddMinutes(eventDuration); events.Add(item); } return events; } 

PS:所有日期都以UTC格式存储在数据库中。

安排或计算未来事件的日期,尤其是重复发生的事件,是一个非常复杂的主题。 虽然从其他语言的角度来看,我已经写过几次这样的文章(参见: 1,2,3,4 )。

我担心这个主题过于宽泛,无法为您提供准确的代码。 详细信息将特定于您的应用程序。 但这里有一些提示。

一般来说:

  • 仅将UTC用于发生单个事件实例的预计时刻。

  • 将实际活动存储在当地时间 。 同时存储时区ID。

  • 存储时区偏移量 。 应该单独查找每次出现的情况。

  • 将事件即将发生的事件计划为UTC,以便您知道何时根据事件执行操作(或者对您的场景有意义的事情)。

  • 决定夏令时,当事件发生在弹跳间隙或后退重叠时要做什么。 您的需求可能会有所不同,但一个常见的策略是跳过spring的差距,并选择秋季的第一次出现。 如果您不确定我的意思,请参阅dst标签wiki 。

  • 仔细考虑如何处理月末附近的日期。 并非所有月份都具有相同的天数,并且日历数学很难。 dt.AddMonths(1).AddMonths(1)不一定与dt.AddMonths(2)相同。

  • 随时掌握时区数据更新。 没人能预测未来,世界各国政府都喜欢改变一切!

  • 您需要保留计划的原始本地时间值,以便可以重新设置事件的UTC值。 您应该定期或在应用时区更新时(如果您手动跟踪它们)执行此操作。 时区标签wiki包含有关不同时区数据库及其更新方式的详细信息。

  • 考虑使用Noda Time和IANA / TZDB时区。 与Microsoft提供的内置类型和时区相比,它们更适合此类工作。

  • 小心避免使用本地时区。 您应该没有调用DateTime.NowToLocalTimeToUniversalTime 。 请记住,本地时区基于运行代码的计算机,并且不会影响代码的行为。 阅读The Case Against DateTime.Now中的更多内容 。

  • 如果您正在完成所有这些工作,那么您应该看一下预先解决的解决方案,例如Quartz.NET 。 它是免费的,开源的,function强大的,涵盖了许多您可能没有想过的边缘情况。

部分答案。 我宁愿将实现更改为

  public enum TimePeriod { None = 0, Day = 1, // Just week, no "two weeks, three weeks etc." Week = 2, Month = 3 } public static class Occurrencies { // May be you want to convert it to method extension: "this DateTime from" public static IEnumerable FindInterval(DateTime from, TimePeriod period, int count) { while (true) { switch (period) { case TimePeriod.Day: from = from.AddDays(count); break; case TimePeriod.Week: from = from.AddDays(7 * count); break; case TimePeriod.Month: from = from.AddMonths(count); break; } yield return from; } } } 

使用:

  // Get 5 2-week intervals List intervals2Weeks = Occurrencies .FindInterval(DateTime.Now, TimePeriod.Week, 2) .Take(5) .ToList(); // Get 11 3-month intervals List intervals3Months = Occurrencies .FindInterval(DateTime.Now, TimePeriod.Month, 3) .Take(11) .ToList();