Provided by Allen Browne, February 2008
To handle recurring events, you need a table of events and one of these approaches:
(A) is fully normalized - easy to implement and maintain. Change any aspect of the event, and all instances reflect the change.
This is the best solution for simple sequences, but it cannot remove specific instances from the series or reschedule them.
(B) lets you move or remove specific instances from the series. However, it cannot cope with events that have no termination date, since you must create a finite number of instances in the related table. Also, if the user alters the information in the original event, you have headaches trying to figure out which new instances to create, old instances to delete, or existing instances to reschedule when the user may have manually set them or entered comments they want.
This is the best solution for finite sequences where each instance must be handled individually, such as a schedule of repayments.
(C) combines most of the advantages of (A) and (B). This is the best solution where you occasionally need to modify the series of events.
This article takes the third approach. Download the sample database (45kb zipped, Access 2000 and later.)
In the sample database, tblEvent has these fields:
Field Name | Data Type | Description |
EventID | AutoNumber | primary key |
EventDescrip | Text | Event name/description |
EventStart | Date/Time | When the series of events start (first instance) |
RecurCount | Number | How many times it will recur. 0 for one-off. Blank if open-ended. |
PeriodFreq | Number | How many days/months/years between recurrences. Required. |
PeriodTypeID | Text | Type of period for recurrence (day, month, year, ...) Required. |
Comment | Text | Comment about this entry. |
PeriodTypeID will be a combo offering values that can be used in a DateAdd() expression: "d", "ww", "m", "q", or "yyyy". For example, if something occurs every calendar month, you enter 1 for PeriodFreq, and "m" for PeriodTypeID. If it happens every 30 days, you enter 30 for PeriodFreq, and "d" for PeriodTypeID.
The lookup table ltPeriodType contains the values for the combo.
To calculate each instance of an event, use a Cartesian Product query. Create a table with a Number field, and enter the values from zero to the highest number of instances you could need. In the sample database, tblCount has 1001 records (0 to 1000.)
qryEventCartesian uses tblCount and tblEvent, with no join between them. This gives every combination of the two, so 1001 records for every event. Add criteria under tblCount.CountID like this:
<=[tblEvent].[RecurCount]
This yields the desired number of records for each occurrence, with the count indicating which instance it is.
But the open-ended events no longer match because the RecurCount is Null. We want the entire count for these, so add this on a separate criteria line:
Is Null
This simple query achieves option (A) above, i.e. all occurrences calculated on the fly.
Now we need another table to handle the instances that are rescheduled to another date, or to be skipped. tblEventException has these fields:
Field Name | Data Type | Description |
EventID | Number | Relates to tblEvent.EventID. Required. |
InstanceID | Number | Instance number. Zero for original. Required. |
InstanceDate | Date/Time | Date this instance is rescheduled to. |
IsCanned | Number | Checked box indicates this instance is cancelled. |
InstanceComment | Text | Why this was cancelled/rescheduled. |
The primary key is EventID + InstanceID, so you cannot enter two exceptions for the same instance of an event.
To reschedule a particular instance to another date, enter the event, the instance number, and the new date. To cancel a particular instance, check the box in the IsCanned field.
qryEventDates then identifies the cancelled instances, and uses the special dates for the rescheduled instances. It uses the query above as an input "table", with an outer join to tblEventException.
The expression to calculate the EventDate involves nested IIf() functions. See the sample database for details.
The report rptEvent displays the results of this query. To limit it to a particular date range, you can easily add criteria on the EventDate field, or use the WhereCondition of OpenReport.
The form is bound to tblEvent, with a subform bound to qryEventDates to display the event occurrences:
Naturally the subform is read-only: most instances do not exist in any table. So, we provide an Edit button beside each instance where the user can enter, modify, or delete the records in tblEventException. The button opens a form in dialog mode, finding the record if it exists, or setting the Default Value for EditID and InstanceID so any new record the user adds will be for the correct row. When you close the dialog, it requeries the subform.
The sample database demonstrates the principles. Develop it further if you need to associate individuals with events and/or specific occurrences.
The recurrences are very flexible, but do not handle things like "the 4th Tuesday of each month."
You will need more reports, showing events in daily/weekly/monthly/annual layouts. MVP Duane Hookom has some examples of Calender Reports.
Question 1. Why did you use a Number field instead of a Yes/No field for tblEventException.IsCanned?
Answer: tblEventException is on the outer side of a join in qryEventDates. For the records that don't have an exception, this field will therefore be Null. JET does not cope when a Yes/No field becomes Null; Access may crash if you manipulate the field further. See Why I stopped using Yes/No fields.
Question 2. Won't this approach fail if I need more than 1000 recurrences of an event?
Answer: Add more records to tblCount if you need them. The beauty of this approach is that it does not rely on a finite number of related records created at the time you originally entered the event (as option (B) above does.) Just add the records to tblCount, and the additional instances appear.
Question 3. What's in the module ajbGlobal?
Answer: Just the error handler. The database contains minimal code. In a real database, I suggest you use an error logger.
Question 4. Will this work with Access 97
Answer: The concept and the queries should all work if you convert to an older version. Some features of the interface won't work, e.g. the conditional formatting on the manually entered dates (blue/bold dates in the subform shown above.)
Question 5. Why do you constantly misspell 'Canceled'?
Answer: We use the British spelling in my part of the world.
Question 6. How can I get the sample database?
Answer: Here 'tis.
Home | Index of tips | Top |