What is the best way to represent "Recurring Events" in database?

I am trying to develop a scheduler- and calendar-dependent event application in C#, for which a crucial requirement is to represent recurring events in the database. What is the best way to represent recurring events in a database?

More Details:

While creating the event I am also sending invites to the certain users and the invitees should be allowed to login to the meeting only during the specified window(meeting duration) or may be decline the login when the invitee attempts to login say, 5 minutes before the scheduled start of the meeting.


Solution 1:

The sysjobs, sysjobsschedule and sysschedules tables in SQL Server does a pretty good job of this. I wouldn't reinvent the wheel, I'd just copy their design.

Here are some of the important fields from sysschedules

freq_type

How frequently a job runs for this schedule.

1 = One time only

4 = Daily

8 = Weekly

16 = Monthly

32 = Monthly, relative to freq_interval

64 = Runs when the SQL Server Agent service starts

128 = Runs when the computer is idle

freq_interval

Days that the job is executed. Depends on the value of freq_type. The default value is 0, which indicates that freq_interval is unused. Value of freq_type Effect on freq_interval

1 (once) freq_interval is unused (0)

4 (daily) Every freq_interval days

8 (weekly) freq_interval is one or more of the following: 1 = Sunday 2 = Monday 4 = Tuesday 8 = Wednesday 16 = Thursday 32 = Friday 64 = Saturday

16 (monthly) On the freq_interval day of the month

32 (monthly, relative) freq_interval is one of the following: 1 = Sunday 2 = Monday 3 = Tuesday 4 = Wednesday 5 = Thursday 6 = Friday 7 = Saturday 8 = Day 9 = Weekday 10 = Weekend day

64 (starts when SQL Server Agent service starts) freq_interval is unused (0)

128 (runs when computer is idle) freq_interval is unused (0)

freq_subday_type

Units for the freq_subday_interval. Can be one of the following values: Value Description (unit)

1 At the specified time

2 Seconds

4 Minutes

8 Hours

freq_subday_interval

Number of freq_subday_type periods to occur between each execution of the job.

freq_relative_interval

When freq_interval occurs in each month, if freq_interval is 32 (monthly relative). Can be one of the following values:

0 = freq_relative_interval is unused

1 = First

2 = Second

4 = Third

8 = Fourth

16 = Last

freq_recurrence_factor

Number of weeks or months between the scheduled execution of a job. freq_recurrence_factor is used only if freq_type is 8, 16, or 32. If this column contains 0, freq_recurrence_factor is unused.

Solution 2:

Well, to store the recurrence rule itself, you could use a cut down version of RFC 5545 (and I really suggest you cut it down heavily). Aside from anything else, that will make it easy to export into other applications should you wish to.

After you've made that decision, for the database side you need to work out whether you want to store each occurrence of the event, or just one record for the repeated event, expanding it as and when you need to. Obviously it's considerably easier to query the database when you've already got everything expanded - but it makes it trickier to maintain.

Unless you fancy writing some pretty complex SQL which may be hard to test (and you'll want a lot of unit tests for all kinds of corner cases) I would suggest that you make the database itself relatively "dumb" and write most of the business logic in a language like Java or C# - either of which may be embeddable within stored procedures depending on your database, of course.

Another thing you need to ask yourself is whether you need to cope with exceptions to events - one event in a series changing time/location etc.

I have some experience with calendaring (I've spent most of the last year working on the calendar bit of Google Sync via ActiveSync) and I should warn you that things get complicated really quickly. Anything you can deem "out of scope" is a blessing. In particular, do you need to work in multiple time zones?

Oh, and finally - be very, very careful when you're doing actual arithmetic with calendar operations. If you're going to use Java, please use Joda Time rather than the built-in Calendar/Date classes. They'll help you a lot.

Solution 3:

The accepted answer here is too convoluted. For example, if an event occurs every 5 days, the 5 is stored in freq_interval, but if it occurs every 5 weeks, the 5 is stored in freq_recurrence. The biggest problem is that freq_interval means three different things depending on the value of freq_type (number of days between occurrences for daily recurrence, day of the month for monthly recurrence, or days of the week for weekly or monthly-relative). Also, the 1,2,4,8... type sequence is used when it is unnecessary and less than helpful. For example, freq_relative_interval can only be "one of" the possible values. This lines up with a drop-down box or radio button type input, not a checkbox type input where multiple choices can be selected. For coding, and for human readability, this sequence gets in the way and just using 1,2,3,4... is simpler, more efficient, more appropriate. Finally, most calendar applications don't need subday intervals (events occurring multiple times in a day - every so many seconds, minutes, or hours).
But, having said this, that answer did help me refine my thoughts on how I am doing this. After mix and matching it with other articles and going from what I see in the Outlook calendar interface and a few other sources, I come up with this:

recurs
0=no recurrence
1=daily
2=weekly
3=monthly

recurs_interval
this is how many of the periods between recurrences. If the event recurs every 5 days, this will have a 5 and recurs will have 1. If the event recurs every 2 weeks, this will have a 2 and recurs will have a 2.

recurs_day
If the user selected monthly type recurrence, on a given day of the month (ex: 10th or the 14th). This has that date. The value is 0 if the user did not select monthly or specific day of month recurrence. The value is 1 to 31 otherwise.

recurs_ordinal
if the user selected a monthly type recurrence, but an ordinal type of day (ex: first monday, second thursday, last friday). This will have that ordinal number. The value is 0 if the user did not select this type of recurrence.
1=first
2=second
3=third
4=fourth
5=last

recurs_weekdays
for weekly and monthly-ordinal recurrence this stores the weekdays where the recurrence happens. 1=Sunday
2=Monday
4=Tuesday
8=Wednesday
16=Thursday
32=Friday
64=Saturday

So, examples:
So, every 4 weeks on Saturday and Sunday would be

  • recurs = 2 ==> weekly recurrence
  • recurs_interval = 4 ==> every 4 weeks
  • recurs_weekdays = 65 ==> (Saturday=64 + Sunday=1)
  • recurs_day and recurs_ordinal = 0 ==> not used

Similarly, Every 6 months on the first Friday of the month would be

  • recurs = 3 ==> monthly recurrence
  • recurs_interval = 6 ==> every 6 months
  • recurs_ordinal = 1 ==> on the first occurrence
  • recurs_weekdays = 32 ==> of Friday

None of this business of having a field that means three entirely different things depending on the value of another field.

On the user interface side of things, I let the user specify a date, start time, end time. They can then specify if they want a type of recurrence other than none. If so, the app expands the relevant section of the web-page to give the user the options required for the stuff above, looking a lot like the Outlook options, except there is no "every weekday" under daily recurrence (that is redundant with weekly recurrence on every mon-fri), and there is no yearly recurrence. If there is recurrence then I also require the user to specify an end-date that is within one year of today (the users want it that way, and it simplifies my code) - I don't do unending recurrence or "end after ## occurrences."

I store these fields with the user selections in my event table, and expand that out in a schedule table which has all occurrences. This facilitates collision detection (I am actually doing a facility reservation application) and editing of individual occurrences or refactoring of future occurrences.

My users are all in CST, and I thank the good Lord for that. It is a helpful simplification for now, and if in the future the user base is going to expand beyond that, then I can figure out how to deal with it then, as a well separated task.

UPDATE Since I first wrote this, I did add daily occurrence with "Every weekday". Our users had a bit of a hard time with thinking that you could use Weekly recurrence for events happening from Thursday one week to Tuesday the next week and only on weekdays. It was more intuitive for them to have this, even if there was already another way that they could do it.