3

I checked several older questions regarding this topic like this one: Calendar Recurring/Repeating Events - Best Storage Method however, the answers are pretty bad performance-wise and cumbersome in implementation. From another answer, it's easy to tell why the accepted answer is a bad idea: a month of events takes 90 queries. Which is unacceptable.

Anyways, here's the issue I'm facing so that you don't have re-read those questions:

  1. Storing events in such a way to allow them to recur. Think Google Calendar where you can specify patterns like "happens on the 1st of the month, every month" or "happens every 2nd monday of the month" (the latter is less important to me.
  2. Querying a list of events for a time period. For example, I want to show someone a list of events for the next 2 months and I don't want to query for every single day of the month. Doing that would just kill the server (per user among thousands of rows of events)
  3. DB agnostic. I use PgSQL and saw many answers for this question on other forums using either MS SQL-specific stuff or Oracle.

I'd appreciate any help! I read a couple of papers on the topic and still can't find something I can make work in SQL specifically.

Community
  • 1
  • 1
antjanus
  • 987
  • 3
  • 15
  • 30
  • possible duplicate of [Calendar Recurring/Repeating Events - Best Storage Method](http://stackoverflow.com/questions/5183630/calendar-recurring-repeating-events-best-storage-method) – Neil McGuigan Jul 14 '15 at 18:54

1 Answers1

1

The solution I have come up with is that I have an event table that has five fields defining the recurrence of the event, as defined below. I then also have a schedule table which I populate with the actual occurrence of the events. I do require an end date, and even when they specify something to go out to a couple years out, it is a monthly type event which does not create that many entries into the schedule table.

So, the event is stored in an event table, with a startDateTime and an endDateTime that describe the entire duration of the event if there is no recurrence. These two datetime fields also define the overall start and end of the event if it is a recurring event. In that same event table, we have the five fields defining recurrence, as laid out below.

The Schedule table stores individual occurrences of each event. So it has an eventId, startDateTime, and endDateTime. This start and end refer only to each occurrence, not the overall span.

For querying for all the scheduled occurrences happening in a period of time, I just query the schedule table checking for any occurrences that match this condition:

select * from schedule where schedule.startDateTime < @queryPeriodEnd and schedule.endDateTime > @queryPeriodStart

This query gives me only the schedule entries that happen partially or wholly within my query period. For getting the event data, it's a simple matter of joining to the event table.

The interesting part is calculating something like the second thursday of the month. That happens in the actual code for figuring out all the scheduled occurrences for a given event. I am also enclosing my code for that below.

EVENT RECURRENCE FIELDS

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, recurs_interval will have a 5 and recurs will have 1. If the event recurs every 3 weeks, recurs_interval will have a 3 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, every 4 weeks on Saturday and Sunday would be recurs=2, recurs_interval=4, recurs_weekdays=65 (64 + 1) Similarly, Every three months on the first Friday of the month would be recurs=3, recurs_interval=3, recurs_ordinal=1, recurs_weekdays=32

CODE

        thisEvent.occurrences = new List<ScheduleInstance>();
        DateTime currentDateTime = (DateTime) thisEvent.start;
        DateTime currentEndTime;
        BitArray WeekDayRecurrenceBits = new BitArray(new Byte[] {(Byte) thisEvent.recursWeekdays});

        while (currentDateTime < thisEvent.end)
        {
            currentEndTime = new DateTime(currentDateTime.Year, currentDateTime.Month, currentDateTime.Day,
                                          thisEvent.end.Value.Hour, thisEvent.end.Value.Minute, thisEvent.end.Value.Second);
            switch (thisEvent.recurs)
            {
                case (RecurrenceTypeEnum.None):
                    AddOccurrenceToRooms(thisEvent, currentDateTime, currentEndTime);
                    currentDateTime = (DateTime)thisEvent.end;
                    break;
                case (RecurrenceTypeEnum.Daily):
                    AddOccurrenceToRooms(thisEvent, currentDateTime, currentEndTime);
                    currentDateTime = currentDateTime.AddDays(thisEvent.recursInterval);
                    break;
                case (RecurrenceTypeEnum.Weekly):
                    int indexIntoCurrentWeek = (int) currentDateTime.DayOfWeek;
                    while ((indexIntoCurrentWeek < 7) && (currentDateTime < thisEvent.end))
                    {
                        if (WeekDayRecurrenceBits[(int) currentDateTime.DayOfWeek])
                        {
                            AddOccurrenceToRooms(thisEvent, currentDateTime, currentEndTime);
                        }
                        currentDateTime = currentDateTime.AddDays(1);
                        currentEndTime = currentEndTime.AddDays(1);
                        indexIntoCurrentWeek++;
                    }
                    currentDateTime = currentDateTime.AddDays(7 * (thisEvent.recursInterval - 1));
                    break;
                case (RecurrenceTypeEnum.Monthly):
                    if (thisEvent.recursDay == 0)
                    {
                        DateTime FirstOfTheMonth = new DateTime(currentDateTime.Year, currentDateTime.Month, 1);
                        int daysToScheduleOccurrence = ((thisEvent.recursWeekdays - (int)FirstOfTheMonth.DayOfWeek + 7) % 7)
                                                       + ((thisEvent.recursOrdinal - 1) * 7)
                                                       - currentDateTime.Day + 1;
                        if (daysToScheduleOccurrence >= 0)
                        {
                            currentDateTime = currentDateTime.AddDays(daysToScheduleOccurrence);
                            currentEndTime = currentEndTime.AddDays(daysToScheduleOccurrence);
                            if (currentDateTime < thisEvent.end)
                            {
                                AddOccurrenceToRooms(thisEvent, currentDateTime, currentEndTime);
                            }
                        }
                    }
                    else
                    {
                        if (currentDateTime.Day <= thisEvent.recursDay && thisEvent.recursDay <= DateTime.DaysInMonth(currentDateTime.Year, currentDateTime.Month) )
                        {
                            currentDateTime = currentDateTime.AddDays(thisEvent.recursDay - currentDateTime.Day);
                            currentEndTime = currentEndTime.AddDays(thisEvent.recursDay - currentEndTime.Day);
                            AddOccurrenceToRooms(thisEvent, currentDateTime, currentEndTime);
                        }
                    }
                    currentDateTime = currentDateTime.AddDays((currentDateTime.Day - 1) * -1).AddMonths(thisEvent.recursInterval);
                    break;
                default:
                    break;
            }
        }
  • I understand your programming implementation and data storage but how do you handle querying the data? Do you just get all of the records in the DB and then sort through it? – antjanus Jul 13 '15 at 19:54
  • Because the schedule table has a startDateTme and endDateTime for each occurrence, I just do a query on that table to give me all the events such that schedule.startDateTime < period.endDateTime and schedule.endDateTime > period.startDateTime (no,the cross-matching is not a mistake, it is intended). That gives me all the events that have a scheduled occurrence that falls partially or wholly within a particular period, in one query. –  Jul 13 '15 at 19:59
  • Ahh! Interesting. So basically you do the best you can with SQL to get the approximate range of stuff and then you post-process it. Question though, how do you deal with events that have no endDate? For instance, "pay phone bill" which has, relatively speaking, no known end date. – antjanus Jul 13 '15 at 20:07
  • No, I don't do the best I can with SQL and then post process it. That query gets me exactly what I want, all the events and only the events occurring within that period of time. And I have an easy way to deal with unending events - I don't allow them. The user must enter an end-date :-) –  Jul 13 '15 at 20:10
  • That doesn't make sense to me. Let's say you have a weekly occurring event on a monday which starts today and ends next year. And you want to figure out what happens between Tuesday and Friday, wouldn't your SQL query catch this weekly event and cause a false positive then? If not, can you post a sample query with something similar? – antjanus Jul 13 '15 at 20:13
  • If I was going to deal with unending events, I would have a last date processed stored in the database. Then on any query to the database, if a date greater than that "lastdate" was used, I would then have to go and expand the unending events up to the date used in the query, before I could satisfy the query. This could be done by user if their data is only accessible to that user, or in my case I would have to do a global lastdate because if anyone starts querying or trying to schedule a room for five years from now, first I would have to expand all unending events that far. –  Jul 13 '15 at 20:19
  • If an event happens every monday, and I query for between tuesday and friday, this is what happens. This monday's occurrence starts and ends this monday, it's enddatetime is less than my period's start time, so it does not fall between tuesday and friday, so it is not a result. Next monday's occurrence starts and ends next monday, its startDateTime is greater than my period's end time, so again, it does not fall between this tuesday and friday. –  Jul 13 '15 at 20:23
  • The important part is that while the event has a stat and end time that span the whole year, each occurrence (schedule table) only has a star time and end time for that occurrence. The Event's startime is July 15, 2-15 3pm; The event's endtime is July 15, 2016 5pm, a year from now. It is a weekly event every week. But this week's occurrence, in the schedule table. has a startDateTime of July 15, 2015 3pm and an endDateTime of July 15, 2015 5pm. –  Jul 13 '15 at 20:23
  • @antjanus - I added some explanation to my answer. if this still does not make sense, let me know. It will only help me clarify my answer. –  Jul 13 '15 at 22:03
  • I understand the query but I don't understand how you'd only get the data you want from it. Unless you're populating all of the recurrences as separate entries? – antjanus Jul 14 '15 at 13:09
  • @antjanus - exactly. In the event table, there is only one entry for the whole thing, with the overall start and end dates. But in the schedule table there is a separate entry, a row, for each scheduled occurrence of the event. There might be one schedule row for a particular event, if it is not a recurring event, or there might be 20 schedule rows for an event row that specifies recurrence every week for twenty weeks. –  Jul 14 '15 at 13:33
  • Awesome, got it! Thanks. – antjanus Jul 14 '15 at 13:47