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

Rewrite the core algorithm to use a generator

Drop compatibility with PHP < 5.6
Ref #43
This commit is contained in:
rlanvin 2019-01-13 10:00:43 +00:00
parent 1373df401e
commit 7c93c0e48a
8 changed files with 145 additions and 240 deletions

View File

@ -1,15 +1,9 @@
language: php
php:
- 5.4
- 5.5
- 5.6
- 7.0
- 7.1
- 7.2
matrix:
include:
- php: 5.3
dist: precise
install:
- composer install -n
script:

View File

@ -1,8 +1,8 @@
# Changelog
## Unreleased
## [Unreleased]
- n/a
- Rewrite the core algorithm to use a native PHP generator, drop compability with PHP < 5.6 [#43](https://github.com/rlanvin/php-rrule/issues/43)
## [1.6.3] - 2019-01-13

View File

@ -32,7 +32,7 @@ Complete documentation and more examples are available in [the wiki](https://git
## Requirements
- PHP >= 5.3
- PHP >= 5.6
- [intl extension](http://php.net/manual/en/book.intl.php) is recommended for `humanReadable()` but not strictly required
## Installation
@ -41,12 +41,12 @@ The recommended way is to install the lib [through Composer](http://getcomposer.
Simply run `composer require rlanvin/php-rrule` for it to be automatically installed and included in your `composer.json`.
Alternatively, just add this to your `composer.json` file and then run `composer install` (you can replace `1.*` by any version selector, or even `dev-master` for the latest development version).
Alternatively, just add this to your `composer.json` file and then run `composer install` (you can replace `2.*` by any version selector, or even `dev-master` for the latest development version).
```JSON
{
"require": {
"rlanvin/php-rrule": "1.*"
"rlanvin/php-rrule": "2.*"
}
}
```

View File

@ -6,7 +6,7 @@
"homepage": "https://github.com/rlanvin/php-rrule",
"license": "MIT",
"require": {
"php": ">=5.3.0"
"php": ">=5.6.0"
},
"suggest": {
"ext-intl": "Intl extension is needed for humanReadable()"

View File

@ -997,52 +997,52 @@ class RRule implements RRuleInterface
// Note: if cache is complete, we could probably avoid completely calling iterate()
// and instead iterate directly on the $this->cache array
/** @internal */
protected $current = 0;
/** @internal */
protected $key = 0;
// /** @internal */
// protected $current = 0;
// /** @internal */
// protected $key = 0;
/**
* @internal
*/
public function rewind()
{
$this->current = $this->iterate(true);
$this->key = 0;
}
// /**
// * @internal
// */
// public function rewind()
// {
// $this->current = $this->iterate(true);
// $this->key = 0;
// }
/**
* @internal
*/
public function current()
{
return $this->current;
}
// /**
// * @internal
// */
// public function current()
// {
// return $this->current;
// }
/**
* @internal
*/
public function key()
{
return $this->key;
}
// /**
// * @internal
// */
// public function key()
// {
// return $this->key;
// }
/**
* @internal
*/
public function next()
{
$this->current = $this->iterate();
$this->key += 1;
}
// /**
// * @internal
// */
// public function next()
// {
// $this->current = $this->iterate();
// $this->key += 1;
// }
/**
* @internal
*/
public function valid()
{
return $this->current !== null;
}
// /**
// * @internal
// */
// public function valid()
// {
// return $this->current !== null;
// }
///////////////////////////////////////////////////////////////////////////////
// ArrayAccess interface
@ -1445,44 +1445,6 @@ class RRule implements RRuleInterface
}
}
// Variables for iterate() method, that will persist to allow iterate()
// to resume where it stopped. For PHP >= 5.5, these would be local variables
// inside a generator method using yield. However since we are compatible with
// PHP 5.3 and 5.4, they have to be implemented this way.
//
// The original implementation used static local variables inside the class
// method, which I think was cleaner scope-wise, but sadly this didn't work
// when multiple instances of RRule existed and are iterated at the same time
// (such as in a ruleset)
//
// DO NOT USE OUTSIDE OF iterate()
/** @internal */
private $_year = null;
/** @internal */
private $_month = null;
/** @internal */
private $_day = null;
/** @internal */
private $_hour = null;
/** @internal */
private $_minute = null;
/** @internal */
private $_second = null;
/** @internal */
private $_dayset = null;
/** @internal */
private $_masks = null;
/** @internal */
private $_timeset = null;
/** @internal */
private $_dtstart = null;
/** @internal */
private $_total = 0;
/** @internal */
private $_use_cache = true;
/**
* This is the main method, where all of the magic happens.
*
@ -1531,115 +1493,78 @@ class RRule implements RRuleInterface
* @param $reset (bool) Whether to restart the iteration, or keep going
* @return \DateTime|null
*/
protected function iterate($reset = false)
public function getIterator()
{
// for readability's sake, and because scope of the variables should be local anyway
$year = & $this->_year;
$month = & $this->_month;
$day = & $this->_day;
$hour = & $this->_hour;
$minute = & $this->_minute;
$second = & $this->_second;
$dayset = & $this->_dayset;
$masks = & $this->_masks;
$timeset = & $this->_timeset;
$dtstart = & $this->_dtstart;
$total = & $this->_total;
$use_cache = & $this->_use_cache;
if ( $reset ) {
$this->_year = $this->_month = $this->_day = null;
$this->_hour = $this->_minute = $this->_second = null;
$this->_dayset = $this->_masks = $this->_timeset = null;
$this->_dtstart = null;
$this->_total = 0;
$this->_use_cache = true;
reset($this->cache);
}
$total = 0;
$occurrence = null;
$dtstart = null;
$dayset = null;
// go through the cache first
if ( $use_cache ) {
while ( ($occurrence = current($this->cache)) !== false ) {
// echo "Cache hit\n";
$dtstart = $occurrence;
next($this->cache);
$total += 1;
return clone $occurrence; // since DateTime is not immutable, avoid any problem
}
reset($this->cache);
// now set use_cache to false to skip the all thing on next iteration
// and start filling the cache instead
$use_cache = false;
// if the cache as been used up completely and we now there is nothing else
if ( $total === $this->total ) {
// echo "Cache used up, nothing else to compute\n";
return null;
}
// echo "Cache used up with occurrences remaining\n";
if ( $dtstart ) {
$dtstart = clone $dtstart; // since DateTime is not immutable, avoid any problem
// so we skip the last occurrence of the cache
if ( $this->freq === self::SECONDLY ) {
$dtstart->modify('+'.$this->interval.'second');
}
else {
$dtstart->modify('+1second');
}
}
foreach ( $this->cache as $occurrence ) {
yield clone $occurrence; // since DateTime is not immutable, avoid any problem
$total += 1;
}
// stop once $total has reached COUNT
if ( $this->count && $total >= $this->count ) {
$this->total = $total;
return null;
// if the cache as been used up completely and we now there is nothing else,
// we can stop the generator
if ( $total === $this->total ) {
return; // end generator
}
if ( $occurrence ) {
$dtstart = clone $occurrence; // since DateTime is not immutable, clone to avoid any problem
// so we skip the last occurrence of the cache
if ( $this->freq === self::SECONDLY ) {
$dtstart->modify('+'.$this->interval.'second');
}
else {
$dtstart->modify('+1second');
}
}
if ( $dtstart === null ) {
$dtstart = clone $this->dtstart;
}
if ( $year === null ) {
if ( $this->freq === self::WEEKLY ) {
// we align the start date to the WKST, so we can then
// simply loop by adding +7 days. The Python lib does some
// calculation magic at the end of the loop (when incrementing)
// to realign on first pass.
$tmp = clone $dtstart;
$tmp->modify('-'.pymod($dtstart->format('N') - $this->wkst,7).'days');
list($year,$month,$day,$hour,$minute,$second) = explode(' ',$tmp->format('Y n j G i s'));
unset($tmp);
}
else {
list($year,$month,$day,$hour,$minute,$second) = explode(' ',$dtstart->format('Y n j G i s'));
}
// remove leading zeros
$minute = (int) $minute;
$second = (int) $second;
if ( $this->freq === self::WEEKLY ) {
// we align the start date to the WKST, so we can then
// simply loop by adding +7 days. The Python lib does some
// calculation magic at the end of the loop (when incrementing)
// to realign on first pass.
$tmp = clone $dtstart;
$tmp->modify('-'.pymod($dtstart->format('N') - $this->wkst,7).'days');
list($year,$month,$day,$hour,$minute,$second) = explode(' ',$tmp->format('Y n j G i s'));
unset($tmp);
}
else {
list($year,$month,$day,$hour,$minute,$second) = explode(' ',$dtstart->format('Y n j G i s'));
}
// remove leading zeros
$minute = (int) $minute;
$second = (int) $second;
// we initialize the timeset
if ( $timeset == null ) {
if ( $this->freq < self::HOURLY ) {
// daily, weekly, monthly or yearly
// we don't need to calculate a new timeset
$timeset = $this->timeset;
if ( $this->freq < self::HOURLY ) {
// daily, weekly, monthly or yearly
// we don't need to calculate a new timeset
$timeset = $this->timeset;
}
else {
// initialize empty if it's not going to occurs on the first iteration
if (
($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::SECONDLY && $this->bysecond && ! in_array($second, $this->bysecond))
) {
$timeset = array();
}
else {
// initialize empty if it's not going to occurs on the first iteration
if (
($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::SECONDLY && $this->bysecond && ! in_array($second, $this->bysecond))
) {
$timeset = array();
}
else {
$timeset = $this->getTimeSet($hour, $minute, $second);
}
$timeset = $this->getTimeSet($hour, $minute, $second);
}
}
// while (true) {
$max_cycles = self::$REPEAT_CYCLES[$this->freq <= self::DAILY ? $this->freq : self::DAILY];
for ( $i = 0; $i < $max_cycles; $i++ ) {
// 1. get an array of all days in the next interval (day, month, week, etc.)
@ -1687,7 +1612,7 @@ class RRule implements RRuleInterface
$filtered_set = array();
// filter out the days based on the BY*** rules
// filter out the days based on the BYXXX rules
foreach ( $dayset as $yearday ) {
if ( $this->bymonth && ! in_array($masks['yearday_to_month'][$yearday], $this->bymonth) ) {
continue;
@ -1729,6 +1654,8 @@ class RRule implements RRuleInterface
// if BYSETPOS is set, we need to expand the timeset to filter by pos
// so we make a special loop to return while generating
// TODO this is not needed with a generator anymore
// we can yield directly within the loop
if ( $this->bysetpos && $timeset ) {
$filtered_set = array();
foreach ( $this->bysetpos as $pos ) {
@ -1767,48 +1694,58 @@ class RRule implements RRuleInterface
// at the same time, we check the end condition and return null if
// we need to stop
if ( $this->bysetpos && $timeset ) {
while ( ($occurrence = current($dayset)) !== false ) {
// while ( ($occurrence = current($dayset)) !== false ) {
foreach ( $dayset as $occurrence ) {
// consider end conditions
if ( $this->until && $occurrence > $this->until ) {
$this->total = $total; // save total for count() cache
return null;
return;
}
next($dayset);
// next($dayset);
if ( $occurrence >= $dtstart ) { // ignore occurrences before DTSTART
if ( $this->count && $total >= $this->count ) {
$this->total = $total;
return;
}
$total += 1;
$this->cache[] = $occurrence;
return clone $occurrence; // yield
$this->cache[] = clone $occurrence;
yield clone $occurrence; // yield
}
}
}
else {
// normal loop, without BYSETPOS
while ( ($yearday = current($dayset)) !== false ) {
// while ( ($yearday = current($dayset)) !== false ) {
foreach ( $dayset as $yearday ) {
$occurrence = \DateTime::createFromFormat(
'Y z',
"$year $yearday",
$this->dtstart->getTimezone()
);
while ( ($time = current($timeset)) !== false ) {
// while ( ($time = current($timeset)) !== false ) {
foreach ( $timeset as $time ) {
$occurrence->setTime($time[0], $time[1], $time[2]);
// consider end conditions
if ( $this->until && $occurrence > $this->until ) {
$this->total = $total; // save total for count() cache
return null;
return;
}
next($timeset);
// next($timeset);
if ( $occurrence >= $dtstart ) { // ignore occurrences before DTSTART
if ( $this->count && $total >= $this->count ) {
$this->total = $total;
return;
}
$total += 1;
$this->cache[] = $occurrence;
return clone $occurrence; // yield
$this->cache[] = clone $occurrence;
yield clone $occurrence; // yield
}
}
reset($timeset);
next($dayset);
// reset($timeset);
// next($dayset);
}
}
@ -1876,7 +1813,7 @@ class RRule implements RRuleInterface
if ( ! $found ) {
$this->total = $total; // save total for count cache
return null; // stop the iterator
return; // stop the iterator
}
$timeset = $this->getTimeSet($hour, $minute, $second);
@ -1910,7 +1847,7 @@ class RRule implements RRuleInterface
if ( ! $found ) {
$this->total = $total; // save total for count cache
return null; // stop the iterator
return; // stop the iterator
}
$timeset = $this->getTimeSet($hour, $minute, $second);
@ -1951,7 +1888,7 @@ class RRule implements RRuleInterface
if ( ! $found ) {
$this->total = $total; // save total for count cache
return null; // stop the iterator
return; // stop the iterator
}
$timeset = $this->getTimeSet($hour, $minute, $second);
@ -1965,7 +1902,7 @@ class RRule implements RRuleInterface
}
$this->total = $total; // save total for count cache
return null; // stop the iterator
return; // stop the iterator
}
///////////////////////////////////////////////////////////////////////////////

View File

@ -14,7 +14,7 @@ namespace RRule;
/**
* Common interface for RRule and RSet objects
*/
interface RRuleInterface extends \Iterator, \ArrayAccess, \Countable
interface RRuleInterface extends \ArrayAccess, \Countable, \IteratorAggregate
{
/**
* Return all the occurrences in an array of \DateTime.

View File

@ -585,15 +585,6 @@ class RSet implements RRuleInterface
protected $exlist_heap = null;
protected $exlist_iterator = null;
// local variables for iterate() (see comment in RRule about that)
/** @internal */
private $_previous_occurrence = null;
/** @internal */
private $_total = 0;
/** @internal */
private $_use_cache = 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
@ -609,34 +600,15 @@ class RSet implements RRuleInterface
* @param $reset (bool) Whether to restart the iteration, or keep going
* @return \DateTime|null
*/
protected function iterate($reset = false)
public function getIterator()
{
$previous_occurrence = & $this->_previous_occurrence;
$total = & $this->_total;
$use_cache = & $this->_use_cache;
$previous_occurrence = null;
$total = 0;
if ( $reset ) {
$this->_previous_occurrence = null;
$this->_total = 0;
$this->_use_cache = true;
reset($this->cache);
}
foreach ( $this->cache as $occurrence ) {
yield clone $occurrence; // since DateTime is not immutable, avoid any problem
// go through the cache first
if ( $use_cache ) {
while ( ($occurrence = current($this->cache)) !== false ) {
next($this->cache);
$total += 1;
return clone $occurrence;
}
reset($this->cache);
// now set use_cache to false to skip the all thing on next iteration
// and start filling the cache instead
$use_cache = false;
// if the cache as been used up completely and we now there is nothing else
if ( $total === $this->total ) {
return null;
}
$total += 1;
}
if ( $this->rlist_heap === null ) {
@ -645,7 +617,7 @@ class RSet implements RRuleInterface
$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->attachIterator($rrule->getIterator());
}
$this->rlist_iterator->rewind();
@ -655,7 +627,7 @@ class RSet implements RRuleInterface
$this->exlist_iterator->attachIterator(new \ArrayIterator($this->exdates));
foreach ( $this->exrules as $rrule ) {
$this->exlist_iterator->attachIterator($rrule);
$this->exlist_iterator->attachIterator($rrule->getIterator());
}
$this->exlist_iterator->rewind();
}
@ -717,11 +689,11 @@ class RSet implements RRuleInterface
}
$total += 1;
$this->cache[] = $occurrence;
return clone $occurrence; // = yield
$this->cache[] = clone $occurrence;
yield clone $occurrence; // = yield
}
$this->total = $total; // save total for count cache
return null; // stop the iterator
return; // stop the iterator
}
}

View File

@ -190,6 +190,8 @@ class RSetTest extends TestCase
'BYDAY' => 'TU, TH',
'DTSTART' => date_create('1997-09-02 09:00')
));
$this->assertEquals(6, count($rset));
$rset->addExdate('1997-09-04 09:00:00');
$rset->addExdate('1997-09-11 09:00:00');
$rset->addExdate('1997-09-18 09:00:00');