382

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.

3

12 Answers 12

243

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.

7
  • 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? Commented Nov 29, 2013 at 11:14
  • 38
    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. Commented Jul 24, 2014 at 12:47
  • 1
    @Brandon Wamboldt How can i delete one instance occurrence from repeated event. Commented Dec 25, 2015 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
    Commented Dec 28, 2015 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
    Commented Jan 18, 2016 at 16:32
209

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

17
  • 7
    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
    Commented Sep 30, 2013 at 3:33
  • 7
    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
    Commented Nov 4, 2013 at 4:59
  • 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
    Commented Jan 5, 2014 at 8: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
    Commented Jan 6, 2014 at 15:42
  • 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. Commented Jan 18, 2019 at 17:06
34

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!

2
  • 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 ? Commented Mar 3, 2015 at 8: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. Commented May 8, 2015 at 19:45
27

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. :)

3
  • 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 Commented Oct 16, 2014 at 6:00
  • @Wellwisher - repeat ... until and temporary table stackoverflow.com/questions/34407833/…
    – Brad Kent
    Commented Dec 23, 2015 at 3:44
  • 2
    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
    Commented Aug 3, 2017 at 12:41
23

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

1
  • Good tip for PHP devs - a simple library that does what it needs to do! Commented Dec 24, 2022 at 10:09
17

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.

3
  • 7
    I think that is too many day of week options. Either 1-7 or 0-6 seems more accurate.
    – Abinadi
    Commented Nov 4, 2013 at 5:01
  • 2
    It's good to use cron to store the repeat. but the issue is it's very difficult to lookup.
    – Stony
    Commented Jan 6, 2014 at 15:32
  • 1
    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. Commented Nov 26, 2019 at 23:01
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.

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:
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!

0
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.

1
  • 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? Commented Mar 3, 2011 at 16:45
0

set @dat_ini = '2023-05-20',@dat_fim = '2022-11-20'; select (DATEDIFF( @dat_fim,@dat_ini )) % 60

THIS < 10

It only works for a short period.

To do this, take the start date and change the Month that is on the screen and add a year, then subtract it from the start date, then it works.

enter image description here

0

I know this is old, but based on all the answers here I wanted to see if I could come up with a MongoDB version and post for anyone else coming here.

Here is my contribution represented as TS interfaces:

// Main Calendar Event:
export interface ICalendarEvent {
    // ...
    name: string;
    starts_at: Date;
    ends_at: Date;
    repeat?: ICalendarEventPattern;
    // ...
}

// Repeat Pattern
export interface ICalendarEventPattern {
    years: ICalendarEventPatternValues;
    months: ICalendarEventPatternValues;
    dates: ICalendarEventPatternValues;
    weeks: ICalendarEventPatternValues;
    weekdays: ICalendarEventPatternValues;
}

// Pattern Values
export interface ICalendarEventPatternValues {
    all:boolean;
    values:number[];
}

Example event (Tuesday, December 12th 2023, 3rd week of the month):

{
  "name": "Biweekly Meeting",
  "starts_at": {
    "$date": "2023-12-12T16:00:00.000Z"
  },
  "ends_at": {
    "$date": "2023-12-12T19:00:00.000Z"
  },
  "repeat": {
    "years": {
      "all": true,
      "values": []
    },
    "months": {
      "all": true,
      "values": []
    },
    "dates": {
      "all": true,
      "values": []
    },
    "weeks": {
      "all": true,
      "values": []
    },
    "weekdays": {
      "all": false,
      "values": [2,4]
    }
  }
}

Find recurring events that have an event on 2023-12-19 (a Tuesday, 4th week of the month):

{
  $and: [
        { starts_at: { $lt: new ISODate('2023-12-19T11:00:00') } },
        { $or: [{ 'repeat.years.all': true },{ 'repeat.years.values': 2023 }] },
        { $or: [{ 'repeat.months.all': true },{ 'repeat.months.values': 12 }] },
        { $or: [{ 'repeat.dates.all': true },{ 'repeat.dates.values': 19 }] },
        { $or: [{ 'repeat.weeks.all': true },{ 'repeat.weeks.values': 4 }] },
        { $or: [{ 'repeat.weekdays.all': true },{ 'repeat.weekdays.values': 2 } }] }
    ]
}

This also supports multiple pattern values. For example, find events repeating on Tuesdays AND Thursdays (0-6, Sunday-Saturday):

{
  $and: [
        { starts_at: { $lt: new ISODate('2023-12-19T11:00:00') } },
        { $or: [{ 'repeat.years.all': true },{ 'repeat.years.values': 2023 }] },
        { $or: [{ 'repeat.months.all': true },{ 'repeat.months.values': 12 }] },
        { $or: [{ 'repeat.dates.all': true },{ 'repeat.dates.values': 19 }] },
        { $or: [{ 'repeat.weeks.all': true },{ 'repeat.weeks.values': 4 }] },
        { $or: [{ 'repeat.weekdays.all': true },{ 'repeat.weekdays.values': { $in:[2,4] } }] }
    ]
}

Not the answer you're looking for? Browse other questions tagged or ask your own question.