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

时间:2014-10-14 14:03:36

标签: c# datetime timezone

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

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

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

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

private static List<DateTime> FindOccurrences(DateTime localStart, Repeat repeat, int occurrences)
{
    var result = new List<DateTime> { 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<Event> CreateRepeating(string timeZone, Repeat repeat, int repeatEnds, DateTime localStart, int eventDuration)
{
    var events = new List<Event>();
    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格式存储在数据库中。

2 个答案:

答案 0 :(得分:11)

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

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

一般来说:

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

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

  • 存储时区 offset 。应该单独查找每个事件。

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

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

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

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

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

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

  • 小心避免使用本地时区。您不应该致电DateTime.NowToLocalTimeToUniversalTime。请记住,本地时区基于运行代码的计算机,不应影响代码的行为。请阅读The Case Against DateTime.Now

  • 如果您正在执行所有这些操作以启动预定作业,您应该查看预先解决的解决方案,例如Quartz.NET。它是免费的,开源的,功能强大的,涵盖了许多您可能没有想过的边缘情况。

答案 1 :(得分:6)

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

  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<DateTime> 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<DateTime> intervals2Weeks = Occurrencies
    .FindInterval(DateTime.Now, TimePeriod.Week, 2)
    .Take(5)
    .ToList();

  // Get 11 3-month intervals 
  List<DateTime> intervals3Months = Occurrencies
    .FindInterval(DateTime.Now, TimePeriod.Month, 3)
    .Take(11)
    .ToList();