diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 554507f19..f8f17f7bb 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -417,6 +417,9 @@ protected function nextWeekly() protected function nextMonthly() { $currentDayOfMonth = $this->currentDate->format('j'); + $currentHourOfMonth = $this->currentDate->format('G'); + $currentMinuteOfMonth = $this->currentDate->format('i'); + $currentSecondOfMonth = $this->currentDate->format('s'); if (!$this->byMonthDay && !$this->byDay) { // If the current day is higher than the 28th, rollover can // occur to the next month. We Must skip these invalid @@ -442,7 +445,22 @@ protected function nextMonthly() foreach ($occurrences as $occurrence) { // The first occurrence thats higher than the current // day of the month wins. - if ($occurrence > $currentDayOfMonth) { + if ($occurrence[0] > $currentDayOfMonth) { + break 2; + } else if ($occurrence[0] < $currentDayOfMonth) { + continue; + } + if ($occurrence[1] > $currentHourOfMonth) { + break 2; + } else if ($occurrence[1] < $currentHourOfMonth) { + continue; + } + if ($occurrence[2] > $currentMinuteOfMonth) { + break 2; + } else if ($occurrence[2] < $currentMinuteOfMonth) { + continue; + } + if ($occurrence[3] > $currentSecondOfMonth) { break 2; } } @@ -461,13 +479,16 @@ protected function nextMonthly() // This goes to 0 because we need to start counting at the // beginning. $currentDayOfMonth = 0; + $currentHourOfMonth = 0; + $currentMinuteOfMonth = 0; + $currentSecondOfMonth = 0; } $this->currentDate = $this->currentDate->setDate( (int) $this->currentDate->format('Y'), (int) $this->currentDate->format('n'), - (int) $occurrence - ); + $occurrence[0] + )->setTime($occurrence[1], $occurrence[2], $occurrence[3]); } /** @@ -478,6 +499,9 @@ protected function nextYearly() $currentMonth = $this->currentDate->format('n'); $currentYear = $this->currentDate->format('Y'); $currentDayOfMonth = $this->currentDate->format('j'); + $currentHourOfMonth = $this->currentDate->format('G'); + $currentMinuteOfMonth = $this->currentDate->format('i'); + $currentSecondOfMonth = $this->currentDate->format('s'); // No sub-rules, so we just advance by year if (empty($this->byMonth)) { @@ -588,25 +612,39 @@ protected function nextYearly() return; } - $currentMonth = $this->currentDate->format('n'); - $currentYear = $this->currentDate->format('Y'); - $currentDayOfMonth = $this->currentDate->format('j'); - $advancedToNewMonth = false; // If we got a byDay or getMonthDay filter, we must first expand // further. if ($this->byDay || $this->byMonthDay) { while (true) { - $occurrences = $this->getMonthlyOccurrences(); - - foreach ($occurrences as $occurrence) { - // The first occurrence that's higher than the current - // day of the month wins. - // If we advanced to the next month or year, the first - // occurrence is always correct. - if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) { - break 2; + + // If the start date is incorrect we must directly jump to the next value + if (in_array($currentMonth, $this->byMonth)) { + $occurrences = $this->getMonthlyOccurrences(); + foreach ($occurrences as $occurrence) { + // The first occurrence that's higher than the current + // day of the month wins. + // If we advanced to the next month or year, the first + // occurrence is always correct. + if ($occurrence[0] > $currentDayOfMonth || $advancedToNewMonth) { + break 2; + } else if ($occurrence[0] < $currentDayOfMonth) { + continue; + } + if ($occurrence[1] > $currentHourOfMonth) { + break 2; + } else if ($occurrence[1] < $currentHourOfMonth) { + continue; + } + if ($occurrence[2] > $currentMinuteOfMonth) { + break 2; + } else if ($occurrence[2] < $currentMinuteOfMonth) { + continue; + } + if ($occurrence[3] > $currentSecondOfMonth) { + break 2; + } } } @@ -633,9 +671,8 @@ protected function nextYearly() $this->currentDate = $this->currentDate->setDate( (int) $currentYear, (int) $currentMonth, - (int) $occurrence - ); - + (int) $occurrence[0] + )->setTime($occurrence[1], $occurrence[2], $occurrence[3]); return; } else { // These are the 'byMonth' rules, if there are no byDay or @@ -798,7 +835,8 @@ protected function parseRRule($rrule) * Returns all the occurrences for a monthly frequency with a 'byDay' or * 'byMonthDay' expansion for the current month. * - * The returned list is an array of integers with the day of month (1-31). + * The returned list is an array of arrays with as first element the day of month (1-31); + * the hour; the minute and second of the occurence * * @return array */ @@ -884,8 +922,22 @@ protected function getMonthlyOccurrences() } else { $result = $byDayResults; } - $result = array_unique($result); - sort($result, SORT_NUMERIC); + + $result = $this->addDailyOccurences($result); + $result = array_unique($result, SORT_REGULAR); + $sortLex = function ($a, $b) { + if ($a[0] != $b[0]) { + return $a[0] - $b[0]; + } + if ($a[1] != $b[1]) { + return $a[1] - $b[1]; + } + if ($a[2] != $b[2]) { + return $a[2] - $b[2]; + } + return $a[3] - $b[3]; + }; + usort($result, $sortLex); // The last thing that needs checking is the BYSETPOS. If it's set, it // means only certain items in the set survive the filter. @@ -903,11 +955,37 @@ protected function getMonthlyOccurrences() } } - sort($filteredResult, SORT_NUMERIC); + usort($result, $sortLex); return $filteredResult; } + /** + * Expends daily occurrences to an array of days that an event occurs on + * @param array $result an array of integers with the day of month (1-31); + * @return array an array of arrays with the day of the month, hours, minute and seconds of the occurence + */ + protected function addDailyOccurences(array $result) { + $output = []; + $hour = (int) $this->currentDate->format('G'); + $minute = (int) $this->currentDate->format('i'); + $second = (int) $this->currentDate->format('s'); + foreach ($result as $day) + { + $seconds = $this->bySecond ? $this->bySecond : [ $second ]; + $minutes = $this->byMinute ? $this->byMinute : [ $minute ]; + $hours = $this->byHour ? $this->byHour : [ $hour ]; + foreach ($hours as $h) { + foreach ($minutes as $m) { + foreach ($seconds as $s) { + $output[] = [(int) $day, (int) $h, (int) $m, (int) $s]; + } + } + } + } + return $output; + } + /** * Simple mapping from iCalendar day names to day numbers. * diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index b8b1706d8..702843794 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -576,6 +576,56 @@ public function testYearlyByYearDayNegative() ); } + public function testEverySundayEveryOtherYearAt830and930() + { + $this->parse('FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=15,17;BYMINUTE=30', + '1999-12-01 12:34:56', + [ + '1999-12-01 12:34:56', + '2001-01-07 15:30:56', + '2001-01-07 17:30:56', + '2001-01-14 15:30:56', + '2001-01-14 17:30:56', + '2001-01-21 15:30:56', + '2001-01-21 17:30:56', + '2001-01-28 15:30:56', + '2001-01-28 17:30:56', + '2003-01-05 15:30:56', + '2003-01-05 17:30:56', + '2003-01-12 15:30:56', + '2003-01-12 17:30:56', + '2003-01-19 15:30:56', + '2003-01-19 17:30:56', + '2003-01-26 15:30:56', + '2003-01-26 17:30:56' + ]); + } + + public function testEverySundayEveryOtherMonthAt830and930() + { + $this->parse('FREQ=MONTHLY;INTERVAL=2;BYDAY=SU;BYHOUR=15,17;BYMINUTE=30', + '2001-01-01 12:34:56', + [ + '2001-01-01 12:34:56', + '2001-01-07 15:30:56', + '2001-01-07 17:30:56', + '2001-01-14 15:30:56', + '2001-01-14 17:30:56', + '2001-01-21 15:30:56', + '2001-01-21 17:30:56', + '2001-01-28 15:30:56', + '2001-01-28 17:30:56', + '2001-03-04 15:30:56', + '2001-03-04 17:30:56', + '2001-03-11 15:30:56', + '2001-03-11 17:30:56', + '2001-03-18 15:30:56', + '2001-03-18 17:30:56', + '2001-03-25 15:30:56', + '2001-03-25 17:30:56' + ]); + } + /** * @expectedException \Sabre\VObject\InvalidDataException */