From d2384c1997b6777d1ff58a6a6a175e71a9c0fd83 Mon Sep 17 00:00:00 2001 From: rlanvin Date: Mon, 21 Mar 2016 22:43:38 +0200 Subject: [PATCH] Adding interface to make RSet behave like RRule Recurrence rules and a recurrence sets now share the same interface. Also added isFinite() and isInfinite() methods, and improved the implementation of RSet (ref #7) --- src/RRule.php | 31 +++++++++-- src/RRuleInterface.php | 30 +++++++++++ src/RSet.php | 83 +++++++++++++++++++++++++--- tests/RRuleTest.php | 45 ++++++++++++---- tests/RSetTest.php | 119 ++++++++++++++++++++++++++++++++++++----- 5 files changed, 274 insertions(+), 34 deletions(-) create mode 100755 src/RRuleInterface.php diff --git a/src/RRule.php b/src/RRule.php index 30ffce8..cf35b05 100755 --- a/src/RRule.php +++ b/src/RRule.php @@ -83,7 +83,7 @@ function is_leap_year($year) * @see https://tools.ietf.org/html/rfc5545 * @see https://labix.org/python-dateutil */ -class RRule implements \Iterator, \ArrayAccess, \Countable +class RRule implements RRuleInterface { const SECONDLY = 7; const MINUTELY = 6; @@ -628,6 +628,29 @@ class RRule implements \Iterator, \ArrayAccess, \Countable return $this; } +/////////////////////////////////////////////////////////////////////////////// +// RRule interface + + /** + * Return true if the rrule has an end condition, false otherwise + * + * @return bool + */ + public function isFinite() + { + return $this->count || $this->until; + } + + /** + * Return true if the rrule has no end condition (infite) + * + * @return bool + */ + public function isInfinite() + { + return ! $this->count && ! $this->until; + } + /** * Return all the occurrences in an array. * @@ -637,7 +660,7 @@ class RRule implements \Iterator, \ArrayAccess, \Countable */ public function getOccurrences() { - if ( ! $this->count && ! $this->until ) { + if ( $this->isInfinite() ) { throw new \LogicException('Cannot get all occurrences of an infinite recurrence rule.'); } @@ -670,7 +693,7 @@ class RRule implements \Iterator, \ArrayAccess, \Countable if ( $end !== null ) { $end = self::parseDate($end); } - elseif ( ! $this->count && ! $this->until ) { + elseif ( $this->isInfinite() ) { throw new \LogicException('Cannot get all occurrences of an infinite recurrence rule.'); } @@ -975,7 +998,7 @@ class RRule implements \Iterator, \ArrayAccess, \Countable */ public function count() { - if ( ! $this->count && ! $this->until ) { + if ( $this->isInfinite() ) { throw new \LogicException('Cannot count an infinite recurrence rule.'); } diff --git a/src/RRuleInterface.php b/src/RRuleInterface.php new file mode 100755 index 0000000..bc51044 --- /dev/null +++ b/src/RRuleInterface.php @@ -0,0 +1,30 @@ + + * @link https://github.com/rlanvin/php-rrule + */ + +namespace RRule; + +interface RRuleInterface extends \Iterator, \ArrayAccess, \Countable +{ + public function getOccurrences(); + + /** + * @param date|null $begin + * @param date|null $end + * @return array Returns an array of DateTime + */ + public function getOccurrencesBetween($begin, $end); + + public function occursAt($date); + + public function isFinite(); + + public function isInfinite(); +} \ No newline at end of file diff --git a/src/RSet.php b/src/RSet.php index b1c7fc5..31bee4d 100755 --- a/src/RSet.php +++ b/src/RSet.php @@ -14,7 +14,7 @@ namespace RRule; /** * Recurrence set */ -class RSet implements \Iterator, \ArrayAccess, \Countable +class RSet implements RRuleInterface { protected $rdates = array(); protected $rrules = array(); @@ -22,27 +22,38 @@ class RSet implements \Iterator, \ArrayAccess, \Countable protected $exdates = array(); protected $exrules = array(); + // cache variable + protected $total = null; + protected $infinite = null; + protected $cache = array(); + public function __construct() { } + /** + * Add a RRule (or another RSet) + */ 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'); + elseif ( ! $rrule instanceof RRuleInterface ) { + throw new \InvalidArgumentException('The rule must be a string, an array, or implement RRuleInterface'); } // cloning because I want to iterate it without being disturbed $this->rrules[] = clone $rrule; + $this->clearCache(); + return $this; } /** + * Add a RRule with exclusion rules. * In RFC 2445 but deprecated in RFC 5545 */ public function addExRule($rrule) @@ -50,17 +61,22 @@ class RSet implements \Iterator, \ArrayAccess, \Countable 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'); + elseif ( ! $rrule instanceof RRuleInterface ) { + throw new \InvalidArgumentException('The rule must be a string, an array or implement RRuleInterface'); } // cloning because I want to iterate it without being disturbed $this->exrules[] = clone $rrule; + $this->clearCache(); + return $this; } - public function addRDate($date) + /** + * Add a RDATE (renamed Date for simplicy, since we don't support full RDATE syntax at the moment) + */ + public function addDate($date) { try { $this->rdates[] = RRule::parseDate($date); @@ -70,9 +86,14 @@ class RSet implements \Iterator, \ArrayAccess, \Countable ); } + $this->clearCache(); + return $this; } + /** + * Add a EXDATE + */ public function addExDate($date) { try { @@ -83,12 +104,50 @@ class RSet implements \Iterator, \ArrayAccess, \Countable ); } + $this->clearCache(); + return $this; } + /** + * Clear the cache. Do NOT use while the class is iterating + * @return $this + */ + public function clearCache() + { + $this->total = null; + $this->infinite = null; + $this->cache = array(); + return $this; + } + +/////////////////////////////////////////////////////////////////////////////// +// RRule interface + + public function isFinite() + { + return ! $this->isInfinite(); + } + + public function isInfinite() + { + if ( $this->infinite === null ) { + $this->infinite = false; + foreach ( $this->rrules as $rrule ) { + if ( $rrule->isInfinite() ) { + $this->infinite = true; + break; + } + } + } + return $this->infinite; + } + public function getOccurrences() { - // TODO: need a wait to test the presence of infinite RRULE + if ( $this->isInfinite() ) { + throw new \LogicException('Cannot get all occurrences of an infinite recurrence set.'); + } $res = array(); foreach ( $this as $occurrence ) { @@ -97,6 +156,16 @@ class RSet implements \Iterator, \ArrayAccess, \Countable return $res; } + public function getOccurrencesBetween($begin, $end) + { + throw new \Exception(__METHOD__.' is unimplemented'); + } + + public function occursAt($date) + { + throw new \Exception(__METHOD__.' is unimplemented'); + } + /////////////////////////////////////////////////////////////////////////////// // Iterator interface diff --git a/tests/RRuleTest.php b/tests/RRuleTest.php index 33b738e..6cf4b0c 100755 --- a/tests/RRuleTest.php +++ b/tests/RRuleTest.php @@ -4,6 +4,39 @@ use RRule\RRule; class RRuleTest extends PHPUnit_Framework_TestCase { + public function testIsFinite() + { + $rrule = new RRule(array( + 'freq' => 'yearly' + )); + $this->assertTrue($rrule->isInfinite()); + $this->assertFalse($rrule->isFinite()); + + $rrule = new RRule(array( + 'freq' => 'yearly', + 'count' => 10 + )); + $this->assertFalse($rrule->isInfinite()); + $this->assertTrue($rrule->isFinite()); + } + + public function testIsLeapYear() + { + $this->assertFalse(\RRule\is_leap_year(1700)); + $this->assertFalse(\RRule\is_leap_year(1800)); + $this->assertFalse(\RRule\is_leap_year(1900)); + $this->assertTrue(\RRule\is_leap_year(2000)); + } + + public function testCountable() + { + $rrule = new RRule(array( + 'freq' => 'yearly', + 'count' => 10 + )); + $this->assertEquals(10, count($rrule)); + } + /** * These rules are invalid according to the RFC */ @@ -179,7 +212,6 @@ class RRuleTest extends PHPUnit_Framework_TestCase } } - /** * MONTHY rules, mostly taken from the Python test suite */ @@ -313,6 +345,7 @@ class RRuleTest extends PHPUnit_Framework_TestCase date_create('1997-09-09 18:00:00'))) ); } + /** * @dataProvider weeklyRules */ @@ -379,6 +412,7 @@ class RRuleTest extends PHPUnit_Framework_TestCase ); } + /** * @dataProvider dailyRules */ @@ -1654,14 +1688,6 @@ class RRuleTest extends PHPUnit_Framework_TestCase $this->assertEquals($rule, new RRule($rule->rfcString())); } - public function testIsLeapYear() - { - $this->assertFalse(\RRule\is_leap_year(1700)); - $this->assertFalse(\RRule\is_leap_year(1800)); - $this->assertFalse(\RRule\is_leap_year(1900)); - $this->assertTrue(\RRule\is_leap_year(2000)); - } - public function testTimezoneIsKeptIdentical() { $rrule = new RRule(array( @@ -1697,4 +1723,5 @@ class RRuleTest extends PHPUnit_Framework_TestCase $this->assertEquals(date_create('1997-09-01 09:00:00', new DateTimeZone('America/New_York')), $rrule[0]); } + } diff --git a/tests/RSetTest.php b/tests/RSetTest.php index 412ca5f..7bcfd12 100755 --- a/tests/RSetTest.php +++ b/tests/RSetTest.php @@ -1,59 +1,150 @@ assertFalse($rset->isInfinite()); + $this->assertTrue($rset->isFinite()); + + $rset->addRRule(array( + 'FREQ' => 'YEARLY', + 'COUNT' => 10 + )); + $this->assertFalse($rset->isInfinite()); + $this->assertTrue($rset->isFinite()); + + $rset->addRRule(array( + 'FREQ' => 'YEARLY' + )); + $this->assertTrue($rset->isInfinite()); + $this->assertFalse($rset->isFinite()); + } + public function testAddRRule() { - $rrset = new RSet(); - $rrset->addRRule(array( + $rset = new RSet(); + $rset->addRRule(array( 'FREQ' => 'YEARLY', 'COUNT' => 2, 'BYDAY' => 'TU', 'DTSTART' => date_create('1997-09-02 09:00') )); - $rrset->addRRule(array( + $rset->addRRule(new RRule(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()); + ), $rset->getOccurrences()); } - public function testAddRDate() + public function testAddDate() { - + $rset = new RSet(); + $rset->addRRule(array( + 'FREQ' => 'YEARLY', + 'COUNT' => 1, + 'BYDAY' => 'TU', + 'DTSTART' => date_create('1997-09-02 09:00') + )); + $rset->addDate(date_create('1997-09-04 09:00')); + $rset->addDate(date_create('1997-09-09 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') + ), $rset->getOccurrences()); } public function testAddExRule() { - + $rset = new RSet(); + $rset->addRRule(array( + 'FREQ' => 'YEARLY', + 'COUNT' => 6, + 'BYDAY' => 'TU,TH', + 'DTSTART' => date_create('1997-09-02 09:00') + )); + $rset->addExRule(array( + 'FREQ' => 'YEARLY', + 'COUNT' => 3, + 'BYDAY' => 'TH', + 'DTSTART' => date_create('1997-09-02 09: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') + ), $rset->getOccurrences()); } public function testAddExDate() { - $rrset = new RSet(); - $rrset->addRRule(array( + $rset = new RSet(); + $rset->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'); + $rset->addExdate('1997-09-04 09:00:00'); + $rset->addExdate('1997-09-11 09:00:00'); + $rset->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()); + ), $rset->getOccurrences()); + } + + public function testAddDateAndExRule() + { + // TODO + } + + public function testCountable() + { + // TODO + } + + public function testRSetInRset() + { + $rset = new RSet(); + $rset->addRRule($rset); + $rset->addDate('2016-03-21'); + + $this->assertEquals( + array(date_create('2016-03-21')), + $rset->getOccurrences(), + 'Adding the RSet into itself does not explode' + ); + + $sub_rset = new RSet(); + $sub_rset->addDate('2016-03-21 10:00'); + $sub_rset->addDate('2016-03-21 11:00'); + + $rset = new RSet(); + $rset->addRRule($sub_rset); + + $this->assertEquals(array( + date_create('2016-03-21 10:00'), + date_create('2016-03-21 11:00') + ), $rset->getOccurrences()); + + $rset->addExDate('2016-03-21 11:00'); + $this->assertEquals(array( + date_create('2016-03-21 10:00') + ), $rset->getOccurrences()); } } \ No newline at end of file