1
0
mirror of https://github.com/rlanvin/php-rrule.git synced 2025-02-20 09:54:16 +01:00

First draft implementation for recurrence set

The new class RSet can contain multiple RRULE, RDATE, EXRULE and EXDATE.

Ref #7
This commit is contained in:
rlanvin 2016-03-15 23:31:57 +02:00
parent 9a5cbdf93e
commit 58a8475ba7
2 changed files with 376 additions and 0 deletions

317
src/RSet.php Executable file
View File

@ -0,0 +1,317 @@
<?php
/**
* Licensed under the MIT license.
*
* For the full copyright and license information, please view the LICENSE file.
*
* @author Rémi Lanvin <remi@cloudconnected.fr>
* @link https://github.com/rlanvin/php-rrule
*/
namespace RRule;
/**
* Recurrence set
*/
class RSet implements \Iterator, \ArrayAccess, \Countable
{
protected $rdates = array();
protected $rrules = array();
protected $exdates = array();
protected $exrules = array();
public function __construct()
{
}
public function addRRule($rrule)
{
if ( is_string($rrule) || is_array($rrule) ) {
$rrule = new RRule($rrule);
}
elseif ( ! $rrule instanceof \Iterator ) {
throw new \InvalidArgumentException('The rule must be a string, an array, an instance of RRule or an Iterator');
}
// cloning because I want to iterate it without being disturbed
$this->rrules[] = clone $rrule;
return $this;
}
/**
* In RFC 2445 but deprecated in RFC 5545
*/
public function addExRule($rrule)
{
if ( is_string($rrule) || is_array($rrule) ) {
$rrule = new RRule($rrule);
}
elseif ( ! $rrule instanceof \Iterator ) {
throw new \InvalidArgumentException('The rule must be a string, an array, an instance of RRule or an Iterator');
}
// cloning because I want to iterate it without being disturbed
$this->exrules[] = clone $rrule;
return $this;
}
public function addRDate($date)
{
try {
$this->rdates[] = RRule::parseDate($date);
} catch (\Exception $e) {
throw new \InvalidArgumentException(
'Failed to parse RDATE - it must be a valid date, timestamp or \DateTime object'
);
}
return $this;
}
public function addExDate($date)
{
try {
$this->exdates[] = RRule::parseDate($date);
} catch (\Exception $e) {
throw new \InvalidArgumentException(
'Failed to parse EXDATE - it must be a valid date, timestamp or \DateTime object'
);
}
return $this;
}
public function getOccurrences()
{
// TODO: need a wait to test the presence of infinite RRULE
$res = array();
foreach ( $this as $occurrence ) {
$res[] = $occurrence;
}
return $res;
}
///////////////////////////////////////////////////////////////////////////////
// Iterator interface
protected $current = 0;
protected $key = 0;
public function rewind()
{
$this->current = $this->iterate(true);
$this->key = 0;
}
public function current()
{
return $this->current;
}
public function key()
{
return $this->key;
}
public function next()
{
$this->current = $this->iterate();
$this->key += 1;
}
public function valid()
{
return $this->current !== null;
}
///////////////////////////////////////////////////////////////////////////////
// ArrayAccess interface
public function offsetExists($offset)
{
throw new \Exception(__METHOD__.' is unimplemented');
// return is_numeric($offset) && $offset >= 0 && $offset < count($this);
}
public function offsetGet($offset)
{
throw new \Exception(__METHOD__.' is unimplemented');
// if ( isset($this->cache[$offset]) ) {
// // found in cache
// return $this->cache[$offset];
// }
// elseif ( $this->total !== null ) {
// // cache complete and not found in cache
// return null;
// }
// // not in cache and cache not complete, we have to loop to find it
// $i = 0;
// foreach ( $this as $occurrence ) {
// if ( $i == $offset ) {
// return $occurrence;
// }
// $i++;
// if ( $i > $offset ) {
// break;
// }
// }
// return null;
}
public function offsetSet($offset, $value)
{
throw new \Exception(__METHOD__.' is unimplemented');
// throw new \LogicException('Setting a Date in a RRule is not supported');
}
public function offsetUnset($offset)
{
throw new \Exception(__METHOD__.' is unimplemented');
// throw new \LogicException('Unsetting a Date in a RRule is not supported');
}
///////////////////////////////////////////////////////////////////////////////
// Countable interface
/**
* Returns the number of recurrences in this set. It will have go
* through the whole recurrence, if this hasn't been done before, which
* introduces a performance penality.
* @return int
*/
public function count()
{
throw new \Exception(__METHOD__.' is unimplemented');
// if ( ! $this->count && ! $this->until ) {
// throw new \LogicException('Cannot count an infinite recurrence rule.');
// }
// if ( $this->total === null ) {
// foreach ( $this as $occurrence ) {}
// }
// return $this->total;
}
///////////////////////////////////////////////////////////////////////////////
// Private methods
private $_rlist = null;
private $_rlist_iterator = null;
private $_exlist = null;
private $_exlist_iterator = null;
private $_previous_occurrence = null;
private $_total = 0;
/**
* This method will iterate over a bunch of different iterators (rrules and arrays),
* keeping the results *in order*, while never attempting to merge or sort
* anything in memory. It can combine both finite and infinite rrule.
*
* What we need to do it to build two heaps: rlist and exlist
* Each heap contains multiple iterators (either RRule or ArrayIterator)
* At each step of the loop, it calls all of the iterators to generate a new item,
* and stores them in the heap, that keeps them in order.
*
* This is made slightly more complicated because this method is a generator.
*/
protected function iterate($reset = false)
{
if ( $reset ) {
$this->_rlist = $this->_rlist_iterator = null;
$this->_exlist = $this->_exlist_iterator = null;
$this->_previous_occurrence = null;
}
if ( $this->_rlist === null ) {
// rrules + rdate
$this->_rlist = new \SplMinHeap();
$this->_rlist_iterator = new \MultipleIterator(\MultipleIterator::MIT_NEED_ANY);
$this->_rlist_iterator->attachIterator(new \ArrayIterator($this->rdates));
foreach ( $this->rrules as $rrule ) {
$this->_rlist_iterator->attachIterator($rrule);
}
$this->_rlist_iterator->rewind();
// exrules + exdate
$this->_exlist = new \SplMinHeap();
$this->_exlist_iterator = new \MultipleIterator(\MultipleIterator::MIT_NEED_ANY);
$this->_exlist_iterator->attachIterator(new \ArrayIterator($this->exdates));
foreach ( $this->exrules as $rrule ) {
$this->_exlist_iterator->attachIterator($rrule);
}
$this->_exlist_iterator->rewind();
}
while ( true ) {
foreach ( $this->_rlist_iterator->current() as $date ) {
if ( $date !== null ) {
$this->_rlist->insert($date);
}
}
$this->_rlist_iterator->next(); // advance the iterator for the next call
if ( $this->_rlist->isEmpty() ) {
break; // exit the loop to stop the iterator
}
$occurrence = $this->_rlist->top();
$this->_rlist->extract(); // remove the occurence from the heap
if ( $occurrence == $this->_previous_occurrence ) {
continue; // skip, was already considered
}
// now we need to check against exlist
// we need to iterate exlist as long as it contains dates lower than occurrence
// (they will be discarded), and then check if the date is the same
// as occurence (in which case it is discarded)
$exclude = false;
while ( true ) {
foreach ( $this->_exlist_iterator->current() as $date ) {
if ( $date !== null ) {
$this->_exlist->insert($date);
}
}
$this->_exlist_iterator->next(); // advance the iterator for the next call
if ( $this->_exlist->isEmpty() ) {
break; // break this loop only
}
$exdate = $this->_exlist->top();
if ( $exdate < $occurrence ) {
$this->_exlist->extract();
continue;
}
elseif ( $exdate == $occurrence ) {
$exclude = true;
break 1;
}
else {
break 1; // exdate is > occurrence, so we'll keep it for later
}
}
$this->_previous_occurrence = $occurrence;
if ( $exclude ) {
continue;
}
$this->_total += 1;
return $occurrence; // = yield
}
$this->total = $this->_total; // save total for count cache
return null; // stop the iterator
}
}

59
tests/RSetTest.php Executable file
View File

@ -0,0 +1,59 @@
<?php
use RRule\RSet;
class RSetTest extends PHPUnit_Framework_TestCase
{
public function testAddRRule()
{
$rrset = new RSet();
$rrset->addRRule(array(
'FREQ' => 'YEARLY',
'COUNT' => 2,
'BYDAY' => 'TU',
'DTSTART' => date_create('1997-09-02 09:00')
));
$rrset->addRRule(array(
'FREQ' => 'YEARLY',
'COUNT' => 1,
'BYDAY' => 'TH',
'DTSTART' => date_create('1997-09-02 09:00')
));
$this->assertEquals(array(
date_create('1997-09-02 09:00'),
date_create('1997-09-04 09:00'),
date_create('1997-09-09 09:00')
), $rrset->getOccurrences());
}
public function testAddRDate()
{
}
public function testAddExRule()
{
}
public function testAddExDate()
{
$rrset = new RSet();
$rrset->addRRule(array(
'FREQ' => 'YEARLY',
'COUNT' => 6,
'BYDAY' => 'TU, TH',
'DTSTART' => date_create('1997-09-02 09:00')
));
$rrset->addExdate('1997-09-04 09:00:00');
$rrset->addExdate('1997-09-11 09:00:00');
$rrset->addExdate('1997-09-18 09:00:00');
$this->assertEquals(array(
date_create('1997-09-02 09:00'),
date_create('1997-09-09 09:00'),
date_create('1997-09-16 09:00')
), $rrset->getOccurrences());
}
}