Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add daily occurences to nextMonth and NextYear #437

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
127 changes: 105 additions & 22 deletions lib/Recur/RRuleIterator.php
Expand Up @@ -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
Expand All @@ -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;
}
}
Expand All @@ -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 ....
Expand All @@ -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]);
}

/**
Expand All @@ -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)) {
Expand Down Expand Up @@ -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;
}
}
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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.
Expand All @@ -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.
*
Expand Down
40 changes: 40 additions & 0 deletions tests/VObject/Recur/RRuleIteratorTest.php
Expand Up @@ -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
*/
Expand Down