From 3035e0c1b60a283ffe7527624b4aa85355900a10 Mon Sep 17 00:00:00 2001 From: Andrew McMillan Date: Tue, 16 Feb 2010 12:33:23 +1300 Subject: [PATCH] New RepeatRule - mostly working. --- inc/RRule-v2.php | 191 ++++++++++++++++++++++++++------------ testing/test-RRULE-v2.php | 33 ++++--- 2 files changed, 153 insertions(+), 71 deletions(-) diff --git a/inc/RRule-v2.php b/inc/RRule-v2.php index 8e226688..73e79b17 100644 --- a/inc/RRule-v2.php +++ b/inc/RRule-v2.php @@ -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; diff --git a/testing/test-RRULE-v2.php b/testing/test-RRULE-v2.php index 2d16fd3e..80d3bb48 100755 --- a/testing/test-RRULE-v2.php +++ b/testing/test-RRULE-v2.php @@ -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 }