Okay, after abandoning the Itertator idea (for reasons I won't go into right now) I've dug into the code from phpicalendar. Their overall idea is to take an rrule and return an array of timestamps. As of 5 minutes ago, I have a solution working in my project where I've
heavily borrowed from their code.
I still have the two database tables, Events and EventExceptions. I now store the rrule in the Events table, but use PHP to calculate all the recurrences of that event. In my case, the framework I've been writing gives me an Events class to do this in, so I just have to write a function for that class.
Right now, I'm only supporting a small subset of the RRULE in the ical spec. Credit where credit's due: this code is from phpicalendar's ical_parser. With a bunch of the stuff I don't need stripped out, and modified to work with my database, instead of ical files.
I'm happy with this solution so far, and am moving on to adding this to my Events class in the framework. And then on to tying it into my views. Hopefully this is helpful to you as well.
The PHP code clocks in over 200 lines
Code: Select all
create table events (
id int unsigned not null primary key auto_increment,
startDate date not null,
startTime time,
endDate date,
endTime time,
allDayEvent tinyint(1) unsigned,
rrule varchar(128),
summary varchar(128) not null,
description text,
calendar_id int unsigned not null,
location_id int unsigned,
user_id int unsigned not null,
foreign key (calendar_id) references calendars(id),
foreign key (location_id) references locations(id),
foreign key (user_id) references users(id)
) engine=InnoDB;
create table event_exceptions (
event_id int unsigned not null,
original_start date not null,
start datetime not null,
end datetime not null,
primary key (event_id,original_start),
foreign key (event_id) references events(id)
) engine=InnoDB;
Code: Select all
$event_startDate = '2006-1-1';
$event_startTime = '08:00:00';
$event_endDate = '2006-4-1';
$event_endTime = '17:00:00';
$range_start = strtotime('2006-2-1');
$range_end = strtotime('2006-2-15');
$rrule = "FREQ=WEEKLY;INTERVAL=1;BYDAY=WE";
$recur_data = getRecursionData($event_startDate,$event_endDate,$event_startTime,$event_endTime,$range_start,$range_end,$rrule);
function getRecursionData ($startDate,$endDate=null,$startTime=null,$endTime=null,$start_range_time,$end_range_time,$rrule)
{
$start_date_time = strtotime("$startDate $startTime");
$end_date_time = $endDate ? strtotime("$endDate $endTime") : $end_range_time;
# We need an array of all the RRULES
$recur_data = array();
$rrule_array = array();
$rrule = explode(';',$rrule);
foreach($rrule as $rule)
{
list($key,$value) = explode('=',$rule);
switch ($key)
{
case 'FREQ':
switch ($value)
{
case 'YEARLY': $freq_type = 'year'; break;
case 'MONTHLY': $freq_type = 'month'; break;
case 'WEEKLY': $freq_type = 'week'; break;
case 'DAILY': $freq_type = 'day'; break;
default:
}
break;
case 'INTERVAL':
$interval = $value;
break;
case 'BYDAY':
$byday = split(',',$value);
break;
case 'BYMONTHDAY':
$bymonthday = split(',',$value);
break;
case 'BYMONTH':
$bymonth = split(',',$value);
break;
}
}
// If the $end_range_time is less than the $start_date_time, or $start_range_time is greater
// than $end_date_time, we may as well forget the whole thing
// It doesn't do us any good to spend time adding data we aren't even looking at
// this will prevent the year view from taking way longer than it needs to
if ($end_range_time >= $start_date_time && $start_range_time <= $end_date_time)
{
if ($start_range_time < $start_date_time) { $start_range_time = $start_date_time; }
if ($end_range_time > $end_date_time) { $end_range_time = $end_date_time; }
$next_range_time = $start_range_time;
while($next_range_time <= $end_range_time)
{
# What is the interval between $next_range_time and $start_date_time
$compare = $freq_type.'Compare';
$diff = $compare($next_range_time,$start_date_time);
if ($diff % $interval == 0)
{
switch ($freq_type)
{
case 'day':
$recur_data[] = $next_range_time;
break;
case 'week':
# Get the sunday of the current week
$d = date('w',$next_range_time);
$sunday = strtotime("-$d days",$next_range_time);
if (!isset($byday))
{
# Use the day of the week from $start_date_time
$byday = array(strtoupper(substr(date('l',$start_date_time),0,2)));
}
foreach($byday as $day)
{
# strtotime needs at least three letters for the day
# RRULES only use two letters
$next_date_time = strtotime(two2threeCharDays($day),$sunday+(12*60*60));
# Reset the $next_range_time to the first instance of the week
if ($next_date_time < $next_range_time) { $next_range_time = $next_date_time; }
$recur_data[] = $next_date_time;
}
break;
case 'month':
if (!isset($bymonth)) { $bymonth = array(1,2,3,4,5,6,7,8,9,10,11,12); }
# Go to the first day of the month
$d = getdate($next_range_time);
$next_range_time = strtotime("$d[year]-$d[mon]-1");
if (isset($bymonthday))
{
foreach($bymonthday as $day)
{
# Convert the negative monthdays into the actual daynum
if ($day < 0) { $day = date('t',$next_range_time) + $day + 1; }
if (checkdate($d['mon'],$day,$d['year']))
{
$recur_data[] = mktime(0,0,0,$d['mon'],$day,$d['year']);
}
}
}
elseif (isset($byday))
{
foreach($byday as $day)
{
$nth = substr($day,0,-2);
$day = substr($day,-2);
$day_num = two2threeCharDays($day,false);
$day = two2threeCharDays($day);
if ($nth < 0)
{
$last_day = date('t',$next_range_time);
$next_range_time = strtotime(date("Y-m-$last_day",$next_range_time));
$last = (date('w',$next_range_time) == $day_num) ? '' : 'last ';
$next_range_time = strtotime("$last$day",$next_range_time) - $nth * 604800;
$month = date('m',$next_range_time);
if (in_array(date('m',$next_range_time),$bymonth))
{
$recur_data[] = $next_range_time;
}
# Reset to the start of the month
$next_range_time = strtotime(date('Y-m-1',$next_range_time));
}
else
{
$next_date_time = strtotime($day,$next_range_time) + $nth * 604800;
if (in_array(date('m',$next_date_time),$bymonth))
{
$recur_data[] = $next_date_time;
}
}
}
}
break;
}
}
$next_range_time = strtotime("+$interval $freq_type",$next_range_time);
}
}
# Recur_data now has dates set.
# We still need to add Time information from the event
$recurringEvents = array();
foreach($recur_data as $key=>$recurrance)
{
$date = date('Y-m-d',$recurrance);
# Here's where you'd want to swap in an exception for this date
# If there's a matching event_id and $date in the EventsExceptions table
$recurringEvents[$date]['start'] = strtotime("$date $startTime");
$recurringEvents[$date]['end'] = strtotime("$date $endTime");
}
return $recurringEvents;
}
# Returns the number of weeks between two timestamps
function weekCompare($now, $then)
{
# Get the timestamps for the sundays
$d = date('w',$now);
$sunday_now = strtotime("-$d days",$now);
$d = date('w',$then);
$sunday_then = strtotime("-$d days",$then);
return round(($sunday_now - $sunday_then)/(60*60*24*7));
}
# Returns the nuimber of days between two timestamps
function dayCompare($now, $then) { return round(((($now-$then)/60)/60)/24); }
# Returns the number of months between two datse
function monthCompare($now, $then)
{
$now = getdate($now);
$then = getdate($then);
$years = $now['year'] - $then['year'];
$months = $now['mon'] - $then['mon'];
if ($now['mon'] < $then['mon'])
{
$years--;
$months = ($months + 12) % 12;
}
return ($years * 12) + $months;
}
// takes iCalendar 2 day format and makes it into 3 characters
// if $txt is true, it returns the 3 letters, otherwise it returns the
// integer of that day; 0=Sun, 1=Mon, etc.
function two2threeCharDays($day, $txt=true)
{
switch($day)
{
case 'SU': return ($txt ? 'sun' : '0');
case 'MO': return ($txt ? 'mon' : '1');
case 'TU': return ($txt ? 'tue' : '2');
case 'WE': return ($txt ? 'wed' : '3');
case 'TH': return ($txt ? 'thu' : '4');
case 'FR': return ($txt ? 'fri' : '5');
case 'SA': return ($txt ? 'sat' : '6');
}
}