From f941bcac75574bc59fb1c8e4c65c90132603bd2a Mon Sep 17 00:00:00 2001 From: Kay Lukas Date: Thu, 29 Nov 2018 13:47:55 +0100 Subject: [PATCH] Add daily occurences to nextMonth and NextYear --- lib/Recur/RRuleIterator.php | 127 ++++++++++++++++++---- tests/VObject/Recur/RRuleIteratorTest.php | 40 +++++++ 2 files changed, 145 insertions(+), 22 deletions(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 75342a2a8..1c0b30e10 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -420,6 +420,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 @@ -445,7 +448,23 @@ 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; + } elseif ($occurrence[0] < $currentDayOfMonth) { + continue; + } + if ($occurrence[1] > $currentHourOfMonth) { + break 2; + } elseif ($occurrence[1] < $currentHourOfMonth) { + continue; + } + + if ($occurrence[2] > $currentMinuteOfMonth) { + break 2; + } elseif ($occurrence[2] < $currentMinuteOfMonth) { + continue; + } + if ($occurrence[3] > $currentSecondOfMonth) { break 2; } } @@ -464,6 +483,9 @@ protected function nextMonthly() // This goes to 0 because we need to start counting at the // beginning. $currentDayOfMonth = 0; + $currentHourOfMonth = 0; + $currentMinuteOfMonth = 0; + $currentSecondOfMonth = 0; // To prevent running this forever (better: until we hit the max date of DateTimeImmutable) we simply // stop at 9999-12-31. Looks like the year 10000 problem is not solved in php .... @@ -477,8 +499,8 @@ protected function nextMonthly() $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]); } /** @@ -489,6 +511,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)) { @@ -599,25 +624,38 @@ 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; + } elseif ($occurrence[0] < $currentDayOfMonth) { + continue; + } + if ($occurrence[1] > $currentHourOfMonth) { + break 2; + } elseif ($occurrence[1] < $currentHourOfMonth) { + continue; + } + if ($occurrence[2] > $currentMinuteOfMonth) { + break 2; + } elseif ($occurrence[2] < $currentMinuteOfMonth) { + continue; + } + if ($occurrence[3] > $currentSecondOfMonth) { + break 2; + } } } @@ -644,8 +682,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 { @@ -809,7 +847,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 */ @@ -895,8 +934,23 @@ 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. @@ -914,11 +968,40 @@ 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 b6a8fdc53..e630246cf 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -576,6 +576,46 @@ public function testYearlyByYearDayNegative() ); } + public function testFirstLastSundayEveryOtherYearAt1530and1730InJanuary() + { + $this->parse('FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=1SU,-1SU;BYHOUR=15,17;BYMINUTE=30,35;BYSECOND=15,56', + '1999-12-01 12:34:56', + [ + '1999-12-01 12:34:56', + '2001-01-07 15:30:15', '2001-01-07 15:30:56', '2001-01-07 15:35:15', '2001-01-07 15:35:56', + '2001-01-07 17:30:15', '2001-01-07 17:30:56', '2001-01-07 17:35:15', '2001-01-07 17:35:56', + + '2001-01-28 15:30:15', '2001-01-28 15:30:56', '2001-01-28 15:35:15', '2001-01-28 15:35:56', + '2001-01-28 17:30:15', '2001-01-28 17:30:56', '2001-01-28 17:35:15', '2001-01-28 17:35:56', + + '2003-01-05 15:30:15', '2003-01-05 15:30:56', '2003-01-05 15:35:15', '2003-01-05 15:35:56', + '2003-01-05 17:30:15', '2003-01-05 17:30:56', '2003-01-05 17:35:15', '2003-01-05 17:35:56', + + '2003-01-26 15:30:15', '2003-01-26 15:30:56', '2003-01-26 15:35:15', '2003-01-26 15:35:56', + '2003-01-26 17:30:15', '2003-01-26 17:30:56', '2003-01-26 17:35:15', '2003-01-26 17:35:56', + ]); + } + + public function testFirstFourthSundayEveryOtherMonthAt830and930() + { + $this->parse('FREQ=MONTHLY;INTERVAL=2;BYDAY=1SU,4SU;BYHOUR=15,17;BYMINUTE=30,32;BYSECOND=11,12', + '2001-01-01 12:34:56', + [ + '2001-01-01 12:34:56', + '2001-01-07 15:30:11', '2001-01-07 15:30:12', '2001-01-07 15:32:11', '2001-01-07 15:32:12', + '2001-01-07 17:30:11', '2001-01-07 17:30:12', '2001-01-07 17:32:11', '2001-01-07 17:32:12', + + '2001-01-28 15:30:11', '2001-01-28 15:30:12', '2001-01-28 15:32:11', '2001-01-28 15:32:12', + '2001-01-28 17:30:11', '2001-01-28 17:30:12', '2001-01-28 17:32:11', '2001-01-28 17:32:12', + + '2001-03-04 15:30:11', '2001-03-04 15:30:12', '2001-03-04 15:32:11', '2001-03-04 15:32:12', + '2001-03-04 17:30:11', '2001-03-04 17:30:12', '2001-03-04 17:32:11', '2001-03-04 17:32:12', + + '2001-03-25 15:30:11', '2001-03-25 15:30:12', '2001-03-25 15:32:11', '2001-03-25 15:32:12', + '2001-03-25 17:30:11', '2001-03-25 17:30:12', '2001-03-25 17:32:11', '2001-03-25 17:32:12', + ]); + } + /** * @expectedException \Sabre\VObject\InvalidDataException */