329

I am building a custom events system, and if you have a repeating event that looks like this:

Event A repeats every 4 days starting on March 3, 2011

or

Event B repeats every 2 weeks on Tuesday starting on March 1, 2011

How can I store that in a Database in a way that would make it simple to lookup. I don't want performance issues if there are a large number of events, and I have to go through each and every one when rendering the calendar.

Brandon Wamboldt
  • 15,241
  • 11
  • 49
  • 82
  • Can you explain why `1299132000 ` is hardcoded ? What this will do if i need to get the occurance dates and user for the given end date? – Murali Murugesan Nov 29 '13 at 14:38
  • @Murali Boy this is old, but I'm pretty sure 1299132000 is supposed to be the current date. – Brandon Wamboldt Nov 29 '13 at 14:57
  • @BrandonWamboldt, I tried your idea with SQL Server. http://stackoverflow.com/questions/20286332/display-next-event-date. I want to find all the next items like [c# version](http://stackoverflow.com/questions/20288508/find-all-slots-in-a-given-range) – Billa Nov 29 '13 at 15:23

12 Answers12

224

Storing "Simple" Repeating Patterns

For my PHP/MySQL based calendar, I wanted to store repeating/recurring event information as efficiently as possibly. I didn't want to have a large number of rows, and I wanted to easily lookup all events that would take place on a specific date.

The method below is great at storing repeating information that occurs at regular intervals, such as every day, every n days, every week, every month every year, etc etc. This includes every Tuesday and Thursday type patterns as well, because they are stored separately as every week starting on a Tuesday and every week starting on a Thursday.

Assuming I have two tables, one called events like this:

ID    NAME
1     Sample Event
2     Another Event

And a table called events_meta like this:

ID    event_id      meta_key           meta_value
1     1             repeat_start       1299132000
2     1             repeat_interval_1  432000

With repeat_start being a date with no time as a unix timestamp, and repeat_interval an amount in seconds between intervals (432000 is 5 days).

repeat_interval_1 goes with repeat_start of the ID 1. So if I have an event that repeats every Tuesday and every Thursday, the repeat_interval would be 604800 (7 days), and there would be 2 repeat_starts and 2 repeat_intervals. The table would look like this:

ID    event_id      meta_key           meta_value
1     1             repeat_start       1298959200 -- This is for the Tuesday repeat
2     1             repeat_interval_1  604800
3     1             repeat_start       1299132000 -- This is for the Thursday repeat
4     1             repeat_interval_3  604800
5     2             repeat_start       1299132000
6     2             repeat_interval_5  1          -- Using 1 as a value gives us an event that only happens once

Then, if you have a calendar that loops through every day, grabbing the events for the day it's at, the query would look like this:

SELECT EV.*
FROM `events` EV
RIGHT JOIN `events_meta` EM1 ON EM1.`event_id` = EV.`id`
RIGHT JOIN `events_meta` EM2 ON EM2.`meta_key` = CONCAT( 'repeat_interval_', EM1.`id` )
WHERE EM1.meta_key = 'repeat_start'
    AND (
        ( CASE ( 1299132000 - EM1.`meta_value` )
            WHEN 0
              THEN 1
            ELSE ( 1299132000 - EM1.`meta_value` )
          END
        ) / EM2.`meta_value`
    ) = 1
LIMIT 0 , 30

Replacing {current_timestamp} with the unix timestamp for the current date (Minus the time, so the hour, minute and second values would be set to 0).

Hopefully this will help somebody else too!


Storing "Complex" Repeating Patterns

This method is better suited for storing complex patterns such as

Event A repeats every month on the 3rd of the month starting on March 3, 2011

or

Event A repeats Friday of the 2nd week of the month starting on March 11, 2011

I'd recommend combining this with the above system for the most flexibility. The tables for this should like like:

ID    NAME
1     Sample Event
2     Another Event

And a table called events_meta like this:

ID    event_id      meta_key           meta_value
1     1             repeat_start       1299132000 -- March 3rd, 2011
2     1             repeat_year_1      *
3     1             repeat_month_1     *
4     1             repeat_week_im_1   2
5     1             repeat_weekday_1   6

repeat_week_im represents the week of the current month, which could be between 1 and 5 potentially. repeat_weekday in the day of the week, 1-7.

Now assuming you are looping through the days/weeks to create a month view in your calendar, you could compose a query like this:

SELECT EV . *
FROM `events` AS EV
JOIN `events_meta` EM1 ON EM1.event_id = EV.id
AND EM1.meta_key = 'repeat_start'
LEFT JOIN `events_meta` EM2 ON EM2.meta_key = CONCAT( 'repeat_year_', EM1.id )
LEFT JOIN `events_meta` EM3 ON EM3.meta_key = CONCAT( 'repeat_month_', EM1.id )
LEFT JOIN `events_meta` EM4 ON EM4.meta_key = CONCAT( 'repeat_week_im_', EM1.id )
LEFT JOIN `events_meta` EM5 ON EM5.meta_key = CONCAT( 'repeat_weekday_', EM1.id )
WHERE (
  EM2.meta_value =2011
  OR EM2.meta_value = '*'
)
AND (
  EM3.meta_value =4
  OR EM3.meta_value = '*'
)
AND (
  EM4.meta_value =2
  OR EM4.meta_value = '*'
)
AND (
  EM5.meta_value =6
  OR EM5.meta_value = '*'
)
AND EM1.meta_value >= {current_timestamp}
LIMIT 0 , 30

This combined with the above method could be combined to cover most repeating/recurring event patterns. If I've missed anything please leave a comment.

Jon Surrell
  • 8,394
  • 6
  • 44
  • 52
Brandon Wamboldt
  • 15,241
  • 11
  • 49
  • 82
  • 1
    I am trying your Storing "Simple" Repeating Patterns. if i need it to repeat every week on Tuesday, do i need to modify the repeat_start or create a new record with the last date. or is there a way for it to repeat every week based on the first repeat_start??? – loo Jan 16 '12 at 17:46
  • 1
    Your answer was a great help @roguecoder but it didn't quite work off the bat... I found my answer after this post: http://stackoverflow.com/questions/10545869/repeating-calendar-events-and-some-final-maths – Ben Sinclair May 12 '12 at 05:57
  • But how do I query for all events that happen in a week range? – Pez Cuckow Dec 09 '12 at 13:34
  • 1
    In `AND ( ( CASE ( 1299132000 - EM1.meta_value ) WHEN 0 THEN 1 ELSE ( 1299132000 - EM1.meta_value) END ) / EM2.meta_value ) = 1` is this `/ EM2.meta_value` placed wrongly? – Murali Murugesan Nov 29 '13 at 11:14
  • 1
    This is a great help. How would you suggest accessing these as individual records, say, if you wanted to have comments or check-ins on individual events? – johnrees Jan 05 '14 at 01:49
  • 27
    It's worth noting that you shouldn't use hardcoded values for repeat intervals, i.e. `86400` seconds in a day, because it doesn't factor in daylight savings time. It's more appropriate to calculate these things dynamically on the fly and instead store `interval = daily` and `interval_count = 1` or `interval = monthly` and `interval_count = 1`. – Corey Ballou Jul 24 '14 at 12:47
  • How do you make the event only repeat until a specific date? – Cam Connor Jul 28 '14 at 16:44
  • 1
    @Brandon Wamboldt How can i delete one instance occurrence from repeated event. – Pugazhenthi Dec 25 '15 at 11:42
  • Cool pattern, but why not create an event row for each recurring event? It's an event. You might do some specific modifications on THAT event.. or wan't some history reporting about previous events? – Mittchel Dec 28 '15 at 10:58
  • @Mittchel 2 issues that come to mind. First is the complexity in handling modifying a series of events when each event in the series has its own row. Mainly though, how would one go about handling repeating events that never end? – Jestep Jan 18 '16 at 16:32
  • @MuraliMurugesan you're right, it is wrongly placed – julestruong Mar 02 '16 at 10:47
  • Solving this with Mongo DB - http://stackoverflow.com/q/39233425/1060044 – Krishna Shetty Aug 31 '16 at 07:40
  • @brandon-wamboldt should i change something in your `complex` model if I want to have recurrent pattern (following RFC 5545) for example `RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=TU,WE,TH` - it means each second week on every Tuesday, Wednesday, Thursday (i've missed some unimportant details, but I can add if you need ones, just let me know, please). – Sergii May 16 '17 at 19:14
  • This is great but can you tell me how to consider time based scheduling in this as there might be scenario of running certain event after every 2 hours – Zeeshan Jul 25 '17 at 15:51
  • Implementing something along these lines for public facing tool. Curious what a streamlined UX looks like for this example. – zeros-and-ones Dec 26 '17 at 06:47
192

While the currently accepted answer was a huge help to me, I wanted to share some useful modifications that simplify the queries and also increase performance.


"Simple" Repeat Events

To handle events which recur at regular intervals, such as:

Repeat every other day 

or

Repeat every week on Tuesday 

You should create two tables, one called events like this:

ID    NAME
1     Sample Event
2     Another Event

And a table called events_meta like this:

ID    event_id      repeat_start       repeat_interval
1     1             1369008000         604800            -- Repeats every Monday after May 20th 2013
1     1             1369008000         604800            -- Also repeats every Friday after May 20th 2013

With repeat_start being a unix timestamp date with no time (1369008000 corresponds to May 20th 2013) , and repeat_interval an amount in seconds between intervals (604800 is 7 days).

By looping over each day in the calendar you can get repeat events using this simple query:

SELECT EV.*
FROM `events` EV
RIGHT JOIN `events_meta` EM1 ON EM1.`event_id` = EV.`id`
WHERE  (( 1299736800 - repeat_start) % repeat_interval = 0 )

Just substitute in the unix-timestamp (1299736800) for each date in your calendar.

Note the use of the modulo (% sign). This symbol is like regular division, but returns the ''remainder'' instead of the quotient, and as such is 0 whenever the current date is an exact multiple of the repeat_interval from the repeat_start.

Performance Comparison

This is significantly faster than the previously suggested "meta_keys"-based answer, which was as follows:

SELECT EV.*
FROM `events` EV
RIGHT JOIN `events_meta` EM1 ON EM1.`event_id` = EV.`id`
RIGHT JOIN `events_meta` EM2 ON EM2.`meta_key` = CONCAT( 'repeat_interval_', EM1.`id` )
WHERE EM1.meta_key = 'repeat_start'
    AND (
        ( CASE ( 1299132000 - EM1.`meta_value` )
            WHEN 0
              THEN 1
            ELSE ( 1299132000 - EM1.`meta_value` )
          END
        ) / EM2.`meta_value`
    ) = 1

If you run EXPLAIN this query, you'll note that it required the use of a join buffer:

+----+-------------+-------+--------+---------------+---------+---------+------------------+------+--------------------------------+
| id | select_type | table | type   | possible_keys | key     | key_len | ref              | rows | Extra                          |
+----+-------------+-------+--------+---------------+---------+---------+------------------+------+--------------------------------+
|  1 | SIMPLE      | EM1   | ALL    | NULL          | NULL    | NULL    | NULL             |    2 | Using where                    |
|  1 | SIMPLE      | EV    | eq_ref | PRIMARY       | PRIMARY | 4       | bcs.EM1.event_id |    1 |                                |
|  1 | SIMPLE      | EM2   | ALL    | NULL          | NULL    | NULL    | NULL             |    2 | Using where; Using join buffer |
+----+-------------+-------+--------+---------------+---------+---------+------------------+------+--------------------------------+

The solution with 1 join above requires no such buffer.


"Complex" Patterns

You can add support for more complex types to support these types of repeat rules:

Event A repeats every month on the 3rd of the month starting on March 3, 2011

or

Event A repeats second Friday of the month starting on March 11, 2011

Your events table can look exactly the same:

ID    NAME
1     Sample Event
2     Another Event

Then to add support for these complex rules add columns to events_meta like so:

ID    event_id      repeat_start       repeat_interval    repeat_year    repeat_month    repeat_day    repeat_week    repeat_weekday
1     1             1369008000         604800             NULL           NULL            NULL          NULL           NULL             -- Repeats every Monday after May 20, 2013
1     1             1368144000         604800             NULL           NULL            NULL          NULL           NULL             -- Repeats every Friday after May 10, 2013
2     2             1369008000         NULL               2013           *               *             2              5                -- Repeats on Friday of the 2nd week in every month    

Note that you simply need to either specify a repeat_interval or a set of repeat_year, repeat_month, repeat_day, repeat_week, and repeat_weekday data.

This makes selection of both types simultaneously very simple. Just loop through each day and fill in the correct values, (1370563200 for June 7th 2013, and then the year, month, day, week number and weekday as follows):

SELECT EV.*
FROM `events` EV
RIGHT JOIN `events_meta` EM1 ON EM1.`event_id` = EV.`id`
WHERE  (( 1370563200 - repeat_start) % repeat_interval = 0 )
  OR ( 
    (repeat_year = 2013 OR repeat_year = '*' )
    AND
    (repeat_month = 6 OR repeat_month = '*' )
    AND
    (repeat_day = 7 OR repeat_day = '*' )
    AND
    (repeat_week = 2 OR repeat_week = '*' )
    AND
    (repeat_weekday = 5 OR repeat_weekday = '*' )
    AND repeat_start <= 1370563200
  )

This returns all events that repeat on the Friday of the 2nd week, as well as any events that repeat every Friday, so it returns both event ID 1 and 2:

ID    NAME
1     Sample Event
2     Another Event

*Sidenote in the above SQL I used PHP Date's default weekday indexes, so "5" for Friday


Hope this helps others as much as the original answer helped me!

Jon Surrell
  • 8,394
  • 6
  • 44
  • 52
ahoffner
  • 3,067
  • 2
  • 20
  • 22
  • 6
    This is amazing, thank you! Do you have any idea how you'd encode "every 2 months on the first Monday" or "every 3 months on the first Monday", etc.? – Jordan Lev Sep 30 '13 at 03:33
  • 6
    I agree this is amazing. I ran into the same dilemma that Jordan Lev did, however. The repeat_interval field is no good for repeating months because some months are longer than others. Also, how do you limit the duration of a recurring event. I.e., every 2 months on the first Monday for 8 months. The table should have some sort of end date. – Abinadi Nov 04 '13 at 04:59
  • 11
    This is an excellent answer. I've dropped the repeat_interval and added repeat_end date, but this answer helped enormously. – Iain Collins Nov 07 '13 at 22:00
  • 3
    Tip: For complex patterns, one could eliminate the `repeat_interval` column and represent it in the subsequent columns (i.e `repeat_year`, etc.) For the first row, the situation of repeating every Monday after May 20, 2013, can be represented by placing a 1 in the `repeat_weekday` and an `*` in the other columns. – musubi Jan 05 '14 at 08:05
  • 2
    @Abinadi About "every 2 months on the first Monday for 8 months.", I think we can use repeat_month to do, such as fill in "1,3,5,7,9,11"; in the case, we also need add end date. – Stony Jan 06 '14 at 15:42
  • 1
    Has anyone thought of a good select to get a months worth of data instead of a days worth. I am trying to think of one as the worst case would be doing 31 selects and I am not keen on doing this – bubblebath Apr 11 '14 at 10:38
  • can anyone show me how to add a record(s) to the meta table for example MWF every other week ?? – Marco Jun 01 '14 at 17:26
  • @Marco you should be able to just double the "repeat interval" for a weekly repeat event, specifying one per day of the week (MWF). Ex. Repeats every OTHER Monday after May 20, 2013: `INSERT INTO events_meta (event_id, repeat_start, repeat_interval) VALUES(1, 1369008000, 1209600);` – ahoffner Jun 04 '14 at 22:24
  • This doesn't work for "The first Monday of the month", if the Monday falls on the second week of the month, right? – user2415992 Nov 19 '14 at 22:30
  • 1
    @bubblebath I am thinking I will run the day by day selects as a cron and populate an instances table for rapid use on front end. – jerrygarciuh Nov 20 '14 at 19:39
  • 1
    I have implemented this design in my project but is struggling to find the free time slots for the given user for the recurring. Anybody have any idea? – Sanjay Kumar N S May 07 '15 at 12:15
  • @user2415992 - that's correct and if you use 5 in repeat_week for "last Friday of month" etc it also fails. I'm working on a second set of queries to cover those using mySQL in-built date math like this for last Friday of month `SELECT LAST_DAY(NOW()) - ((7 + WEEKDAY(LAST_DAY(NOW())) - 4) % 7);` – jerrygarciuh May 08 '15 at 20:08
  • Repeats on Friday of the 2nd week in every month not worked for me, it s always return true for all Friday in a month, any updates? – Gohel Kiran Jul 17 '15 at 11:38
  • How can I adjust the sql SELECT for the complex example to only select a specific event_id? I thought I'd be able to just say `...RIGHT JOIN events_meta EM1 ON EM1.event_id = EV.id WHERE EV.id = $event_id AND ( DATEDIFF( '$date', repeat_start ) % repeat_interval = 0 ) OR ( ...` but it's selecting other event_id's as well. – ctown4life Aug 14 '15 at 19:33
  • 1
    You just need to watch how you group that `EV.I'd = $event_id` portion with the existing where clauses, wrap everything after the next AND with parentheses. – ahoffner Aug 14 '15 at 19:51
  • 1
    I'm hung up on a case that this may not support. Any thoughts on how I can create a recurring event that happens on the first monday of the month every X number of months (ex: every 2 months or every 3 months)? – ctown4life Sep 02 '15 at 05:41
  • I don't think you can get it to do every X number of months but using Stony's suggestion above I am able to get it to work for every 2 months, 3 months, 4 months, and 6 months. That's good enough for what my users need. I added a 0 to the single digit months ex: 02,04,06,08,10,12. You'll also need to update your sql at the repeat_month section with `(repeat_month = $month OR repeat_month = '*' OR repeat_month LIKE '%{$month}%' )` – ctown4life Sep 02 '15 at 19:30
  • I dig it; and seems to mirror some of my own thoughts after reading the accepted answer; this should be the new accepted answer imho – T.W.R. Cole Apr 04 '16 at 20:03
  • Like this. Had to add OR ((repeat_interval=0) AND (repeat_start='$nowString'))"; to SQL to also have non-repeating calendar items. Otherwise you would have to specify them as a repeating pattern that could only occur once. – Enigma Plus May 23 '16 at 13:56
  • I am working on achieving this using Mongo DB. Do you think this should be feasible on document database like Mongo also? Thanks – Krishna Shetty Aug 05 '16 at 13:20
  • 1
    @KrishnaShetty You *could* use similar selection criteria in MongoDB, yes. Performance of this technique may be different than it is in MySQL; as firstly the "join" would instead be a part of the Aggregation pipeline for that one-to-many relationship, and secondly I believe you'll need to also use [Aggregation for the modulo operation](http://stackoverflow.com/a/37354483/2403513). There might be a more efficient MongoDB method than this one, I'm not immediately sure. – ahoffner Aug 08 '16 at 17:49
  • My concerns with this solution: Months and years have variable lengths. How can we deal with that? (For the Simple Solution) – maracuja-juice Aug 18 '16 at 11:30
  • Trying to model this in Mongo DB as described at: http://stackoverflow.com/q/39233425/1060044 Trying to fix a query performance – Krishna Shetty Aug 30 '16 at 17:16
  • I know this is an old post, but could I have the meta record for the following pattern : Event A repeats every month on the 3rd of the month starting on March 3, 2011 – Olivier MATROT Jun 12 '17 at 12:49
  • @OlivierMATROT Did you find how meta records look like for Event A repeats every month on the 3rd of the month starting on March 3, 2011 – Milos Jan 16 '19 at 00:28
  • @milos unfortunately, no. – Olivier MATROT Jan 17 '19 at 13:04
  • 3
    @OlivierMATROT @milos The idea is to set the field you want to be fixed explicitly, and the rest to the wildcard `*`. So for "every month on the 3rd" you just set `repeat_day` to 3, the rest of the `repeat` fields to * (leave `repeat_interval` null), and set the repeat_start to the unix timecode for March 3, 2011 to be your anchor date. – ahoffner Jan 18 '19 at 17:06
  • @ahoffner thx you!!! I need to ask you one more question, What about every 2months or every 4 months? i was thinking about this solution repeat_month=[1,3,5,7,9,11] – Milos Jan 22 '19 at 10:43
  • @SanjayKumarNS Did you find the solution for the recurring event issue? I want to implement the same – hardiksa Apr 07 '20 at 14:33
  • How can we get all events for a month? – Akshay katale Oct 03 '20 at 14:22
  • @Akshaykatale you can do a simple query; assuming you removed `repeat_interval` as suggested above, and you are looking for all the events in January 2021, it would be: SELECT EV.* FROM `events` EV RIGHT JOIN `events_meta` EM1 ON EM1.`event_id` = EV.`id` WHERE (repeat_year = 2021 OR repeat_year = '*' ) AND (repeat_month = 1 OR repeat_month = '*' ) – Luciano Jan 01 '21 at 18:47
  • With the complex representation, how do we go about finding all the occurrences of some event in a time range? An easy way I can think of is to create an RRule then simply use the RRule.between(start_date, end_date) method. However, the standard RRule constructor requires a frequency parameter (weekly, daily, etc) which to calculate based on this schema requires an if statement for each supported frequency I think, which is a bit annoying. Anybody have an easier way? Maybe adding the RRule as an attribute in the table? – Luciano Jan 01 '21 at 18:52
30

Enhancement: replace timestamp with date

As a small enhancement to the accepted answer that was subsequently refined by ahoffner - it is possible to use a date format rather than timestamp. The advantages are:

  1. readable dates in the database
  2. no issue with the years > 2038 and timestamp
  3. removes need to be careful with timestamps that are based on seasonally adjusted dates i.e. in the UK 28th June starts one hour earlier than 28th December so deriving a timestamp from a date can break the recursion algorithm.

to do this, change the DB repeat_start to be stored as type 'date' and repeat_interval now hold days rather than seconds. i.e. 7 for a repeat of 7 days.

change the sql line:

WHERE (( 1370563200 - repeat_start) % repeat_interval = 0 )

to:

WHERE ( DATEDIFF( '2013-6-7', repeat_start ) % repeat_interval = 0)

everything else remains the same. Simples!

Community
  • 1
  • 1
user3781087
  • 369
  • 3
  • 4
  • So what If I want my event to repeat year by year ? repeat_interval should store 365 days ? What if they year has 366 days ? – Tarida George Mar 03 '15 at 08:17
  • 3
    @George02 if event is annual you leave repeat_interval NULL and repeat_year is * then depending on what recurrence is you may set repeat_month and repeat_day eg March 11 or repeat_month, repeat_week, and repeat_weekday to set 2nd Tuesday in April. – jerrygarciuh May 08 '15 at 19:45
29

I would follow this guide: https://github.com/bmoeskau/Extensible/blob/master/recurrence-overview.md

Also make sure you use the iCal format so not to reinvent the wheel and remember Rule #0: Do NOT store individual recurring event instances as rows in your database!

Gal Bracha
  • 14,231
  • 9
  • 61
  • 77
  • 2
    How would you model a tracking users who have attended a specific instance? Does it make sense to break from Rule #0 in this case? – Danny Sullivan Sep 26 '17 at 07:12
  • 2
    @DannySullivan From the top of my head, I would have another entity `attendedEvent` with `baseInstanceId` and `instanceStartDate` - That is for example the base event from which you created the recurring rules calendar view and use the start date to specify information on that specific instance Then this entity could also have something like `attendedListId` which leads to another table of `id`, `attendedUserId` – Gal Bracha Sep 26 '17 at 16:10
  • @DannySullivan I know it's been awhile since you asked. But outside of the previous comment you could always do a reverse lookup to see if that user was part of the event recurrence pattern. That would tell you if they were at least scheduled for the event. Whether they actually attended or not is a different story which would be more along the lines of DannySullivan's comment. – BRogers Mar 23 '20 at 22:03
25

For all of you who are interested in this, now you can just copy and paste to get started within minutes. I took the advice in the comments as well as I could. Let me know if I'm missing something.

"COMPLEX VERSION":

events

+----------+----------------+
| ID       | NAME           | 
+----------+----------------+
| 1        | Sample event 1 |
| 2        | Second  event  |
| 3        | Third event    |
+----------+----------------+

events_meta

+----+----------+--------------+------------------+-------------+--------------+------------+-------------+----------------+
| ID | event_id | repeat_start | repeat_interval  | repeat_year | repeat_month | repeat_day | repeat_week | repeat_weekday |
+----+----------+--------------+------------------+-------------+--------------+------------+-------------+----------------+
| 1  | 1        | 2014-07-04   | 7                | NULL        | NULL         | NULL       | NULL        | NULL           |
| 2  | 2        | 2014-06-26   | NULL             | 2014        | *            | *          | 2           | 5              |
| 3  | 3        | 2014-07-04   | NULL             | *           | *            | *          | *           | 5              |
+----+----------+--------------+------------------+-------------+--------------+------------+-------------+----------------+

SQL code:

CREATE TABLE IF NOT EXISTS `events` (
  `ID` int(11) NOT NULL AUTO_INCREMENT,
  `NAME` varchar(255) NOT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=7 ;

--
-- Dumping data for table `events`
--

INSERT INTO `events` (`ID`, `NAME`) VALUES
(1, 'Sample event'),
(2, 'Another event'),
(3, 'Third event...');

CREATE TABLE IF NOT EXISTS `events_meta` (
  `ID` int(11) NOT NULL AUTO_INCREMENT,
  `event_id` int(11) NOT NULL,
  `repeat_start` date NOT NULL,
  `repeat_interval` varchar(255) NOT NULL,
  `repeat_year` varchar(255) NOT NULL,
  `repeat_month` varchar(255) NOT NULL,
  `repeat_day` varchar(255) NOT NULL,
  `repeat_week` varchar(255) NOT NULL,
  `repeat_weekday` varchar(255) NOT NULL,
  PRIMARY KEY (`ID`),
  UNIQUE KEY `ID` (`ID`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=6 ;

--
-- Dumping data for table `events_meta`
--

INSERT INTO `events_meta` (`ID`, `event_id`, `repeat_start`, `repeat_interval`, `repeat_year`, `repeat_month`, `repeat_day`, `repeat_week`, `repeat_weekday`) VALUES
(1, 1, '2014-07-04', '7', 'NULL', 'NULL', 'NULL', 'NULL', 'NULL'),
(2, 2, '2014-06-26', 'NULL', '2014', '*', '*', '2', '5'),
(3, 3, '2014-07-04', 'NULL', '*', '*', '*', '*', '1');

also available as MySQL export (for easy access)

PHP example code index.php:

<?php
    require 'connect.php';    

    $now = strtotime("yesterday");

    $pushToFirst = -11;
    for($i = $pushToFirst; $i < $pushToFirst+30; $i++)
    {
        $now = strtotime("+".$i." day");
        $year = date("Y", $now);
        $month = date("m", $now);
        $day = date("d", $now);
        $nowString = $year . "-" . $month . "-" . $day;
        $week = (int) ((date('d', $now) - 1) / 7) + 1;
        $weekday = date("N", $now);

        echo $nowString . "<br />";
        echo $week . " " . $weekday . "<br />";



        $sql = "SELECT EV.*
                FROM `events` EV
                RIGHT JOIN `events_meta` EM1 ON EM1.`event_id` = EV.`id`
                WHERE ( DATEDIFF( '$nowString', repeat_start ) % repeat_interval = 0 )
                OR ( 
                    (repeat_year = $year OR repeat_year = '*' )
                    AND
                    (repeat_month = $month OR repeat_month = '*' )
                    AND
                    (repeat_day = $day OR repeat_day = '*' )
                    AND
                    (repeat_week = $week OR repeat_week = '*' )
                    AND
                    (repeat_weekday = $weekday OR repeat_weekday = '*' )
                    AND repeat_start <= DATE('$nowString')
                )";
        foreach ($dbConnect->query($sql) as $row) {
            print $row['ID'] . "\t";
            print $row['NAME'] . "<br />";
        }

        echo "<br /><br /><br />";
    }
?>

PHP example code connect.php:

<?
// ----------------------------------------------------------------------------------------------------
//                                       Connecting to database
// ----------------------------------------------------------------------------------------------------
// Database variables
$username = "";
$password = "";
$hostname = ""; 
$database = ""; 

// Try to connect to database and set charset to UTF8
try {
    $dbConnect = new PDO("mysql:host=$hostname;dbname=$database;charset=utf8", $username, $password);
    $dbConnect->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

} catch(PDOException $e) {
    echo 'ERROR: ' . $e->getMessage();
}
// ----------------------------------------------------------------------------------------------------
//                                      / Connecting to database
// ----------------------------------------------------------------------------------------------------
?>

Also the php code is available here (for better readability):
index.php
and
connect.php
Now setting this up should take you minutes. Not hours. :)

Alex
  • 401
  • 4
  • 7
  • 2
    how can i query to get all repeated events within a date range.. that is to get the all repeated events between 2014-10-01 to 2014-12-30. thanks for your post – Well Wisher Oct 16 '14 at 06:00
  • @Wellwisher - repeat ... until and temporary table http://stackoverflow.com/questions/34407833/calendar-recurring-repeating-events-find-events-in-date-range – Brad Kent Dec 23 '15 at 03:44
  • 1
    @Alex How can i delete one instance occurrence from repeated event. – Pugazhenthi Dec 25 '15 at 11:41
  • 1
    I know that this is an old thread but why the varchar type on repeat_* columns ? Could'nt you use an integer and a negative value instead of '*' ? – Olivier MATROT Jun 09 '17 at 11:36
  • This is great but can you tell me how to consider time based scheduling in this as there might be scenario of running certain event after every 2 hours – Zeeshan Jul 25 '17 at 15:51
  • 1
    Thanks for the code. I must however remark that your db/queries implementation is a bit disturbing, and very inefficient. For instance why use varchar(255) for such simple columns (as @OlivierMATROT mentioned, you could use integers, and even if not, why 255?). And if you`re repeating the query 30 times, why not use statements or procedures? Just saying for the sake if someone is about to implement this. – Rony Aug 03 '17 at 12:41
  • Do you have idea how events_meta looks like for reccuring event like 'Run every 3 month on 10th' or Run this payment on the given date '10th Januar 2019' – Milos Jan 16 '19 at 13:44
15

While the proposed solutions work, I was trying to implement with Full Calendar and it would require over 90 database calls for each view (as it loads current, previous, and next month), which, I wasn't too thrilled about.

I found an recursion library https://github.com/tplaner/When where you simply store the rules in the database and one query to pull all the relevant rules.

Hopefully this will help someone else, as I spent so many hours trying to find a good solution.

Edit: This Library is for PHP

Tim Ramsey
  • 752
  • 12
  • 21
  • I also want to use fullcalendar. How When library could help me? How to extract propoer events? – piernik Oct 11 '16 at 08:02
  • @piernik - I would setup the library as in the documentation and if you are running into specific issues open a new question on stackoverflow with the code you have setup and the issues you are having. I am sure if you put that much effort in some members will help you out. – Tim Ramsey Oct 11 '16 at 14:14
  • I mean that using `When` You have to store all reccuring dates in database or get all reccuring events and generate dates in php no in database. Am I right? – piernik Oct 14 '16 at 10:37
  • @piernik You would store the initial Date and the rule/s in the database and use `When` to generate all the dates - which are populated from the initial stored date/rules. – Tim Ramsey Oct 14 '16 at 20:23
  • It's not good either - You can't in a single mysql commande get correct events - You have to use PHP for that. Thanks anyway – piernik Oct 15 '16 at 12:40
  • I thought it was kind of obvious when viewing the link "Date/Calendar recursion library for PHP 5.3+", but I have updated my answer to include this information. – Tim Ramsey Oct 17 '16 at 13:49
15

Why not use a mechanism similar to Apache cron jobs? http://en.wikipedia.org/wiki/Cron

For calendar\scheduling I'd use slightly different values for "bits" to accommodate standard calendar reoccurence events - instead of [day of week (0 - 7), month (1 - 12), day of month (1 - 31), hour (0 - 23), min (0 - 59)]

-- I'd use something like [Year (repeat every N years), month (1 - 12), day of month (1 - 31), week of month (1-5), day of week (0 - 7)]

Hope this helps.

Vladimir
  • 151
  • 1
  • 2
  • 6
    I think that is too many day of week options. Either 1-7 or 0-6 seems more accurate. – Abinadi Nov 04 '13 at 05:01
  • 2
    It's good to use cron to store the repeat. but the issue is it's very difficult to lookup. – Stony Jan 06 '14 at 15:32
  • @Vladimir how do you store , every two tuesday ( every two weeks on tuesday) – julestruong Mar 02 '16 at 11:59
  • @julestruong this website looks like it has the answer: https://coderwall.com/p/yzzu5a/running-a-cron-job-every-other-week – Ashton Wiersdorf Dec 12 '17 at 20:38
  • cron has limited expressivity, since it is stateless (merely comparing current/hypotherical date/time to a pattern), as such, it cannot represent certain common business/human patterns such as "every third day" or "every 7 hours", which require remembering the last occurrence. This isn't obvious; you might think that you just say day/3 or hour/7 in crontab, but then at the end of the month/day, you have "leftover" days/hours which are fewer than 3 or 7; with possible catastrophic results. – Jaime Guerrero Nov 26 '19 at 23:01
8

RRULE standard is built for exactly this requirement i.e. saving and understanding recurrences. Microsoft and google both use it in their calendar events. Please go through this document for more details. https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html

Ravinder_Chahal
  • 193
  • 1
  • 4
5

I developed an esoteric programming language just for this case. The best part about it is that it is schema less and platform independent. You just have to write a selector program, for your schedule, syntax of which is constrained by the set of rules described here -

https://github.com/tusharmath/sheql/wiki/Rules

The rules are extendible and you can add any sort of customization based on the kind of repetition logic you want to perform, without worrying about schema migrations etc.

This is a completely different approach and might have some disadvantages of its own.

tusharmath
  • 9,120
  • 11
  • 52
  • 70
4

Sounds very much like MySQL events that are stored in system tables. You can look at the structure and figure out which columns are not needed:

   EVENT_CATALOG: NULL
    EVENT_SCHEMA: myschema
      EVENT_NAME: e_store_ts
         DEFINER: jon@ghidora
      EVENT_BODY: SQL
EVENT_DEFINITION: INSERT INTO myschema.mytable VALUES (UNIX_TIMESTAMP())
      EVENT_TYPE: RECURRING
      EXECUTE_AT: NULL
  INTERVAL_VALUE: 5
  INTERVAL_FIELD: SECOND
        SQL_MODE: NULL
          STARTS: 0000-00-00 00:00:00
            ENDS: 0000-00-00 00:00:00
          STATUS: ENABLED
   ON_COMPLETION: NOT PRESERVE
         CREATED: 2006-02-09 22:36:06
    LAST_ALTERED: 2006-02-09 22:36:06
   LAST_EXECUTED: NULL
   EVENT_COMMENT:
Volte
  • 1,879
  • 16
  • 24
Valentin Kuzub
  • 10,477
  • 6
  • 50
  • 87
3

@Rogue Coder

This is great!

You could simply use the modulo operation (MOD or % in mysql) to make your code simple at the end:

Instead of:

AND (
    ( CASE ( 1299132000 - EM1.`meta_value` )
        WHEN 0
          THEN 1
        ELSE ( 1299132000 - EM1.`meta_value` )
      END
    ) / EM2.`meta_value`
) = 1

Do:

$current_timestamp = 1299132000 ;

AND ( ('$current_timestamp' - EM1.`meta_value` ) MOD EM2.`meta_value`) = 1")

To take this further, one could include events that do not recur for ever.

Something like "repeat_interval_1_end" to denote the date of the last "repeat_interval_1" could be added. This however, makes the query more complicated and I can't really figure out how to do this ...

Maybe someone could help!

Stephan
  • 37,597
  • 55
  • 216
  • 310
dorogz
  • 171
  • 1
  • 9
1

The two examples you've given are very simple; they can be represented as a simple interval (the first being four days, the second being 14 days). How you model this will depend entirely on the complexity of your recurrences. If what you have above is truly that simple, then store a start date and the number of days in the repeat interval.

If, however, you need to support things like

Event A repeats every month on the 3rd of the month starting on March 3, 2011

Or

Event A repeats second Friday of the month starting on March 11, 2011

Then that's a much more complex pattern.

Adam Robinson
  • 171,726
  • 31
  • 271
  • 330
  • 1
    I made add the more complex rules you just stated at a later point but not for now. How would I model the SQL query to get events on say March 7th, 2011 so that it would get my recurring event? – Brandon Wamboldt Mar 03 '11 at 16:45