New RepeatRule - mostly working.

This commit is contained in:
Andrew McMillan 2010-02-16 12:33:23 +13:00
parent d2d6bf137a
commit 3035e0c1b6
2 changed files with 153 additions and 71 deletions

View File

@ -26,6 +26,8 @@ $rrule_expand_limit = array(
'byday' => 'limit', 'byhour' => 'limit', 'byminute' => 'limit', 'bysecond' => 'limit' ),
);
$rrule_day_numbers = array( 'SU' => 0, 'MO' => 1, 'TU' => 2, 'WE' => 3, 'TH' => 4, 'FR' => 5, 'SA' => 6 );
$GLOBALS['debug_rrule'] = false;
// $GLOBALS['debug_rrule'] = true;
@ -146,7 +148,9 @@ class RepeatRule {
if ( $this->finished ) return;
$got_more = false;
while( !$this->finished && !$got_more ) {
$loop_limit = 10;
$loops = 0;
while( !$this->finished && !$got_more && $loops++ < $loop_limit ) {
if ( !isset($this->current_base) ) {
$this->current_base = clone($this->base);
}
@ -157,6 +161,7 @@ class RepeatRule {
$this->current_set = array( clone($this->current_base) );
foreach( $rrule_expand_limit[$this->freq] AS $bytype => $action ) {
if ( isset($this->{$bytype}) ) $this->{$action.'_'.$bytype}();
if ( !isset($this->current_set[0]) ) break;
}
sort($this->current_set);
if ( isset($this->bysetpos) ) $this->limit_bysetpos();
@ -206,7 +211,9 @@ class RepeatRule {
$this->current_set = array();
foreach( $instances AS $k => $instance ) {
foreach( $this->bymonth AS $k => $month ) {
$this->current_set[] = $this->date_mask( clone($instance), null, $month, null, null, null, null);
$expanded = $this->date_mask( clone($instance), null, $month, null, null, null, null);
if ( $GLOBALS['debug_rrule'] ) printf( "Expanded BYMONTH $month into date %s\n", $expanded->format('c') );
$this->current_set[] = $expanded;
}
}
}
@ -221,71 +228,139 @@ class RepeatRule {
}
}
private function expand_byday_in_week( $day_in_week ) {
global $rrule_day_numbers;
/**
* @TODO: This should really allow for WKST, since if we start a series
* on (eg.) TH and interval > 1, a MO, TU, FR repeat will not be in the
* same week with this code.
*/
$dow_of_instance = $day_in_week->format('w'); // 0 == Sunday
foreach( $this->byday AS $k => $weekday ) {
$dow = $rrule_day_numbers[$weekday];
$offset = $dow - $dow_of_instance;
if ( $offset < 0 ) $offset += 7;
$expanded = clone($day_in_week);
$expanded->modify( sprintf('+%d day', $offset) );
$this->current_set[] = $expanded;
if ( $GLOBALS['debug_rrule'] ) printf( "Expanded BYDAY(W) $weekday into date %s\n", $expanded->format('c') );
}
}
private function expand_byday_in_month( $day_in_month ) {
global $rrule_day_numbers;
$first_of_month = $this->date_mask( clone($day_in_month), null, null, 1, null, null, null);
$dow_of_first = $first_of_month->format('w'); // 0 == Sunday
$days_in_month = cal_days_in_month(CAL_GREGORIAN, $first_of_month->format('m'), $first_of_month->format('Y'));
foreach( $this->byday AS $k => $weekday ) {
if ( preg_match('{([+-])?(\d)?(MO|TU|WE|TH|FR|SA|SU)}', $weekday, $matches ) ) {
$dow = $rrule_day_numbers[$matches[3]];
$first_dom = 1 + $dow - $dow_of_first; if ( $first_dom < 1 ) $first_dom +=7; // e.g. 1st=WE, dow=MO => 1+1-3=-1 => MO is 6th, etc.
$whichweek = intval($matches[2]);
if ( $GLOBALS['debug_rrule'] ) printf( "Expanding BYDAY(M) $weekday in month of %s\n", $instance->format('c') );
if ( $whichweek > 0 ) {
$whichweek--;
$monthday = $first_dom;
if ( $matches[1] == '-' ) {
$monthday += 35;
while( $monthday > $days_in_month ) $monthday -= 7;
$monthday -= (7 * $whichweek);
}
else {
$monthday += (7 * $whichweek);
}
if ( $monthday > 0 && $monthday <= $days_in_month ) {
$expanded = $this->date_mask( clone($day_in_month), null, null, $monthday, null, null, null);
if ( $GLOBALS['debug_rrule'] ) printf( "Expanded BYDAY(M) $weekday now $monthday into date %s\n", $expanded->format('c') );
$this->current_set[] = $expanded;
}
}
else {
for( $monthday = $first_dom; $monthday <= $days_in_month; $monthday += 7 ) {
$expanded = $this->date_mask( clone($day_in_month), null, null, $monthday, null, null, null);
if ( $GLOBALS['debug_rrule'] ) printf( "Expanded BYDAY(M) $weekday now $monthday into date %s\n", $expanded->format('c') );
$this->current_set[] = $expanded;
}
}
}
}
}
private function expand_byday_in_year( $day_in_year ) {
global $rrule_day_numbers;
$first_of_year = $this->date_mask( clone($day_in_year), null, 1, 1, null, null, null);
$dow_of_first = $first_of_year->format('w'); // 0 == Sunday
$days_in_year = 337 + cal_days_in_month(CAL_GREGORIAN, 2, $first_of_year->format('Y'));
foreach( $this->byday AS $k => $weekday ) {
if ( preg_match('{([+-])?(\d)?(MO|TU|WE|TH|FR|SA|SU)}', $weekday, $matches ) ) {
$expanded = clone($first_of_year);
$dow = $rrule_day_numbers[$matches[3]];
$first_doy = 1 + $dow - $dow_of_first; if ( $first_doy < 1 ) $first_doy +=7; // e.g. 1st=WE, dow=MO => 1+1-3=-1 => MO is 6th, etc.
$whichweek = intval($matches[2]);
if ( $GLOBALS['debug_rrule'] ) printf( "Expanding BYDAY(Y) $weekday from date %s\n", $instance->format('c') );
if ( $whichweek > 0 ) {
$whichweek--;
$yearday = $first_doy;
if ( $matches[1] == '-' ) {
$yearday += 371;
while( $yearday > $days_in_year ) $yearday -= 7;
$yearday -= (7 * $whichweek);
}
else {
$yearday += (7 * $whichweek);
}
if ( $yearday > 0 && $yearday <= $days_in_year ) {
$expanded->modify(sprintf('+%d day', $yearday - 1));
if ( $GLOBALS['debug_rrule'] ) printf( "Expanded BYDAY(Y) $weekday now $yearday into date %s\n", $expanded->format('c') );
$this->current_set[] = $expanded;
}
}
else {
$expanded->modify(sprintf('+%d day', $first_doy - 1));
for( $yearday = $first_doy; $yearday <= $days_in_year; $yearday += 7 ) {
if ( $GLOBALS['debug_rrule'] ) printf( "Expanded BYDAY(Y) $weekday now $yearday into date %s\n", $expanded->format('c') );
$this->current_set[] = clone($expanded);
$expanded->modify('+1 week');
}
}
}
}
}
private function expand_byday() {
if ( $this->freq == 'MONTHLY' ) {
if ( isset($this->bymonthday) ) {
$this->limit_byday(); /** Per RFC5545 3.3.10 from note 1 to table */
if ( !isset($this->current_set[0]) ) return;
if ( $this->freq == 'MONTHLY' || $this->freq == 'YEARLY' ) {
if ( isset($this->bymonthday) || isset($this->byyearday) ) {
$this->limit_byday(); /** Per RFC5545 3.3.10 from note 1&2 to table */
return;
}
$first_of_month = $this->date_mask( clone($this->current_set[0]), null, null, 1, null, null, null);
$dow_of_first = $first_of_month->format('w'); // 0 == Sunday
$days_in_month = cal_days_in_month(CAL_GREGORIAN, $first_of_month->format('m'), $first_of_month->format('Y'));
}
$instances = $this->current_set;
$this->current_set = array();
foreach( $instances AS $k => $instance ) {
if ( $this->freq == 'MONTHLY' ) {
foreach( $this->byday AS $k => $weekday ) {
if ( preg_match('{([+-])?(\d)?(MO|TU|WE|TH|FR|SA|SU)}', $weekday, $matches ) ) {
$dow = (strpos('**SUMOTUWETHFRSA', $matches[3]) / 2) - 1;
$first_dom = 1 + $dow - $dow_of_first; if ( $first_dom < 1 ) $first_dom +=7; // e.g. 1st=WE, dow=MO => 1+1-3=-1 => MO is 6th, etc.
$whichweek = intval($matches[2]);
if ( $GLOBALS['debug_rrule'] ) printf( "Expanding MONTHLY $weekday from date %s\n", $instance->format('c') );
if ( $whichweek > 0 ) {
$whichweek--;
$monthday = $first_dom;
if ( $matches[1] == '-' ) {
$monthday += 35;
while( $monthday > $days_in_month ) $monthday -= 7;
$monthday -= (7 * $whichweek);
}
else {
$monthday += (7 * $whichweek);
}
if ( $monthday > 0 && $monthday <= $days_in_month ) {
$expanded = $this->date_mask( clone($instance), null, null, $monthday, null, null, null);
if ( $GLOBALS['debug_rrule'] ) printf( "Expanded MONTHLY $weekday now $monthday into date %s\n", $expanded->format('c') );
$this->current_set[] = $expanded;
}
}
else {
for( $monthday = $first_dom; $monthday <= $days_in_month; $monthday += 7 ) {
$expanded = $this->date_mask( clone($instance), null, null, $monthday, null, null, null);
if ( $GLOBALS['debug_rrule'] ) printf( "Expanded MONTHLY $weekday now $monthday into date %s\n", $expanded->format('c') );
$this->current_set[] = $expanded;
}
}
}
}
$this->expand_byday_in_month($instance);
}
else if ( $this->freq == 'WEEKLY' ) {
/**
* @TODO: This should really allow for WKST, since if we start a series
* on (eg.) TH and interval > 1, a MO, TU, FR repeat will not be in the
* same week with this code.
*/
$dow_of_instance = $instance->format('w'); // 0 == Sunday
foreach( $this->byday AS $k => $weekday ) {
$dow = (strpos('**SUMOTUWETHFRSA', $weekday) / 2) - 1;
$offset = $dow - $dow_of_instance;
if ( $offset < 0 ) $offset += 7;
$this_expand = clone($instance);
$this_expand->modify( sprintf('+%d day', $offset) );
$this->current_set[] = $this_expand;
if ( $GLOBALS['debug_rrule'] ) printf( "Expanded WEEKLY $weekday into date %s\n", $this_expand->format('c') );
}
$this->expand_byday_in_week($instance);
}
else { // YEARLY
if ( isset($this->bymonth) ) {
$this->expand_byday_in_month($instance);
}
else if ( isset($this->byweekno) ) {
$this->expand_byday_in_week($instance);
}
else {
$this->expand_byday_in_year($instance);
}
}
}
@ -327,18 +402,20 @@ class RepeatRule {
$this->current_set = array();
foreach( $instances AS $k => $instance ) {
foreach( $this->{$element_name} AS $k => $element_value ) {
/* if ( $GLOBALS['debug_rrule'] ) */ printf( "Limiting '$fmt_char' on '%s' => '%s' ?=? '%s' \n", $instance->format('c'), $instance->format($fmt_char), $element_value );
if ( $GLOBALS['debug_rrule'] ) printf( "Limiting '$fmt_char' on '%s' => '%s' ?=? '%s' \n", $instance->format('c'), $instance->format($fmt_char), $element_value );
if ( $instance->format($fmt_char) == $element_value ) $this->current_set[] = $instance;
}
}
}
private function limit_byday() {
global $rrule_day_numbers;
$fmt_char = 'w';
$instances = $this->current_set;
$this->current_set = array();
foreach( $this->byday AS $k => $weekday ) {
$dow = (strpos('**SUMOTUWETHFRSA', $weekday) / 2) - 1;
$dow = $rrule_day_numbers[$weekday];
foreach( $instances AS $k => $instance ) {
if ( $GLOBALS['debug_rrule'] ) printf( "Limiting '$fmt_char' on '%s' => '%s' ?=? '%s' (%d) \n", $instance->format('c'), $instance->format($fmt_char), $weekday, $dow );
if ( $instance->format($fmt_char) == $dow ) $this->current_set[] = $instance;

View File

@ -24,6 +24,8 @@ class RRuleTest {
var $recur;
var $description;
var $result_description;
var $PHP_time;
var $SQL_time;
function __construct( $description, $start, $recur, $result_description = null ) {
$this->description = $description;
@ -35,6 +37,7 @@ class RRuleTest {
function PHPTest() {
$result = '';
$start = microtime(true);
$rule = new RepeatRule( $this->dtstart, $this->recur );
$i = 0;
while( $date = $rule->next() ) {
@ -42,12 +45,14 @@ class RRuleTest {
$result .= " " . $date->format('Y-m-d H:i:s');
if ( $i >= $this->result_limit ) break;
}
$this->PHP_time = microtime(true) - $start;
return $result;
}
function SQLTest() {
$result = '';
$sql = "SELECT event_instances::timestamp AS event_date FROM event_instances(?,?) LIMIT ".$this->result_limit;
$start = microtime(true);
$qry = new AwlQuery($sql, $this->dtstart, $this->recur);
// printf( "%s\n", $qry->querystring);
if ( $qry->Exec("test") && $qry->rows() > 0 ) {
@ -57,6 +62,7 @@ class RRuleTest {
$result .= " " . $row->event_date;
}
}
$this->SQL_time = microtime(true) - $start;
return $result;
}
}
@ -71,17 +77,17 @@ $tests = array(
, new RRuleTest( "Monthly forever", "20061104T073000", "RRULE:FREQ=MONTHLY" )
, new RRuleTest( "Monthly, on the 1st monday, 2nd wednesday, 3rd friday and last sunday, forever", "20061117T073000", "RRULE:FREQ=MONTHLY;BYDAY=1MO,2WE,3FR,-1SU" )
, new RRuleTest( "The working days of each month", "20061107T113000", "RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;UNTIL=20070101T000000" )
, new RRuleTest( "The last working day of each month", "20061107T113000", "RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1" )
, new RRuleTest( "Every working day", "20081020T103000", "RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR" )
, new RRuleTest( "Every working day", "20081020T110000", "RRULE:FREQ=DAILY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR" )
// , new RRuleTest( "1st Tuesday, 2nd Wednesday, 3rd Thursday & 4th Friday, every March, June, September, October and December", "20081001T133000", "RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=1TU,2WE,3TH,4FR;BYMONTH=3,6,9,10,12" )
// , new RRuleTest( "Every tuesday and friday", "20081017T084500", "RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=TU,FR" )
// , new RRuleTest( "Every tuesday and friday", "20081017T084500", "RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=TU,FR" )
// , new RRuleTest( "Every tuesday and friday", "20081017T084500", "RRULE:FREQ=DAILY;INTERVAL=1;BYDAY=TU,FR" )
// , new RRuleTest( "Time zone 1", "19700315T030000", "FREQ=YEARLY;INTERVAL=1;BYDAY=3SU;BYMONTH=3" )
// , new RRuleTest( "Time zone 2", "19700927T020000", "FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=9" )
// , new RRuleTest( "Time zone 3", "19810329T030000", "FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU" )
// , new RRuleTest( "Time zone 4", "20000404T020000", "FREQ=YEARLY;BYDAY=1SU;BYMONTH=4" )
, new RRuleTest( "The last working day of each month", "20061107T113000", "RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1;COUNT=30" )
, new RRuleTest( "Every working day", "20081020T103000", "RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;COUNT=30" )
, new RRuleTest( "Every working day", "20081020T110000", "RRULE:FREQ=DAILY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR;COUNT=30" )
//**SQL is wrong , new RRuleTest( "1st Tuesday, 2nd Wednesday, 3rd Thursday & 4th Friday, every March, June, September, October and December", "20081001T133000", "RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=1TU,2WE,3TH,4FR;BYMONTH=3,6,9,10,12" )
, new RRuleTest( "Every tuesday and friday", "20081017T084500", "RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=TU,FR;COUNT=30" )
, new RRuleTest( "Every tuesday and friday", "20081017T084500", "RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=TU,FR;COUNT=30" )
, new RRuleTest( "Every tuesday and friday", "20081017T084500", "RRULE:FREQ=DAILY;INTERVAL=1;BYDAY=TU,FR;COUNT=30" )
, new RRuleTest( "Time zone 1", "19700315T030000", "FREQ=YEARLY;INTERVAL=1;BYDAY=3SU;BYMONTH=3" )
, new RRuleTest( "Time zone 2", "19700927T020000", "FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=9" )
, new RRuleTest( "Time zone 3", "19810329T030000", "FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU" )
, new RRuleTest( "Time zone 4", "20000404T010000", "FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;COUNT=15" )
);
foreach( $tests AS $k => $test ) {
@ -91,11 +97,10 @@ foreach( $tests AS $k => $test ) {
$php_result = $test->PHPTest();
$sql_result = $test->SQLTest();
if ( $php_result == $sql_result ) {
echo "PHP & SQL results are identical (-:";
echo "$php_result\n";
printf( 'PHP & SQL results are identical (-: P: %6.4lf & S: %6.4lf'."\n", $test->PHP_time, $test->SQL_time);
}
else {
echo "PHP & SQL results differ :-(\n";
printf( 'PHP & SQL results differ :-( P: %6.4lf & S: %6.4lf'."\n", $test->PHP_time, $test->SQL_time);
echo "PHP Result:\n$php_result\n\n";
echo "SQL Result:\n$sql_result\n\n"; // Still under development
}