Correct way to calculate recurring dates in C#

Scheduling or calculating the dates of future events, especially recurring events, is a very complex subject. I’ve written about this a few times, though from the perspective of other languages (See: 1, 2, 3, 4).

I’m afraid this is too broad of a subject to give you the exact code to run. The details will be very specific to your application. But here are some tips.

In general:

  • Use UTC only for the projected moment in time that a single instance of the event is to occur.

  • Store the actual event in local time. Store the time zone id also.

  • Do not store the time zone offset. That should be looked up for each occurrence individually.

  • Project upcoming occurrence(s) of the event as UTC so you know how when to perform an action based on the event (or whatever makes sense for your scenario).

  • Decide what to do for daylight saving time, when an occurrence falls into a spring-forward gap, or a fall-back overlap. Your needs may vary, but a common strategy is to jump ahead of the spring gap, and choose the first occurrence in the fall. If you’re not sure what I mean, refer to the dst tag wiki.

  • Think carefully about how to handle dates near the end of a month. Not all months have the same number of days, and calendar math is difficult. dt.AddMonths(1).AddMonths(1) is not necessarily the same as dt.AddMonths(2).

  • Stay on top of time zone data updates. Nobody can predict the future, and the governments of the world like to change things!

  • You need to retain the original local-time values of the schedule, so that you can re-project the UTC values of the occurrences. You should do this either periodically, or whenever you apply a time zone update (if you’re tracking them manually). The timezone tag wiki has details about the different time zone databases and how they are updated.

  • Consider using Noda Time and IANA/TZDB time zones. They are much more suited for this type of work than the built in types and time zones Microsoft provides.

  • Be careful to avoid using the local time zone. You should have no calls to DateTime.Now, ToLocalTime, or ToUniversalTime. Remember, the local time zone is based on the machine where the code is running, and that should not impact the behavior of your code. Read more in The Case Against DateTime.Now.

  • If you are doing all of this to just kick off a scheduled job, you should probably take a look at a pre-canned solution, such as Quartz.NET. It is free, open source, highly functional, and covers a lot of edge cases you may not have thought about.

Leave a Comment