Skip to content

Instantly share code, notes, and snippets.

@edewaal97
Forked from seebz/iCal.php
Last active May 7, 2021
Embed
What would you like to do?
iCal PHP Parser
<?php
// SOURCE: https://gist.github.com/seebz/c00a38d9520e035a6a8c
class iCal
{
/**
* @var string
*/
public $title;
/**
* @var string
*/
public $description;
/**
* @var array
*/
public $events = array();
/**
* @var array
*/
protected $_eventsByDate;
public function __construct($content = null)
{
if ($content) {
$isUrl = strpos($content, 'http') === 0 && filter_var($content, FILTER_VALIDATE_URL);
$isFile = strpos($content, "\n") === false && file_exists($content);
if ($isUrl || $isFile) {
$content = file_get_contents($content);
}
$this->parse($content);
}
}
public function title()
{
return $this->summary;
}
public function description()
{
return $this->description;
}
public function events()
{
return $this->events;
}
public function eventsByDate()
{
if (! $this->_eventsByDate) {
$this->_eventsByDate = array();
$tmpEventsByDate = array();
foreach ($this->events() as $event) {
foreach ($event->occurrences() as $occurrence) {
$date = date('Y-m-d', $occurrence);
$newevent = clone $event;
$newevent->fixOccurringDate($occurrence);
// generate key for sorting
$key = strtotime($newevent->dateStart);
while(isset($tmpEventsByDate[$date][$key])) $key++;
$tmpEventsByDate[$date][$key] = $newevent;
}
}
// sort array
ksort($tmpEventsByDate);
foreach ($tmpEventsByDate as $date => $value) {
ksort($value);
$this->_eventsByDate[$date] = $value;
}
// prevent duplicates for edited dates in recurring events
foreach ($this->_eventsByDate as $dateKey => $date) {
foreach ($date as $event) {
if(!empty($event->recurrenceId)) {
$uid = $event->uid;
foreach ($date as $eventKey => $eventValue) {
if($eventValue->uid == $uid && (empty($eventValue->recurrenceId))) {
unset($this->_eventsByDate[$dateKey][$eventKey]);
}
}
}
}
}
}
return $this->_eventsByDate;
}
public function eventsByDateBetween($start, $end, int $limit=NULL)
{
if ((string) (int) $start !== (string) $start) {
$start = strtotime($start);
}
$start = date('Y-m-d', $start);
if ((string) (int) $end !== (string) $end) {
$end = strtotime($end);
}
$end = date('Y-m-d', $end);
$return = array();
foreach ($this->eventsByDate() as $date => $events) {
if ($start <= $date && $date < $end) {
if(empty($limit) || count($return) <= $limit) {
$return[$date] = $events;
}
}
if(!empty($limit) && count($return) >= $limit){
break;
}
}
return $return;
}
public function eventsByDateSince($start, int $limit=NULL)
{
if ((string) (int) $start !== (string) $start) {
$start = strtotime($start);
}
$start = date('Y-m-d', $start);
$return = array();
foreach ($this->eventsByDate() as $date => $events) {
if ($start <= $date) {
if(empty($limit) || count($return) <= $limit) {
$return[$date] = $events;
}
}
if(!empty($limit) && count($return) >= $limit){
break;
}
}
return $return;
}
public function eventsByDateUntil($end, int $limit=NULL)
{
if ((string) (int) $end !== (string) $end) {
$end = strtotime($end);
}
$start = date('Y-m-d');
$end = date('Y-m-d', $end);
$return = array();
foreach ($this->eventsByDate() as $date => $events) {
if ($start <= $date && $end >= $date) {
if(empty($limit) || count($return) <= $limit) {
$return[$date] = $events;
}
}
if(!empty($limit) && count($return) >= $limit){
break;
}
}
return $return;
}
public function parse($content)
{
$content = str_replace("\r\n ", '', $content);
// Title
preg_match('`^X-WR-CALNAME:(.*)$`m', $content, $m);
$this->title = $m ? trim($m[1]) : null;
// Description
preg_match('`^X-WR-CALDESC:(.*)$`m', $content, $m);
$this->description = $m ? trim($m[1]) : null;
// Events
preg_match_all('`BEGIN:VEVENT(.+)END:VEVENT`Us', $content, $m);
foreach ($m[0] as $c) {
$this->events[] = new iCal_Event($c);
}
return $this;
}
}
class iCal_Event
{
/**
* @var string
*/
public $uid;
/**
* @var string
*/
public $summary;
/**
* @var string
*/
public $description;
/**
* @var string
*/
public $dateStart;
/**
* @var string
*/
public $dateEnd;
/**
* @var string
*/
public $recurrenceId;
/**
* @var array
*/
public $exdate = array();
/**
* @var stdClass
*/
public $recurrence;
/**
* @var string
*/
public $location;
/**
* @var string
*/
public $status;
/**
* @var string
*/
public $created;
/**
* @var string
*/
public $updated;
/**
* @var integer
*/
protected $_timeStart;
/**
* @var integer
*/
protected $_timeEnd;
/**
* @var integer
*/
protected $_recurrenceId;
/**
* @var array
*/
protected $_occurrences;
public function __construct($content = null)
{
if ($content) {
$this->parse($content);
}
}
public function summary()
{
return $this->summary;
}
public function title()
{
return $this->summary;
}
public function description()
{
return $this->description;
}
public function occurrences()
{
if (empty($this->_occurrences)) {
$this->_occurrences = $this->_calculateOccurrences();
}
return $this->_occurrences;
}
public function duration()
{
// if ($this->_timeEnd) {
return $this->_timeEnd - $this->_timeStart;
// }
}
public function parse($content)
{
$content = str_replace("\r\n ", '', $content);
// UID
if (preg_match('`^UID:(.*)$`m', $content, $m))
$this->uid = trim($m[1]);
// Summary
if (preg_match('`^SUMMARY:(.*)$`m', $content, $m))
$this->summary = trim($m[1]);
// Description
if (preg_match('`^DESCRIPTION:(.*)$`m', $content, $m))
$this->description = trim($m[1]);
// Date start
if (preg_match('`^DTSTART(?:;.+)?:([0-9]+(T[0-9]+Z?)?)`m', $content, $m)) {
$this->_timeStart = strtotime($m[1]);
$this->dateStart = date('Y-m-d H:i:s', $this->_timeStart);
}
// Date end
if (preg_match('`^DTEND(?:;.+)?:([0-9]+(T[0-9]+Z?)?)`m', $content, $m)) {
$this->_timeEnd = strtotime($m[1]);
$this->dateEnd = date('Y-m-d H:i:s', $this->_timeEnd);
}
// Recurrence-Id
if (preg_match('`^RECURRENCE-ID(?:;.+)?:([0-9]+(T[0-9]+Z?)?)`m', $content, $m)) {
$this->_recurrenceId = strtotime($m[1]);
$this->recurrenceId = date('Y-m-d H:i:s', $this->_recurrenceId);
}
// Exdate
if (preg_match_all('`^EXDATE(;.+)?:([0-9]+(T[0-9]+Z?)?)`m', $content, $m)) {
foreach ($m[2] as $dates) {
$dates = explode(',', $dates);
foreach ($dates as $d) {
$this->exdate[] = date('Y-m-d', strtotime($d));
}
}
}
// Recurrence
if (preg_match('`^RRULE:(.*)`m', $content, $m)) {
$rules = (object) array();
$rule = trim($m[1]);
$rule = explode(';', $rule);
foreach ($rule as $r) {
list($key, $value) = explode('=', $r);
$rules->{ strtolower($key) } = $value;
}
if (isset($rules->until)) {
$rules->until = date('Y-m-d H:i:s', strtotime($rules->until));
}
if (isset($rules->count)) {
$rules->count = intval($rules->count);
}
if (isset($rules->interval)) {
$rules->interval = intval($rules->interval);
}
if (isset($rules->byday)) {
$rules->byday = explode(',', $rules->byday);
}
// Avoid infinite recurrences
if (! isset($rules->until) && ! isset($rules->count)) {
$rules->count = 500;
}
$this->recurrence = $rules;
}
// Location
if (preg_match('`^LOCATION:(.*)$`m', $content, $m))
$this->location = trim($m[1]);
// Status
if (preg_match('`^STATUS:(.*)$`m', $content, $m))
$this->status = trim($m[1]);
// Created
if (preg_match('`^CREATED:(.*)`m', $content, $m))
$this->created = date('Y-m-d H:i:s', strtotime(trim($m[1])));
// Updated
if (preg_match('`^LAST-MODIFIED:(.*)`m', $content, $m))
$this->updated = date('Y-m-d H:i:s', strtotime(trim($m[1])));
return $this;
}
public function isRecurrent()
{
return ! empty($this->recurrence);
}
public function fixOccurringDate($timestamp)
{
if($timestamp != $this->_timeStart) {
// calculate correct start & end date if not a repeating event
$duration = $this->duration();
// get date from occurrences
$timestampCalc = new DateTime();
$timestampCalc->setTimestamp($timestamp);
// make new startdate and start timestamp
$startCalc = new DateTime();
$startCalc->setTimestamp($this->_timeStart);
$startCalc->setDate($timestampCalc->format('Y'), $timestampCalc->format('m'), $timestampCalc->format('d'));
$this->_timeStart = $startCalc->getTimestamp();
$this->dateStart = date('Y-m-d H:i:s', $this->_timeStart);
// calculate end date and time with duration of original event.
$this->_timeEnd += - $this->_timeStart + $duration;
$this->dateEnd = date('Y-m-d H:i:s', $this->_timeEnd);
}
}
protected function _isExdate($date)
{
if ((string) (int) $date != $date) {
$date = strtotime($date);
}
$date = date('Y-m-d', $date);
return in_array($date, $this->exdate);
}
protected function _calculateOccurrences()
{
$occurrences = array($this->_timeStart);
if ($this->isRecurrent())
{
$freq = $this->recurrence->freq;
$count = isset($this->recurrence->count) ? $this->recurrence->count : null;
$until = isset($this->recurrence->until) ? strtotime($this->recurrence->until) : null;
$callbacks = array(
'YEARLY' => '_nextYearlyOccurrence',
'MONTHLY' => '_nextMonthlyOccurrence',
'WEEKLY' => '_nextWeeklyOccurrence',
'DAILY' => '_nextDailyOccurrence'
);
$callback = $callbacks[$freq];
$offset = $this->_timeStart;
$continue = $until ? ($offset < $until) : ($count > 1);
while ($continue) {
if(isset($occurrence)) {
if (! $this->_isExdate($occurrence)) {
$occurrences[] = $occurrence;
$count--;
}
}
$occurrence = $this->{$callback}($offset);
$offset = $occurrence;
$continue = $until ? ($offset < $until) : ($count > 1);
}
}
if ($this->_isExdate($occurrences[0])) {
unset($occurrences[0]);
$occurrences = array_values($occurrences);
}
return $occurrences;
}
protected function _nextYearlyOccurrence($offset)
{
$interval = isset($this->recurrence->interval)
? $this->recurrence->interval
: 1;
return strtotime("+{$interval} year", $offset);
}
protected function _nextMonthlyOccurrence($offset)
{
$dayname = array(
'MO' => 'monday',
'TU' => 'tuesday',
'WE' => 'wednesday',
'TH' => 'thursday',
'FR' => 'friday',
'SA' => 'saturday',
'SU' => 'sunday'
);
$interval = isset($this->recurrence->interval)
? $this->recurrence->interval
: 1;
// INTERVAL IS BY (COUNT)DAYNAME
if(isset($this->recurrence->byday)){
$dates = array();
foreach ($this->recurrence->byday as $pattern) {
$offsetDateTime = new DateTime();
$offsetDateTime->setTimestamp((int) $offset);
preg_match('`([-]?\d+)?(MO|TU|WE|TH|FR|SA|SU)`m', $pattern, $m);
$recurrenceOffset = (isset($m[1])) ? (int) $m[1] : 1;
$recurrenceDay = strtr($m[2], $dayname);
$forDateTime = clone $offsetDateTime;
for (
$month = (int) $offsetDateTime->format('Ym');
$month <= date('Ym', strtotime('+' . $interval*12 . ' months'));
$month = (int) $forDateTime->modify('+'.$interval.' months')->format('Ym')
) {
$yearMonth = $forDateTime->format('Y-m');
$firstDay = new DateTime('first '. $recurrenceDay . ' of ' . $yearMonth);
$lastDay = new DateTime('last '. $recurrenceDay . ' of ' . $yearMonth);
$newDate = $firstDay;
$daysInMonth = array();
while ($newDate->getTimestamp() <= $lastDay->getTimestamp()) {
$daysInMonth[] = $newDate->getTimestamp();
$newDate->modify('next '. $recurrenceDay);
}
if($recurrenceOffset < 0) {
$dates[] = $daysInMonth[count($daysInMonth) + $recurrenceOffset];
} else {
$dates[] = $daysInMonth[$recurrenceOffset - 1];
}
}
}
sort($dates);
foreach ($dates as $date) {
if ($date > $offset) {
return $date;
}
}
}
// INTERVAL IS BY DAYNUMBER OF MONTH
$bymonthday = isset($this->recurrence->bymonthday)
? explode(',', $this->recurrence->bymonthday)
: array(date('d', $offset));
$start = strtotime(date('Y-m-01 H:i:s', $offset));
$dates = array();
foreach ($bymonthday as $day) {
// this month
$dates[] = strtotime(($day-1) . ' day', $start);
// next 'interval' month
$tmp = strtotime("+{$interval} month", $start);
$time = strtotime(($day-1) . ' day', $tmp);
if ((string) (int) date('d', $time) == (int) $day) {
$dates[] = $time;
}
// 2x 'interval' month
$interval *= 2;
$tmp = strtotime("+{$interval} month", $start);
$time = strtotime(($day-1) . ' day', $tmp);
if ((string) (int) date('d', $time) === (int) $day) {
$dates[] = $time;
}
}
sort($dates);
foreach ($dates as $date) {
if ($date > $offset) {
return $date;
}
}
}
protected function _nextWeeklyOccurrence($offset)
{
$interval = isset($this->recurrence->interval)
? $this->recurrence->interval
: 1;
$byday = isset($this->recurrence->byday)
? $this->recurrence->byday
: array( substr(strtoupper(date('D', $offset)), 0, 2) );
$start = date('l', $offset) !== 'Monday'
? strtotime('last monday', $offset)
: $offset;
$daysname = array(
'MO' => 'monday',
'TU' => 'tuesday',
'WE' => 'wednesday',
'TH' => 'thursday',
'FR' => 'friday',
'SA' => 'saturday',
'SU' => 'sunday',
);
$dates = array();
foreach ($byday as $day) {
$dayname = $daysname[$day];
// this week
$dates[] = strtotime($dayname, $start);
// next 'interval' week
$tmp = strtotime("+{$interval} week", $start);
$time = strtotime($dayname, $tmp);
$dates[] = $time;
}
sort($dates);
foreach ($dates as $date) {
if ($date > $offset) {
return $date;
}
}
}
protected function _nextDailyOccurrence($offset)
{
$interval = isset($this->recurrence->interval)
? $this->recurrence->interval
: 1;
return strtotime("+{$interval} day", $offset);
}
}
<?php
include 'iCal.php';
$file = 'http://www.google.com/calendar/ical/ht3jlfaac5lfd6263ulfh4tql8%40group.calendar.google.com/public/basic.ics';
$iCal = new iCal($file);
$events = $iCal->eventsByDate();
// or :
// $events = $iCal->eventsByDateBetween('2014-01-01', '2015-01-01');
// or :
// $events = $iCal->eventsByDateSince('2014-01-01');
// or :
// $events = $iCal->eventsByDateSince('today');
// or :
// $events = $iCal->eventsByDateUntil('+30 days');
// in addition, every function starting with eventsBy... can be extended with another variable with a limit on the amount of events.
// for example:
// $events = $iCal->eventsByDateUntil('+30 days', 10);
foreach ($events as $date => $events)
{
echo $date . "\n";
echo '----------' . "\n";
foreach ($events as $event)
{
echo '* ' . $event->title() . "\n";
}
echo "\n";
}
@TCOA

This comment has been minimized.

Copy link

@TCOA TCOA commented Feb 13, 2020

Class works fine on example, but missing some code to handle the calendars I tested.....

Fixed one bug....

in fixOccurringDate($timestamp) you have
$this->_timeEnd - $this->_timeStart + $duration;
should be
$this->_timeEnd = $this->_timeEnd - $this->_timeStart + $duration;

Added needed features
in $callbacks, add to the array:
'DAILY' => '_nextDailyOccurrence'

also needs the function.....

protected function _nextDailyOccurrence($offset) { $interval = isset($this->recurrence->interval) ? $this->recurrence->interval : 1; return strtotime("+{$interval} day", $offset); }

and a needed function (IMHO - and the one I will likely use the most...) is

public function eventsByDateUntil($end) { if ((string)(int)$end !== (string)$end) { $end = strtotime($end); } $start = date('Y-m-d'); $end = date('Y-m-d', $end); $return = array(); foreach ($this->eventsByDate() as $date => $events) { if ($start <= $date && $end >= $date) { $return[$date] = $events; } } return $return; }å

with a useage of

$events = $iCal->eventsByDateUntil('+30 days');

Another feature that would be good is to add a $limit to each 'eventsBy....' function that would allow only returning X number of events.

So, for example, my function becomes:

public function eventsByDateUntil($end, $limit='') { if ((string)(int)$end !== (string)$end) { $end = strtotime($end); } $start = date('Y-m-d'); $end = date('Y-m-d', $end); $return = array(); foreach ($this->eventsByDate() as $date => $events) { if ($start <= $date && $end >= $date) { if(empty($limit) || count($return) <= $limit){ $return[$date] = $events; } } if(!empty($limit) && count($return) >= $limit){ break; } } return $return; }

So things like recurring birthdays don't get out of hand....... ;)

with a useage of

$events = $iCal->eventsByDateUntil('+30 days');
is still OK (returns everything within the next 30 days)
or
$events = $iCal->eventsByDateUntil('+30 days', 10);
limits the data to only the next 10 things happening within the next 30 days (good for "heads up" displays and such)

@edewaal97

This comment has been minimized.

Copy link
Owner Author

@edewaal97 edewaal97 commented Feb 21, 2020

@TCOA, good call. I didn't need it at the time but it's a good addition. I added your updates for future use. Thanks for the feedback!

@ThosG

This comment has been minimized.

Copy link

@ThosG ThosG commented Apr 18, 2020

@edewaal97 Thank you for this. Could you help me with usage? My php segment simply includes your file 'usage.php', which looks as though it should cause some output, but nothing happens.

@ThosG

This comment has been minimized.

Copy link

@ThosG ThosG commented Apr 18, 2020

Oh dear. I'm very sorry. I forgot to use the .php extension.

@cinnamon17

This comment has been minimized.

Copy link

@cinnamon17 cinnamon17 commented Sep 4, 2020

Is there some method to add a new event??

@edewaal97

This comment has been minimized.

Copy link
Owner Author

@edewaal97 edewaal97 commented Sep 5, 2020

@cinnamon17 Unfortunately that is not possible currently. You can add events by adding them to the calendar using the program for the source (Google calendar in this example). Otherwise you are free to fork it and code it yourself. Unfortunately there is no way to push the new event back to the source. That's a limitation of the api/ical specification.

@bitnulleins

This comment has been minimized.

Copy link

@bitnulleins bitnulleins commented Jan 9, 2021

@edewaal97 Correct regex in line 349 and 353 for utf-8 encoding. It requires a regex for form: SUMMATY ;CHARSET=utf-8 : ...
Here's my fork.

@sissad

This comment has been minimized.

Copy link

@sissad sissad commented Jan 25, 2021

Hi,
Thank you for this Script! Ist Very helpfull!
How can i diplay Start time of the event?
Thanks
Sissad

@wodim

This comment has been minimized.

Copy link

@wodim wodim commented Feb 14, 2021

@edewaal97 I think a nice addition for the iCal class would be a function that returns the current event, if any.

@mdhassanemam

This comment has been minimized.

Copy link

@mdhassanemam mdhassanemam commented Apr 26, 2021

@edewaal97 I am new on this i need the available dates not reserved date, I am trying to find the property by between two dates where property is available.

@philbert25

This comment has been minimized.

Copy link

@philbert25 philbert25 commented May 4, 2021

I would like to output something like this:
<li><a href="link to event in calendar">Title of Event - Date of Event using Month, Day format</a></li>
This would need to output the next 3 events from today forward.
So far I have been able to do it all except for the link and reformatting the date. Is this possible? Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment