1
0
mirror of https://github.com/rlanvin/php-rrule.git synced 2025-04-08 23:53:48 +02:00

Implementing BYSETPOS with freq > DAILY

Needs more tests
This commit is contained in:
rlanvin 2015-06-30 18:19:09 +03:00
parent 066a97d517
commit 66e56cd4f9
2 changed files with 105 additions and 45 deletions

View File

@ -482,6 +482,9 @@ class RRule implements \Iterator, \ArrayAccess
$this->bysecond = array(); $this->bysecond = array();
foreach ( $parts['BYSECOND'] as $value ) { foreach ( $parts['BYSECOND'] as $value ) {
// yes, "60" is a valid value, in (very rare) cases on leap seconds
// December 31, 2005 23:59:60 UTC is a valid date...
// so is 2012-06-30T23:59:60UTC
if ( $value < 0 || $value > 60 ) { if ( $value < 0 || $value > 60 ) {
throw new \InvalidArgumentException('Invalid BYSECOND value: '.$value); throw new \InvalidArgumentException('Invalid BYSECOND value: '.$value);
} }
@ -497,10 +500,11 @@ class RRule implements \Iterator, \ArrayAccess
} }
if ( $this->freq < self::HOURLY ) { if ( $this->freq < self::HOURLY ) {
// for frequencies DAILY, WEEKLY, MONTHLY AND YEARLY, we build // for frequencies DAILY, WEEKLY, MONTHLY AND YEARLY, we can build
// an array of every time of the day at which there should be an // an array of every time of the day at which there should be an
// occurrence - default, if no BYHOUR/BYMINUTE/BYSECOND are provided // occurrence - default, if no BYHOUR/BYMINUTE/BYSECOND are provided
// is only one time, and it's the DTSTART time. // is only one time, and it's the DTSTART time. This is a cached version
// if you will, since it'll never change at these frequencies
$this->timeset = array(); $this->timeset = array();
foreach ( $this->byhour as $hour ) { foreach ( $this->byhour as $hour ) {
foreach ( $this->byminute as $minute ) { foreach ( $this->byminute as $minute ) {
@ -510,7 +514,8 @@ class RRule implements \Iterator, \ArrayAccess
} }
} }
} }
sort($this->timeset); // FIXME sort ??
// sort($this->timeset);
} }
} }
@ -704,6 +709,7 @@ class RRule implements \Iterator, \ArrayAccess
break; break;
case self::SECONDLY: case self::SECONDLY:
$diff = $date->diff($this->dtstart); $diff = $date->diff($this->dtstart);
// XXX does not account for leap second (should it?)
$diff = $diff->s + $diff->i * 60 + $diff->h * 3600 + $diff->days * 86400; $diff = $diff->s + $diff->i * 60 + $diff->h * 3600 + $diff->days * 86400;
if ( $diff % $this->interval !== 0 ) { if ( $diff % $this->interval !== 0 ) {
return false; return false;
@ -1013,7 +1019,13 @@ class RRule implements \Iterator, \ArrayAccess
/** /**
* Not sure what it does yet * This builds an array of every time of the day that matches the BYXXX time
* criteria. It will only process $this->frequency at one time. So:
* - for HOURLY frequencies it builds the minutes and second of the given hour
* - for MINUTELY frequencies it builds the seconds of the given minute
* - for SECONDLY frequencies, it returns an array with one element
*
* This method is called everytime an increment of at least one hour is made.
*/ */
protected function getTimeSet($hour, $minute, $second) protected function getTimeSet($hour, $minute, $second)
{ {
@ -1070,7 +1082,8 @@ class RRule implements \Iterator, \ArrayAccess
* *
* Another quirk of this approach is that because the granularity is by day, * Another quirk of this approach is that because the granularity is by day,
* higher frequencies (hourly, minutely and secondly) have to have * higher frequencies (hourly, minutely and secondly) have to have
* their own special loops within the main loop. * their own special loops within the main loop, making the all thing quite
* convoluted.
* Moreover, at such frequencies, the brute-force approach starts to really * Moreover, at such frequencies, the brute-force approach starts to really
* suck. For example, a rule like * suck. For example, a rule like
* "Every minute, every Jan 1st between 10:00 and 10:59, for 10 years" * "Every minute, every Jan 1st between 10:00 and 10:59, for 10 years"
@ -1094,13 +1107,13 @@ class RRule implements \Iterator, \ArrayAccess
// at every call of the method (to emulate a generator) // at every call of the method (to emulate a generator)
static $year = null, $month = null, $day = null; static $year = null, $month = null, $day = null;
static $hour = null, $minute = null, $second = null; static $hour = null, $minute = null, $second = null;
static $current_set = null, $masks = null, $timeset = null; static $dayset = null, $masks = null, $timeset = null;
static $total = 0; static $total = 0;
if ( $reset ) { if ( $reset ) {
$year = $month = $day = null; $year = $month = $day = null;
$hour = $minute = $second = null; $hour = $minute = $second = null;
$current_set = $masks = $timeset = null; $dayset = $masks = $timeset = null;
$total = 0; $total = 0;
} }
@ -1128,8 +1141,7 @@ class RRule implements \Iterator, \ArrayAccess
$second = (int) $second; $second = (int) $second;
} }
// todo, not sure when this should be rebuilt // we initialize the timeset
// and not sure what this does anyway
if ( $timeset == null ) { if ( $timeset == null ) {
if ( $this->freq < self::HOURLY ) { if ( $this->freq < self::HOURLY ) {
// daily, weekly, monthly or yearly // daily, weekly, monthly or yearly
@ -1137,6 +1149,7 @@ class RRule implements \Iterator, \ArrayAccess
$timeset = $this->timeset; $timeset = $this->timeset;
} }
else { else {
// initialize empty if it's not going to occurs on the first iteration
if ( if (
($this->freq >= self::HOURLY && $this->byhour && ! in_array($hour, $this->byhour)) ($this->freq >= self::HOURLY && $this->byhour && ! in_array($hour, $this->byhour))
|| ($this->freq >= self::MINUTELY && $this->byminute && ! in_array($minute, $this->byminute)) || ($this->freq >= self::MINUTELY && $this->byminute && ! in_array($minute, $this->byminute))
@ -1149,8 +1162,6 @@ class RRule implements \Iterator, \ArrayAccess
} }
} }
} }
// echo json_encode($timeset),"\n";
// fgets(STDIN);
// while (true) { // while (true) {
$max_cycles = self::$REPEAT_CYCLES[$this->freq <= self::DAILY ? $this->freq : self::DAILY]; $max_cycles = self::$REPEAT_CYCLES[$this->freq <= self::DAILY ? $this->freq : self::DAILY];
@ -1158,7 +1169,7 @@ class RRule implements \Iterator, \ArrayAccess
// 1. get an array of all days in the next interval (day, month, week, etc.) // 1. get an array of all days in the next interval (day, month, week, etc.)
// we filter out from this array all days that do not match the BYXXX conditions // we filter out from this array all days that do not match the BYXXX conditions
// to speed things up, we use days of the year (day numbers) instead of date // to speed things up, we use days of the year (day numbers) instead of date
if ( $current_set === null ) { if ( $dayset === null ) {
// rebuild the various masks and converters // rebuild the various masks and converters
// these arrays will allow fast date operations // these arrays will allow fast date operations
// without relying on date() methods // without relying on date() methods
@ -1196,13 +1207,13 @@ class RRule implements \Iterator, \ArrayAccess
} }
// calculate the current set // calculate the current set
$current_set = $this->getDaySet($year, $month, $day, $masks); $dayset = $this->getDaySet($year, $month, $day, $masks);
// echo"\tWorking with $year-$month-$day set=".json_encode($current_set)."\n"; // echo"\tWorking with $year-$month-$day set=".json_encode($dayset)."\n";
// print_r(json_encode($masks)); // print_r(json_encode($masks));
// fgets(STDIN); // fgets(STDIN);
$filtered_set = array(); $filtered_set = array();
foreach ( $current_set as $yearday ) { foreach ( $dayset as $yearday ) {
if ( $this->bymonth && ! in_array($masks['yearday_to_month'][$yearday], $this->bymonth) ) { if ( $this->bymonth && ! in_array($masks['yearday_to_month'][$yearday], $this->bymonth) ) {
continue; continue;
} }
@ -1242,51 +1253,91 @@ class RRule implements \Iterator, \ArrayAccess
} }
// echo "\tFiltered set (before BYSETPOS)=".json_encode($filtered_set)."\n"; // echo "\tFiltered set (before BYSETPOS)=".json_encode($filtered_set)."\n";
$current_set = $filtered_set; $dayset = $filtered_set;
// XXX this needs to be applied after expanding the timeset // if BYSETPOS is set, we need to expand the timeset to filter by pos
if ( $this->bysetpos ) { // so we make a special loop to return while generating
if ( $this->bysetpos && $timeset ) {
$filtered_set = array(); $filtered_set = array();
$n = sizeof($current_set);
foreach ( $this->bysetpos as $pos ) { foreach ( $this->bysetpos as $pos ) {
// echo "pos = $pos => ";
$n = count($timeset);
if ( $pos < 0 ) { if ( $pos < 0 ) {
$pos = $n + $pos; $pos = $n * count($dayset) + $pos;
} }
else { else {
$pos = $pos - 1; $pos = $pos - 1;
} }
if ( isset($current_set[$pos]) ) { // echo "$pos => ";
$filtered_set[] = $current_set[$pos]; $div = (int) ($pos / $n); // daypos
$mod = $pos % $n; // timepos
// echo "array index $div / $mod\n";
// echo "div = $div, mod = $mod\n";
// echo "dayset[$div] = ",$dayset[$div]," timeset[$mod] = ",json_encode($timeset[$mod]),"\n";
// fgets(STDIN);
if ( isset($dayset[$div]) && isset($timeset[$mod]) ) {
$yearday = $dayset[$div];
$time = $timeset[$mod];
// used as array key to ensure uniqueness
$tmp = $year.':'.$yearday.':'.$time[0].':'.$time[1].':'.$time[2];
if ( ! isset($filtered_set[$tmp]) ) {
$occurrence = \DateTime::createFromFormat(
'Y z',
"$year $yearday"
);
$occurrence->setTime($time[0], $time[1], $time[2]);
$filtered_set[$tmp] = $occurrence;
}
} }
} }
$current_set = array_unique($filtered_set); sort($filtered_set);
$dayset = $filtered_set;
} }
// echo "\tFiltered set (after BYSETPOS)=".json_encode($filtered_set)."\n"; // echo "\tFiltered set (after BYSETPOS)=".json_encode($filtered_set)."\n";
} }
// 2. loop, generate a valid date, and return the result (fake "yield") // 2. loop, generate a valid date, and return the result (fake "yield")
// at the same time, we check the end condition and return null if // at the same time, we check the end condition and return null if
// we need to stop // we need to stop
while ( ($yearday = current($current_set)) !== false ) { if ( $this->bysetpos && $timeset ) {
$occurrence = \DateTime::createFromFormat('Y z', "$year $yearday"); while ( ($occurrence = current($dayset)) !== false ) {
while ( ($time = current($timeset)) !== false ) {
$occurrence->setTime($time[0], $time[1], $time[2]);
// consider end conditions // consider end conditions
if ( $this->until && $occurrence > $this->until ) { if ( $this->until && $occurrence > $this->until ) {
// $this->length = $total (?) // $this->length = $total (?)
return null; return null;
} }
next($timeset); next($dayset);
if ( $occurrence >= $this->dtstart ) { // ignore occurrences before DTSTART if ( $occurrence >= $this->dtstart ) { // ignore occurrences before DTSTART
$total += 1; $total += 1;
return $occurrence; // yield return $occurrence; // yield
} }
} }
reset($timeset); }
next($current_set); else {
// normal loop, without BYSETPOS
while ( ($yearday = current($dayset)) !== false ) {
$occurrence = \DateTime::createFromFormat('Y z', "$year $yearday");
while ( ($time = current($timeset)) !== false ) {
$occurrence->setTime($time[0], $time[1], $time[2]);
// consider end conditions
if ( $this->until && $occurrence > $this->until ) {
// $this->length = $total (?)
return null;
}
next($timeset);
if ( $occurrence >= $this->dtstart ) { // ignore occurrences before DTSTART
$total += 1;
return $occurrence; // yield
}
}
reset($timeset);
next($dayset);
}
} }
// 3. we reset the loop to the next interval // 3. we reset the loop to the next interval
@ -1327,7 +1378,7 @@ class RRule implements \Iterator, \ArrayAccess
// call the DateTime method at the very end. // call the DateTime method at the very end.
case self::HOURLY: case self::HOURLY:
if ( empty($current_set) ) { if ( empty($dayset) ) {
// an empty set means that this day has been filtered out // an empty set means that this day has been filtered out
// by one of the BYXXX rule. So there is no need to // by one of the BYXXX rule. So there is no need to
// examine it any further, we know nothing is going to // examine it any further, we know nothing is going to
@ -1358,7 +1409,7 @@ class RRule implements \Iterator, \ArrayAccess
$timeset = $this->getTimeSet($hour, $minute, $second); $timeset = $this->getTimeSet($hour, $minute, $second);
break; break;
case self::MINUTELY: case self::MINUTELY:
if ( empty($current_set) ) { if ( empty($dayset) ) {
$minute += ((int) ((1439 - ($hour*60+$minute)) / $this->interval)) * $this->interval; $minute += ((int) ((1439 - ($hour*60+$minute)) / $this->interval)) * $this->interval;
} }
@ -1391,7 +1442,7 @@ class RRule implements \Iterator, \ArrayAccess
$timeset = $this->getTimeSet($hour, $minute, $second); $timeset = $this->getTimeSet($hour, $minute, $second);
break; break;
case self::SECONDLY: case self::SECONDLY:
if ( empty($current_set) ) { if ( empty($dayset) ) {
$second += ((int) ((86399 - ($hour*3600 + $minute*60 + $second)) / $this->interval)) * $this->interval; $second += ((int) ((86399 - ($hour*3600 + $minute*60 + $second)) / $this->interval)) * $this->interval;
} }
@ -1435,7 +1486,7 @@ class RRule implements \Iterator, \ArrayAccess
if ( $days_increment ) { if ( $days_increment ) {
list($year,$month,$day) = explode('-',date_create("$year-$month-$day")->modify("+ $days_increment days")->format('Y-n-j')); list($year,$month,$day) = explode('-',date_create("$year-$month-$day")->modify("+ $days_increment days")->format('Y-n-j'));
} }
$current_set = null; // reset the loop $dayset = null; // reset the loop
} }
return null; // stop the iterator return null; // stop the iterator
} }

View File

@ -112,7 +112,7 @@ class RRuleTest extends PHPUnit_Framework_TestCase
array(array('BYWEEKNO' => 53, 'BYDAY' => 'MO'), array( array(array('BYWEEKNO' => 53, 'BYDAY' => 'MO'), array(
date_create('1998-12-28'), date_create('2004-12-27'), date_create('2009-12-28'))), date_create('1998-12-28'), date_create('2004-12-27'), date_create('2009-12-28'))),
// TODO BYSETPOS // todo bysetpos
array(array('BYHOUR' => array(6, 18)),array( array(array('BYHOUR' => array(6, 18)),array(
date_create('1997-09-02 06:00:00'), date_create('1997-09-02 06:00:00'),
@ -142,10 +142,10 @@ class RRuleTest extends PHPUnit_Framework_TestCase
date_create('1997-09-02 06:15:10'), date_create('1997-09-02 06:15:10'),
date_create('1997-09-02 06:15:20'), date_create('1997-09-02 06:15:20'),
date_create('1997-09-02 06:30:10'))), date_create('1997-09-02 06:30:10'))),
// array(array('BYMONTHDAY'=>15,'BYHOUR'=>array(6, 18),'BYSETPOS'=>array(3, -3)),array( array(array('BYMONTHDAY'=>15,'BYHOUR'=>array(6, 18),'BYSETPOS'=>array(3, -3)),array(
// date_create('1997-11-15 18:00:00'), date_create('1997-11-15 18:00:00'),
// date_create('1998-02-15 06:00:00'), date_create('1998-02-15 06:00:00'),
// date_create('1998-11-15 18:00:00'))) date_create('1998-11-15 18:00:00')))
); );
} }
@ -204,7 +204,11 @@ class RRuleTest extends PHPUnit_Framework_TestCase
array(array('BYMONTH' => array(1, 3), 'BYMONTHDAY' => array(1, 3), 'BYDAY' => array('TU', 'TH')),array( array(array('BYMONTH' => array(1, 3), 'BYMONTHDAY' => array(1, 3), 'BYDAY' => array('TU', 'TH')),array(
date_create('1998-01-01'),date_create('1998-03-03'),date_create('2001-03-01'))), date_create('1998-01-01'),date_create('1998-03-03'),date_create('2001-03-01'))),
// TODO BYSETPOS // last workday of the month
array(array('BYDAY'=>'MO,TU,WE,TH,FR','BYSETPOS'=>-1), array(
date_create('1997-09-30'),
date_create('1997-10-31'),
date_create('1997-11-28'))),
array(array('BYHOUR'=> array(6, 18)),array( array(array('BYHOUR'=> array(6, 18)),array(
date_create('1997-09-02 06:00:00'),date_create('1997-09-02 18:00:00'),date_create('1997-10-02 06:00:00'))), date_create('1997-09-02 06:00:00'),date_create('1997-09-02 18:00:00'),date_create('1997-10-02 06:00:00'))),
@ -220,8 +224,13 @@ class RRuleTest extends PHPUnit_Framework_TestCase
date_create('1997-09-02 00:06:06'),date_create('1997-09-02 00:06:18'),date_create('1997-09-02 00:18:06'))), date_create('1997-09-02 00:06:06'),date_create('1997-09-02 00:06:18'),date_create('1997-09-02 00:18:06'))),
array(array('BYHOUR'=>array(6, 18),'BYMINUTE'=>array(6, 18),'BYSECOND'=>array(6, 18)),array( array(array('BYHOUR'=>array(6, 18),'BYMINUTE'=>array(6, 18),'BYSECOND'=>array(6, 18)),array(
date_create('1997-09-02 06:06:06'),date_create('1997-09-02 06:06:18'),date_create('1997-09-02 06:18:06'))), date_create('1997-09-02 06:06:06'),date_create('1997-09-02 06:06:18'),date_create('1997-09-02 06:18:06'))),
// array(array('BYMONTHDAY'=>array(13, 17),'BYHOUR'=>array(6, 18),'BYSETPOS'=>array(3, -3)),array( array(array('BYMONTHDAY'=>array(13, 17),'BYHOUR'=>array(6, 18),'BYSETPOS'=>array(3, -3)),array(
// date_create('1997-09-13 06:00'),date_create('1997-09-17'),date_create('1997-10-13'))) date_create('1997-09-13 18:00'),date_create('1997-09-17 06:00'),date_create('1997-10-13 18:00'))),
// avoid duplicates
array(array('BYMONTHDAY'=>array(13, 17),'BYHOUR'=>array(6, 18),'BYSETPOS'=>array(3, 3, -3)),array(
date_create('1997-09-13 18:00'),date_create('1997-09-17 06:00'),date_create('1997-10-13 18:00'))),
array(array('BYMONTHDAY'=>array(13, 17),'BYHOUR'=>array(6, 18),'BYSETPOS'=>array(4, -1)),array(
date_create('1997-09-17 18:00'),date_create('1997-10-17 18:00'),date_create('1997-11-17 18:00')))
); );
} }
@ -1300,7 +1309,7 @@ class RRuleTest extends PHPUnit_Framework_TestCase
array(array( array(array(
'freq' => 'monthly', 'freq' => 'monthly',
'interval' => 2, 'interval' => 2,
'bymonth' => [1,3,5,7,9,11], 'bymonth' => '1,3,5,7,9,11',
'dtstart' => '1997-02-02 09:00:00', 'dtstart' => '1997-02-02 09:00:00',
'count' => 1 'count' => 1
)), )),
@ -1406,7 +1415,7 @@ class RRuleTest extends PHPUnit_Framework_TestCase
array( array(
array('freq' => 'hourly', 'dtstart' => '1999-09-02 09:00:00', 'INTERVAL' => 2), array('freq' => 'hourly', 'dtstart' => '1999-09-02 09:00:00', 'INTERVAL' => 2),
array('1999-09-02 10:00:00', '1999-09-02 12:00:00') array('1999-09-02 10:00:00', '1999-09-02 09:01:01','1999-09-02 12:00:00')
), ),
array( array(
array('freq' => 'hourly', 'dtstart' => '1999-09-02 09:00:00', 'INTERVAL' => 5), array('freq' => 'hourly', 'dtstart' => '1999-09-02 09:00:00', 'INTERVAL' => 5),