Event calendar: repeating dates

We are working on a site featuring an event calendar. Each event is stored in a single page, containing a start and end date. Now we’d need the option to make events recuring in given intervals (every day, every week, every month, every year) with optional end date (“repeat until …”).

Are there any existing solutions to handle this scenario in Kirby?

Not that I know of. I had a project where I used a start date, end date and two additional fields: a select field for the interval, and a structure field for exceptions. Then calculated all events from this data.

I thought of something similar but it sounds computationally intensive for Kirby to get all dates related to – say – the current week. Or is this neglectable? We are not dealing with that many dates.

I would limit the end date, either through the date field itself or by setting your end date (if none is given) to let’s say 1 year or so in the future (or less, depending on interval). You can do this dynamically.

You can also cache your results, so I don’t see any problems.

1 Like

Okay, I think we just have to try this out and see how far it goes.

I actually need something like this too at the moment, but there is a small issue with repeating events. It works ok if the event is, for example, on the second Sunday of every month. That you can repeat easily but if the event is always the 13th day of the month, that gets harder. This year that day might be a Monday and the event should be on a Sunday. Being able to adjust the event to the 12th for that year is a tricky thing to implement.

We actually gave up and just use a structure field so that the specific dates can be picked manually for the next few years.

Haven’t had that case yet, so have to think first. But since I have to work, I don’t have time to think. Now that should make you think :wink:

Does it help to use PHPs DateInterval and DatePeriod?

Yep. DateInterval

Posting it for reference here, this example from the PHP docs might be helpful:


<?php

$begin = new DateTime( '2012-08-01' );
$end = new DateTime( '2012-08-31' );
$end = $end->modify( '+1 day' );

$interval = new DateInterval('P1D');
$daterange = new DatePeriod($begin, $interval ,$end);

foreach($daterange as $date){
    echo $date->format("Ymd") . "<br>";
}
?>
1 Like

May be you want to take the leap year into account…
But then note, that 2100 is NO leap year.

No, PHP’s DateTime class does that automatically.

Yes,

But our calculation, if we want to start on the same day of week, has to think of this.

If I run…

<?php

$begin = new DateTime( '2012-08-01' );
$end = new DateTime( '2022-08-31' );
// $end = $end->modify( '+1 day' );

$interval = new DateInterval('P1Y');
$daterange = new DatePeriod($begin, $interval ,$end);

foreach($daterange as $date){
    echo $date->format("l, Y-m-d") . "<br>";
}
?>

… I see, that the day of week for the first of august changes:

Wednesday, 2012-08-01
Thursday, 2013-08-01
Friday, 2014-08-01
Saturday, 2015-08-01
Monday, 2016-08-01
Tuesday, 2017-08-01
Wednesday, 2018-08-01
Thursday, 2019-08-01
Saturday, 2020-08-01
Sunday, 2021-08-01
Monday, 2022-08-01

and not everywhere by ONE day of week.

The result looks perfectly alright to me. I really don’t understand what your point it. It would be great if we could keep this thread clean.

@nilshoerrmann, this is one of the methods I used in my project (note Kirby 2 syntax, haven’t updated yet):

page::$methods['getEventDates'] = function($page) {

  $dates = [];

  $begin = new DateTime($page->startdate('Y-m-d'));
  $end = new DateTime($page->enddate('Y-m-d'));
  $end->modify('+1 day');
  $repeatOptions = ['1 day', '1 week', '2 weeks', '1 month'];
  $repeatValue = in_array($page->event_repeat(), $repeatOptions)? $page->event_repeat()->value(): '1 day';
  $interval = DateInterval::createFromDateString($repeatValue);

  // create an array of exceptions, these were entered manually into a structure field
  // to allow for public holidays, vacations etc.
  $exceptionDates = [];
  $exceptions = $page->exceptions()->toStructure();
  if($exceptions->count()) {
    $exceptionDates = $exceptions->pluck('date', ',');
  }
  // create a new DatePeriod object
  $period = new DatePeriod($begin, $interval, $end);

  //create date array for each child
  foreach ($period as $dt):
    $dates[] = $dt->getTimestamp();
  endforeach;
  // remove exceptions from the dates array
  $dates = array_diff($dates, $exceptionDates);

  return $dates;

}; 
1 Like

Thank you so much! That’s really helpful.

How would this work if the event started at say, 10pm one day but didn’t finish until 2am the next day, like a gig or something?. How would you repeat an event that does not start and end on the same day?

Hey @nilshoerrmann, did you ever get around to apply this solution to your project?

I just found this thread and have the exact same situation: The event calendar I’m working on needs a subpage for each event, so I store them as individual pages. I can’t seem to crack this repeat problem though:

So far, I have unsuccessfully tried it with the kirby-recurr plugin which has helped me output a list of repeating dates out of a structure field, but I struggle to feed the rest of the data back into that array and then generate the necessary subpages out of it.

For the other approach, so starting from “actual” pages (content folders) for the events, I am suspecting that I would need to clone the repeating events’ pages somehow but got stuck there :woman_shrugging:

I would be super thankful if you have any old code from your project that could help me figure this out! Unfortunately my php knowledge is not yet on the level I would need to turn the method @texnixe proposed into something useful.

Yes, we finished that project sucessfully but it’s a complex setup that I cannot share here publicly. But maybe this custom collection will give you an idea how to solve your issues. Be aware we also have a structure field to define exeptions – like canceled or moved recurrences – which might this a bit hard to read.

<?php

return function ($site) {
    $boundaries = [new DateTime('today'), new DateTime('+6 month')];

    $recurrings = $site
        ->find('kalender')
        ->children()
        ->filter(function ($child) {
            return $child->interval()->isNotEmpty();
        });

    $recurrences = new Pages();
    foreach ($recurrings as $recurring) {
        $start = $end = new DateTime($recurring->start());

        if ($recurring->end()->isNotEmpty()) {
            $end = new DateTime($recurring->end());
        }

        $duration = $start->diff($end);

        $interval = new DateInterval($recurring->interval());
        $endinterval = $boundaries[1];

        if ($recurring->endinterval()->isNotEmpty()) {
            $endinterval = $recurring->endinterval()->toDateTime();
        }

        // The end date is exclude thus we have to add an extra day to the end
        $period = new DatePeriod(
            $start,
            $interval,
            $endinterval->modify('+1 day')
        );

        $exceptions = $recurring
            ->exceptions()
            ->toStructure()
            ->filterBy('date', 'date >=', $boundaries[0]->format('Y-m-d'))
            ->filterBy('date', 'date <=', $boundaries[1]->format('Y-m-d'));

        foreach ($period as $occurrence) {
            $date = $occurrence->format('Y-m-d');

            // Ignore occurrences outside of current calendar boundaries
            if ($occurrence < $boundaries[0]) {
                continue;
            }

            // Check exceptions
            if ($exceptions->isNotEmpty()) {
                $skip = false;

                foreach ($exceptions as $exception) {
                    if ($exception->date()->value() === $date) {
                        if ($exception->change()->isNotEmpty()) {
                            // Apply date change
                            $occurrence = $exception->change()->toDateTime();
                        } else {
                            $skip = true;
                        }
                    }
                }

                // Ignore canceled occurrence
                if ($skip === true) {
                    continue;
                }
            }

            $content = $recurring->content()->toArray();
            $content['start'] = $date;
            $content['end'] = $occurrence->add($duration)->format('Y-m-d');

            $recurrences->append(
                $recurring->clone([
                    'slug' =>
                        Str::replace($date, '-', '') . '-' . $recurring->slug(),
                    'content' => $content
                ])
            );
        }
    }

    return $recurrences;
};

Editing is actually done on the frontend. To give you some context: