diff --git a/src/Recurr/Rule.php b/src/Recurr/Rule.php index dfcf40f..c75e088 100644 --- a/src/Recurr/Rule.php +++ b/src/Recurr/Rule.php @@ -210,7 +210,7 @@ public function __construct($rrule = null, $startDate = null, $endDate = null, $ if (is_array($rrule)) { $this->loadFromArray($rrule); - } else if (!empty($rrule)) { + } elseif (!empty($rrule)) { $this->loadFromString($rrule); } } @@ -286,35 +286,64 @@ public function loadFromString($rrule) */ public function parseString($rrule) { - if (strpos($rrule, 'DTSTART:') === 0) { - $pieces = explode(':', $rrule); + //rrule here can be: + //1) DTSTART:20200607T120200 + //2) DTSTART;TZID=UTC:20200607T120200 + //3) RRULE:FREQ=DAILY;INTERVAL=1 + //4) FREQ=DAILY;DTSTART;TZID=UTC:20200607T120200;INTERVAL=1 + //5) FREQ=DAILY;DTSTART:20200607T120200;INTERVAL=1 + + //SOLUTION: split by ';': + //1) [DTSTART:20200607T120200] + //2) [DTSTART, TZID=UTC:20200607T120200] + //3) [RRULE:FREQ=DAILY, INTERVAL=1] + //4) [FREQ=DAILY, DTSTART, TZID=UTC:20200607T120200, INTERVAL=1] + //5) [FREQ=DAILY, DTSTART:20200607T120200, INTERVAL=1] + + $fragments = explode(';', $rrule); + + if (!count($fragments)) { + throw new InvalidRRule('RRULE is empty'); + } - if (count($pieces) !== 2) { - throw new InvalidRRule('DSTART is not valid'); + $parts = array(); + foreach ($fragments as $fragment) { + if (strpos($fragment, 'RRULE:') === 0) { + $fragment = str_replace('RRULE:', '', $fragment); } - return array('DTSTART' => $pieces[1]); - } - if (strpos($rrule, 'RRULE:') === 0) { - $rrule = str_replace('RRULE:', '', $rrule); - } + if (strpos($fragment, 'DTSTART') === 0) { + if ($fragment === 'DTSTART') { + $parts['DTSTART'] = '';//to be replaced by next token - $pieces = explode(';', $rrule); - $parts = array(); + continue; + } - if (!count($pieces)) { - throw new InvalidRRule('RRULE is empty'); - } + $p = explode(':', $fragment); + if (count($p) !== 2) { + throw new InvalidRRule('DTSTART is not valid'); + } + + $parts['DTSTART'] = $p[1]; - // Split each piece of the RRULE in to KEY=>VAL - foreach ($pieces as $piece) { - if (false === strpos($piece, '=')) { continue; } - list($key, $val) = explode('=', $piece); - $parts[$key] = $val; + if (strpos($fragment, '=')) { + list($key, $val) = explode('=', $fragment); + + if ($key === 'TZID') { + $p = explode(':', $val); + + $parts['TZID'] = $p[0]; + $parts['DTSTART'] = $p[1]; + + continue; + } + + $parts[$key] = $val; + } } return $parts; @@ -341,11 +370,26 @@ public function loadFromArray($parts) $this->setFreq(self::$freqs[$parts['FREQ']]); } + // TZID + if (isset($parts['TZID'])) { + $this->setTimezone($parts['TZID']); + } + // DTSTART if (isset($parts['DTSTART'])) { $this->isStartDateFromDtstart = true; - $date = new \DateTime($parts['DTSTART']); - $date = $date->setTimezone(new \DateTimeZone($this->getTimezone())); + + $timezone = new \DateTimeZone($this->getTimezone()); + $date = null; + if (isset($parts['TZID'])) { + //DTSTART is datetime in TZID timezone + $date = new \DateTime($parts['DTSTART'], $timezone); + } else { + //DTSTART is UTC, convert to timezone coming from constructor/startDate/default + $date = new \DateTime($parts['DTSTART']); + $date = $date->setTimezone($timezone); + } + $this->setStartDate($date); } @@ -474,12 +518,12 @@ public function getString($timezoneType=self::TZ_FLOAT) $date = $d->format($format); $parts[] = "DTSTART;TZID=$tzid:$date"; } else { - $parts[] = 'DTSTART='.$this->getStartDate()->format($format); + $parts[] = 'DTSTART:'.$this->getStartDate()->format($format); } } // DTEND - if ($this->endDate instanceof \DateTime) { + if ($this->endDate instanceof \DateTimeInterface) { if ($timezoneType === self::TZ_FIXED) { $d = $this->getEndDate(); $tzid = $d->getTimezone()->getName(); diff --git a/tests/Recurr/Test/RuleTest.php b/tests/Recurr/Test/RuleTest.php index 3471f34..0e19155 100644 --- a/tests/Recurr/Test/RuleTest.php +++ b/tests/Recurr/Test/RuleTest.php @@ -6,6 +6,7 @@ use Recurr\DateInclusion; use Recurr\Frequency; use Recurr\Rule; +use DateTimeImmutable; class RuleTest extends \PHPUnit_Framework_TestCase { @@ -17,6 +18,49 @@ public function setUp() $this->rule = new Rule; } + public function testCanConvertRruleBackAndForthAndGetSameResult() + { + $rrules = [ + "DTSTART:20200607T120200\r\nRRULE:FREQ=DAILY;INTERVAL=1", + "DTSTART;TZID=Europe/London:20200607T120200\r\nRRULE:FREQ=DAILY;INTERVAL=1" + ]; + + foreach ($rrules as $rrule) { + $rule = new Rule($rrule); + $rruleOne = $rule->getString(Rule::TZ_FIXED); + + $rule2 = new Rule($rruleOne); + $rruleTwo = $rule2->getString(Rule::TZ_FIXED); + + $this->assertSame($rruleOne, $rruleTwo); + } + } + + public function testCanCreateRuleFromStringHavingTzid() + { + $rule = new Rule("DTSTART;TZID=Europe/London:20200607T120200\r\nRRULE:FREQ=DAILY;INTERVAL=1"); + + $this->assertEquals('EUROPE/LONDON', $rule->getTimezone()); + + $startDate = $rule->getStartDate(); + $this->assertSame('2020-06-07 12:02:00', $startDate->format('Y-m-d H:i:s')); + + + $this->assertSame('EUROPE/LONDON', $startDate->getTimezone()->getName()); + } + + public function testCanCreateRruleIfEndIsPassedAsDateTimeImmutable() + { + $begin = new DateTimeImmutable('2012-08-01'); + $end = new DateTimeImmutable('2012-08-31'); + $xmas = new DateTimeImmutable('2012-12-25'); + + $rule = new Rule('FREQ=WEEKLY;COUNT=5', $begin, $end); + $string = $rule->getString(); + + $this->assertEquals('FREQ=WEEKLY;COUNT=5;DTEND=20120831T000000', $string); + } + public function testConstructAcceptableStartDate() { $this->rule = new Rule(null, null); @@ -282,7 +326,7 @@ public function testLoadFromStringWithDtstart() $defaultTimezone = date_default_timezone_get(); date_default_timezone_set('America/Chicago'); - $string = 'FREQ=MONTHLY;DTSTART=20140222T073000'; + $string = 'FREQ=MONTHLY;DTSTART:20140222T073000'; $this->rule->setTimezone('America/Los_Angeles'); $this->rule->loadFromString($string); @@ -363,7 +407,7 @@ public function testGetStringWithUTC() public function testGetStringWithDtstart() { - $string = 'FREQ=MONTHLY;DTSTART=20140210T163045;INTERVAL=1;WKST=MO'; + $string = 'FREQ=MONTHLY;DTSTART:20140210T163045;INTERVAL=1;WKST=MO'; $this->rule->loadFromString($string); @@ -424,7 +468,7 @@ public function testSetStartDateAffectsStringOutput() $this->assertEquals('FREQ=MONTHLY;COUNT=2', $this->rule->getString()); $this->rule->setStartDate(new \DateTime('2015-12-10'), true); - $this->assertEquals('FREQ=MONTHLY;COUNT=2;DTSTART=20151210T000000', $this->rule->getString()); + $this->assertEquals('FREQ=MONTHLY;COUNT=2;DTSTART:20151210T000000', $this->rule->getString()); $this->rule->setStartDate(new \DateTime('2015-12-10'), false); $this->assertEquals('FREQ=MONTHLY;COUNT=2', $this->rule->getString());