From 958fb824dea0bd06a3e76b55c1d415c82fed2c62 Mon Sep 17 00:00:00 2001 From: s0600204 Date: Wed, 12 Jan 2022 15:04:11 +0000 Subject: [PATCH] Handle case of RRULE:COUNT=1 (any FREQ) Outlook, as reported in #293, may in some cases reduce `COUNT` to `1`. Whilst not disallowed in the RFC5545 spec (or any of its errata to date), having a `COUNT` of `1` has no practical purpose that I can see. We currently only check we've reached the count-limit *after* we've generated the first few recurrences of an event and - more pressingly - *after* we've appended the first candidate to the array of generated recurrences. In this case `PHP_MAX_INT` could be replaced with some suitable default number, akin to the current `defaultSpan` property used to put a limit on UNTIL. But that's something for another PR. --- src/ICal/ICal.php | 17 +++++++---------- tests/RecurrencesTest.php | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/ICal/ICal.php b/src/ICal/ICal.php index ff2766a..bc5e2d2 100644 --- a/src/ICal/ICal.php +++ b/src/ICal/ICal.php @@ -1363,7 +1363,7 @@ protected function processRecurrences() * enddate = || */ $count = 1; - $countLimit = (isset($rrules['COUNT'])) ? intval($rrules['COUNT']) : 0; + $countLimit = (isset($rrules['COUNT'])) ? intval($rrules['COUNT']) : PHP_INT_MAX; $until = date_create()->modify("{$this->defaultSpan} years")->setTime(23, 59, 59)->getTimestamp(); if (isset($rrules['UNTIL'])) { @@ -1373,7 +1373,7 @@ protected function processRecurrences() $eventRecurrences = array(); $frequencyRecurringDateTime = clone $initialEventDate; - while ($frequencyRecurringDateTime->getTimestamp() <= $until) { + while ($frequencyRecurringDateTime->getTimestamp() <= $until && $count < $countLimit) { $candidateDateTimes = array(); // phpcs:ignore Squiz.ControlStructures.SwitchDeclaration.MissingDefault @@ -1587,14 +1587,11 @@ function ($yearDay) use ($matchingDays) { $this->eventCount++; } - // Count all evaluated candidates including excluded ones - if (isset($rrules['COUNT'])) { - $count++; - - // If RRULE[COUNT] is reached then break - if ($count >= $countLimit) { - break 2; - } + // Count all evaluated candidates including excluded ones, + // and if RRULE[COUNT] (if set) is reached then break. + $count++; + if ($count >= $countLimit) { + break 2; } } diff --git a/tests/RecurrencesTest.php b/tests/RecurrencesTest.php index 43654df..8a8255d 100644 --- a/tests/RecurrencesTest.php +++ b/tests/RecurrencesTest.php @@ -429,6 +429,23 @@ public function testYearlyWithByMonthAndByMonthDay() ); } + public function testCountIsOne() + { + $checks = array( + array('index' => 0, 'dateString' => '20211201T090000', 'message' => '1st and only expected event: '), + ); + $this->assertVEVENT( + 'UTC', + array( + 'DTSTART:20211201T090000', + 'DTEND:20211201T100000', + 'RRULE:FREQ=DAILY;COUNT=1', + ), + 1, + $checks + ); + } + public function assertVEVENT($defaultTimezone, $veventParts, $count, $checks) { $options = $this->getOptions($defaultTimezone);