]> source.dussan.org Git - nextcloud-server.git/commitdiff
fix: RDATE and EXDATE property instances backport/48833/stable30 49302/head
authorSebastianKrupinski <krupinskis05@gmail.com>
Mon, 21 Oct 2024 18:08:43 +0000 (14:08 -0400)
committerbackportbot[bot] <backportbot[bot]@users.noreply.github.com>
Fri, 15 Nov 2024 06:50:14 +0000 (06:50 +0000)
Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
apps/dav/lib/CalDAV/EventReader.php
apps/dav/lib/CalDAV/Schedule/IMipService.php
apps/dav/tests/unit/CalDAV/EventReaderTest.php
apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php

index 6906572cc3709f8737cbb86edc13afff2eb3dd5f..1820576cc66bb41c86beb46ea1cb34513600c6f8 100644 (file)
@@ -200,8 +200,12 @@ class EventReader {
                }
                // evaluate if RDATE exist and construct iterator
                if (isset($this->baseEvent->RDATE)) {
+                       $dates = [];
+                       foreach ($this->baseEvent->RDATE as $entry) {
+                               $dates[] = $entry->getValue();
+                       }
                        $this->rdateIterator = new EventReaderRDate(
-                               $this->baseEvent->RDATE->getValue(),
+                               implode(',', $dates),
                                $this->baseEventStartDate
                        );
                }
@@ -214,8 +218,12 @@ class EventReader {
                }
                // evaluate if EXDATE exist and construct iterator
                if (isset($this->baseEvent->EXDATE)) {
+                       $dates = [];
+                       foreach ($this->baseEvent->EXDATE as $entry) {
+                               $dates[] = $entry->getValue();
+                       }
                        $this->edateIterator = new EventReaderRDate(
-                               $this->baseEvent->EXDATE->getValue(),
+                               implode(',', $dates),
                                $this->baseEventStartDate
                        );
                }
index 14272cb5206597953de75bbbee54d4a4bb8ccbd2..87cf3067742dcdd01abf80e46e2513c9f0145d04 100644 (file)
@@ -164,7 +164,7 @@ class IMipService {
 
                        $data['meeting_when_html'] = $oldMeetingWhen !== $data['meeting_when'] ? sprintf("<span style='text-decoration: line-through'>%s</span><br />%s", $oldMeetingWhen, $data['meeting_when']) : $data['meeting_when'];
                }
-               // generate occuring next string
+               // generate occurring next string
                if ($eventReaderCurrent->recurs()) {
                        $data['meeting_occurring'] = $this->generateOccurringString($eventReaderCurrent);
                }
@@ -173,7 +173,7 @@ class IMipService {
        }
 
        /**
-        * genarates a when string based on if a event has an recurrence or not
+        * generates a when string based on if a event has an recurrence or not
         *
         * @since 30.0.0
         *
@@ -189,7 +189,7 @@ class IMipService {
        }
 
        /**
-        * genarates a when string for a non recurring event
+        * generates a when string for a non recurring event
         *
         * @since 30.0.0
         *
@@ -198,8 +198,8 @@ class IMipService {
         * @return string
         */
        public function generateWhenStringSingular(EventReader $er): string {
-               // calculate time differnce from now to start of event
-               $occuring = $this->minimizeInterval($this->timeFactory->getDateTime()->diff($er->recurrenceDate()));
+               // calculate time difference from now to start of event
+               $occurring = $this->minimizeInterval($this->timeFactory->getDateTime()->diff($er->recurrenceDate()));
                // extract start date
                $startDate = $this->l10n->l('date', $er->startDateTime(), ['width' => 'full']);
                // time of the day
@@ -214,19 +214,19 @@ class IMipService {
                // Output produced in order:
                // In a day/week/month/year on July 1, 2024 for the entire day
                // In a day/week/month/year on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)
-               // In 2 days/weeks/monthss/years on July 1, 2024 for the entire day
-               // In 2 days/weeks/monthss/years on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)
-               return match ([($occuring[0] > 1), !empty($endTime)]) {
-                       [false, false] => $this->l10n->t('In a %1$s on %2$s for the entire day', [$occuring[1], $startDate]),
-                       [false, true] => $this->l10n->t('In a %1$s on %2$s between %3$s - %4$s', [$occuring[1], $startDate, $startTime, $endTime]),
-                       [true, false] => $this->l10n->t('In %1$s %2$s on %3$s for the entire day', [$occuring[0], $occuring[1], $startDate]),
-                       [true, true] => $this->l10n->t('In %1$s %2$s on %3$s between %4$s - %5$s', [$occuring[0], $occuring[1], $startDate, $startTime, $endTime]),
+               // In 2 days/weeks/months/years on July 1, 2024 for the entire day
+               // In 2 days/weeks/months/years on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)
+               return match ([($occurring[0] > 1), !empty($endTime)]) {
+                       [false, false] => $this->l10n->t('In a %1$s on %2$s for the entire day', [$occurring[1], $startDate]),
+                       [false, true] => $this->l10n->t('In a %1$s on %2$s between %3$s - %4$s', [$occurring[1], $startDate, $startTime, $endTime]),
+                       [true, false] => $this->l10n->t('In %1$s %2$s on %3$s for the entire day', [$occurring[0], $occurring[1], $startDate]),
+                       [true, true] => $this->l10n->t('In %1$s %2$s on %3$s between %4$s - %5$s', [$occurring[0], $occurring[1], $startDate, $startTime, $endTime]),
                        default => $this->l10n->t('Could not generate when statement')
                };
        }
 
        /**
-        * genarates a when string based on recurrance precision/frequency
+        * generates a when string based on recurrence precision/frequency
         *
         * @since 30.0.0
         *
@@ -245,7 +245,7 @@ class IMipService {
        }
 
        /**
-        * genarates a when string for a daily precision/frequency
+        * generates a when string for a daily precision/frequency
         *
         * @since 30.0.0
         *
@@ -297,7 +297,7 @@ class IMipService {
        }
 
        /**
-        * genarates a when string for a weekly precision/frequency
+        * generates a when string for a weekly precision/frequency
         *
         * @since 30.0.0
         *
@@ -351,7 +351,7 @@ class IMipService {
        }
 
        /**
-        * genarates a when string for a monthly precision/frequency
+        * generates a when string for a monthly precision/frequency
         *
         * @since 30.0.0
         *
@@ -417,7 +417,7 @@ class IMipService {
        }
 
        /**
-        * genarates a when string for a yearly precision/frequency
+        * generates a when string for a yearly precision/frequency
         *
         * @since 30.0.0
         *
@@ -485,7 +485,7 @@ class IMipService {
        }
 
        /**
-        * genarates a when string for a fixed precision/frequency
+        * generates a when string for a fixed precision/frequency
         *
         * @since 30.0.0
         *
@@ -519,7 +519,7 @@ class IMipService {
        }
        
        /**
-        * genarates a occurring next string for a recurring event
+        * generates a occurring next string for a recurring event
         *
         * @since 30.0.0
         *
@@ -529,26 +529,26 @@ class IMipService {
         */
        public function generateOccurringString(EventReader $er): string {
 
-               // reset to initial occurance
+               // reset to initial occurrence
                $er->recurrenceRewind();
                // forward to current date
                $er->recurrenceAdvanceTo($this->timeFactory->getDateTime());
-               // calculate time differnce from now to start of next event occurance and minimize it
-               $occuranceIn = $this->minimizeInterval($this->timeFactory->getDateTime()->diff($er->recurrenceDate()));
-               // store next occurance value
-               $occurance = $this->l10n->l('date', $er->recurrenceDate(), ['width' => 'long']);
-               // forward one occurance
+               // calculate time difference from now to start of next event occurrence and minimize it
+               $occurrenceIn = $this->minimizeInterval($this->timeFactory->getDateTime()->diff($er->recurrenceDate()));
+               // store next occurrence value
+               $occurrence = $this->l10n->l('date', $er->recurrenceDate(), ['width' => 'long']);
+               // forward one occurrence
                $er->recurrenceAdvance();
-               // evaluate if occurance is valid
+               // evaluate if occurrence is valid
                if ($er->recurrenceDate() !== null) {
-                       // store following occurance value
-                       $occurance2 = $this->l10n->l('date', $er->recurrenceDate(), ['width' => 'long']);
-                       // forward one occurance
+                       // store following occurrence value
+                       $occurrence2 = $this->l10n->l('date', $er->recurrenceDate(), ['width' => 'long']);
+                       // forward one occurrence
                        $er->recurrenceAdvance();
-                       // evaluate if occurance is valid
+                       // evaluate if occurrence is valid
                        if ($er->recurrenceDate()) {
-                               // store following occurance value
-                               $occurance3 = $this->l10n->l('date', $er->recurrenceDate(), ['width' => 'long']);
+                               // store following occurrence value
+                               $occurrence3 = $this->l10n->l('date', $er->recurrenceDate(), ['width' => 'long']);
                        }
                }
                // generate localized when string
@@ -561,13 +561,13 @@ class IMipService {
                // In 2 days/weeks/months/years on July 1, 2024
                // In 2 days/weeks/months/years on July 1, 2024 then on July 3, 2024
                // In 2 days/weeks/months/years on July 1, 2024 then on July 3, 2024 and July 5, 2024
-               return match ([($occuranceIn[0] > 1), !empty($occurance2), !empty($occurance3)]) {
-                       [false, false, false] => $this->l10n->t('In a %1$s on %2$s', [$occuranceIn[1], $occurance]),
-                       [false, true, false] => $this->l10n->t('In a %1$s on %2$s then on %3$s', [$occuranceIn[1], $occurance, $occurance2]),
-                       [false, true, true] => $this->l10n->t('In a %1$s on %2$s then on %3$s and %4$s', [$occuranceIn[1], $occurance, $occurance2, $occurance3]),
-                       [true, false, false] => $this->l10n->t('In %1$s %2$s on %3$s', [$occuranceIn[0], $occuranceIn[1], $occurance]),
-                       [true, true, false] => $this->l10n->t('In %1$s %2$s on %3$s then on %4$s', [$occuranceIn[0], $occuranceIn[1], $occurance, $occurance2]),
-                       [true, true, true] => $this->l10n->t('In %1$s %2$s on %3$s then on %4$s and %5$s', [$occuranceIn[0], $occuranceIn[1], $occurance, $occurance2, $occurance3]),
+               return match ([($occurrenceIn[0] > 1), !empty($occurrence2), !empty($occurrence3)]) {
+                       [false, false, false] => $this->l10n->t('In a %1$s on %2$s', [$occurrenceIn[1], $occurrence]),
+                       [false, true, false] => $this->l10n->t('In a %1$s on %2$s then on %3$s', [$occurrenceIn[1], $occurrence, $occurrence2]),
+                       [false, true, true] => $this->l10n->t('In a %1$s on %2$s then on %3$s and %4$s', [$occurrenceIn[1], $occurrence, $occurrence2, $occurrence3]),
+                       [true, false, false] => $this->l10n->t('In %1$s %2$s on %3$s', [$occurrenceIn[0], $occurrenceIn[1], $occurrence]),
+                       [true, true, false] => $this->l10n->t('In %1$s %2$s on %3$s then on %4$s', [$occurrenceIn[0], $occurrenceIn[1], $occurrence, $occurrence2]),
+                       [true, true, true] => $this->l10n->t('In %1$s %2$s on %3$s then on %4$s and %5$s', [$occurrenceIn[0], $occurrenceIn[1], $occurrence, $occurrence2, $occurrence3]),
                        default => $this->l10n->t('Could not generate next recurrence statement')
                };
 
index 23f0172131d0a56672af9c9558cc8482fc1adbdf..d2c30ff3d1bf04c6b425ee5a9ccd4eba5e0acfb4 100644 (file)
@@ -533,6 +533,15 @@ class EventReaderTest extends TestCase {
                // test set by constructor
                $this->assertTrue($er->recurringConcludes());
 
+               /** test rdate (multiple property instances) recurrance */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RDATE', '20240703');
+               $vCalendar->VEVENT[0]->add('RDATE', '20240705');
+               // construct event reader
+               $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test set by constructor
+               $this->assertTrue($er->recurringConcludes());
+
                /** test rrule and rdate recurrance with rdate as last date */
                $vCalendar = clone $this->vCalendar1a;
                $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR');
@@ -578,6 +587,15 @@ class EventReaderTest extends TestCase {
                // test set by constructor
                $this->assertEquals(2, $er->recurringConcludesAfter());
 
+               /** test rdate (multiple property instances) recurrance */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RDATE', '20240703');
+               $vCalendar->VEVENT[0]->add('RDATE', '20240705');
+               // construct event reader
+               $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test set by constructor
+               $this->assertEquals(2, $er->recurringConcludesAfter());
+
                /** test rrule and rdate recurrance */
                $vCalendar = clone $this->vCalendar1a;
                $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR');
@@ -624,6 +642,15 @@ class EventReaderTest extends TestCase {
                // test set by constructor
                $this->assertEquals((new \DateTime('20240705T000000', (new DateTimeZone('America/Toronto')))), $er->recurringConcludesOn());
 
+               /** test rdate (multiple property instances) recurrance */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RDATE', '20240703');
+               $vCalendar->VEVENT[0]->add('RDATE', '20240705');
+               // construct event reader
+               $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test set by constructor
+               $this->assertEquals((new \DateTime('20240705T000000', (new DateTimeZone('America/Toronto')))), $er->recurringConcludesOn());
+
                /** test rrule and rdate recurrance with rdate as last date */
                $vCalendar = clone $this->vCalendar1a;
                $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR');
index 667604f9b3ee3a4ec062fcdb50f04ecbcda7c85c..63ccb19d4d2280b1268547dda196230a3fa0b4f8 100644 (file)
@@ -1248,7 +1248,7 @@ class IMipServiceTest extends TestCase {
 
        }
 
-       public function testGenerateOccurringString(): void {
+       public function testGenerateOccurringStringWithRrule(): void {
                
                // construct l10n return(s)
                $this->l10n->method('l')->willReturnCallback(
@@ -1285,7 +1285,7 @@ class IMipServiceTest extends TestCase {
                        (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
                );
 
-               /** test patrial day recurring event in 1 day with single occurance remaining */
+               /** test patrial day recurring event in 1 day with single occurrence remaining */
                $vCalendar = clone $this->vCalendar1a;
                $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1');
                // construct event reader
@@ -1296,7 +1296,7 @@ class IMipServiceTest extends TestCase {
                        $this->service->generateOccurringString($eventReader)
                );
 
-               /** test patrial day recurring event in 1 day with two occurances remaining */
+               /** test patrial day recurring event in 1 day with two occurrences remaining */
                $vCalendar = clone $this->vCalendar1a;
                $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2');
                // construct event reader
@@ -1307,7 +1307,7 @@ class IMipServiceTest extends TestCase {
                        $this->service->generateOccurringString($eventReader)
                );
 
-               /** test patrial day recurring event in 1 day with three occurances remaining */
+               /** test patrial day recurring event in 1 day with three occurrences remaining */
                $vCalendar = clone $this->vCalendar1a;
                $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3');
                // construct event reader
@@ -1318,7 +1318,7 @@ class IMipServiceTest extends TestCase {
                        $this->service->generateOccurringString($eventReader)
                );
 
-               /** test patrial day recurring event in 2 days with single occurance remaining */
+               /** test patrial day recurring event in 2 days with single occurrence remaining */
                $vCalendar = clone $this->vCalendar1a;
                $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1');
                // construct event reader
@@ -1329,7 +1329,7 @@ class IMipServiceTest extends TestCase {
                        $this->service->generateOccurringString($eventReader)
                );
 
-               /** test patrial day recurring event in 2 days with two occurances remaining */
+               /** test patrial day recurring event in 2 days with two occurrences remaining */
                $vCalendar = clone $this->vCalendar1a;
                $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2');
                // construct event reader
@@ -1340,7 +1340,7 @@ class IMipServiceTest extends TestCase {
                        $this->service->generateOccurringString($eventReader)
                );
 
-               /** test patrial day recurring event in 2 days with three occurances remaining */
+               /** test patrial day recurring event in 2 days with three occurrences remaining */
                $vCalendar = clone $this->vCalendar1a;
                $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3');
                // construct event reader
@@ -1352,4 +1352,417 @@ class IMipServiceTest extends TestCase {
                );
        }
 
+       public function testGenerateOccurringStringWithRdate(): void {
+               
+               // construct l10n return(s)
+               $this->l10n->method('l')->willReturnCallback(
+                       function ($v1, $v2, $v3) {
+                               return match (true) {
+                                       $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 1, 2024',
+                                       $v1 === 'date' && $v2 == (new \DateTime('20240703T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 3, 2024',
+                                       $v1 === 'date' && $v2 == (new \DateTime('20240705T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 5, 2024'
+                               };
+                       }
+               );
+               $this->l10n->method('t')->willReturnMap([
+                       ['In a %1$s on %2$s', ['day', 'July 1, 2024'], 'In a day on July 1, 2024'],
+                       ['In a %1$s on %2$s then on %3$s', ['day', 'July 1, 2024', 'July 3, 2024'], 'In a day on July 1, 2024 then on July 3, 2024'],
+                       ['In a %1$s on %2$s then on %3$s and %4$s', ['day', 'July 1, 2024', 'July 3, 2024', 'July 5, 2024'], 'In a day on July 1, 2024 then on July 3, 2024 and July 5, 2024'],
+                       ['In %1$s %2$s on %3$s', [2, 'days', 'July 1, 2024'], 'In 2 days on July 1, 2024'],
+                       ['In %1$s %2$s on %3$s then on %4$s', [2, 'days', 'July 1, 2024', 'July 3, 2024'], 'In 2 days on July 1, 2024 then on July 3, 2024'],
+                       ['In %1$s %2$s on %3$s then on %4$s and %5$s', [2, 'days', 'July 1, 2024', 'July 3, 2024', 'July 5, 2024'], 'In 2 days on July 1, 2024 then on July 3, 2024 and July 5, 2024'],
+               ]);
+
+               // construct time factory return(s)
+               $this->timeFactory->method('getDateTime')->willReturnOnConsecutiveCalls(
+                       (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+               );
+
+               /** test patrial day recurring event in 1 day with single occurrence remaining */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RDATE', '20240701T080000');
+               // construct event reader
+               $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test output
+               $this->assertEquals(
+                       'In a day on July 1, 2024',
+                       $this->service->generateOccurringString($eventReader),
+                       'test patrial day recurring event in 1 day with single occurrence remaining'
+               );
+
+               /** test patrial day recurring event in 1 day with two occurrences remaining */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RDATE', '20240701T080000,20240703T080000');
+               // construct event reader
+               $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test output
+               $this->assertEquals(
+                       'In a day on July 1, 2024 then on July 3, 2024',
+                       $this->service->generateOccurringString($eventReader),
+                       'test patrial day recurring event in 1 day with two occurrences remaining'
+               );
+
+               /** test patrial day recurring event in 1 day with three occurrences remaining */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RDATE', '20240701T080000,20240703T080000,20240705T080000');
+               // construct event reader
+               $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test output
+               $this->assertEquals(
+                       'In a day on July 1, 2024 then on July 3, 2024 and July 5, 2024',
+                       $this->service->generateOccurringString($eventReader),
+                       ''
+               );
+
+               /** test patrial day recurring event in 2 days with single occurrences remaining */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RDATE', '20240701T080000');
+               // construct event reader
+               $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test output
+               $this->assertEquals(
+                       'In 2 days on July 1, 2024',
+                       $this->service->generateOccurringString($eventReader),
+                       ''
+               );
+
+               /** test patrial day recurring event in 2 days with two occurrences remaining */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RDATE', '20240701T080000');
+               $vCalendar->VEVENT[0]->add('RDATE', '20240703T080000');
+               // construct event reader
+               $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test output
+               $this->assertEquals(
+                       'In 2 days on July 1, 2024 then on July 3, 2024',
+                       $this->service->generateOccurringString($eventReader),
+                       ''
+               );
+
+               /** test patrial day recurring event in 2 days with three occurrences remaining */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RDATE', '20240701T080000');
+               $vCalendar->VEVENT[0]->add('RDATE', '20240703T080000');
+               $vCalendar->VEVENT[0]->add('RDATE', '20240705T080000');
+               // construct event reader
+               $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test output
+               $this->assertEquals(
+                       'In 2 days on July 1, 2024 then on July 3, 2024 and July 5, 2024',
+                       $this->service->generateOccurringString($eventReader),
+                       'test patrial day recurring event in 2 days with three occurrences remaining'
+               );
+       }
+
+       public function testGenerateOccurringStringWithOneExdate(): void {
+               
+               // construct l10n return(s)
+               $this->l10n->method('l')->willReturnCallback(
+                       function ($v1, $v2, $v3) {
+                               return match (true) {
+                                       $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 1, 2024',
+                                       $v1 === 'date' && $v2 == (new \DateTime('20240705T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 5, 2024',
+                                       $v1 === 'date' && $v2 == (new \DateTime('20240707T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 7, 2024'
+                               };
+                       }
+               );
+               $this->l10n->method('t')->willReturnMap([
+                       ['In a %1$s on %2$s', ['day', 'July 1, 2024'], 'In a day on July 1, 2024'],
+                       ['In a %1$s on %2$s then on %3$s', ['day', 'July 1, 2024', 'July 5, 2024'], 'In a day on July 1, 2024 then on July 5, 2024'],
+                       ['In a %1$s on %2$s then on %3$s and %4$s', ['day', 'July 1, 2024', 'July 5, 2024', 'July 7, 2024'], 'In a day on July 1, 2024 then on July 5, 2024 and July 7, 2024'],
+                       ['In %1$s %2$s on %3$s', [2, 'days', 'July 1, 2024'], 'In 2 days on July 1, 2024'],
+                       ['In %1$s %2$s on %3$s then on %4$s', [2, 'days', 'July 1, 2024', 'July 5, 2024'], 'In 2 days on July 1, 2024 then on July 5, 2024'],
+                       ['In %1$s %2$s on %3$s then on %4$s and %5$s', [2, 'days', 'July 1, 2024', 'July 5, 2024', 'July 7, 2024'], 'In 2 days on July 1, 2024 then on July 5, 2024 and July 7, 2024'],
+               ]);
+
+               // construct time factory return(s)
+               $this->timeFactory->method('getDateTime')->willReturnOnConsecutiveCalls(
+                       (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+               );
+
+               /** test patrial day recurring event in 1 day with single occurrence remaining and one exception */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1');
+               $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+               // construct event reader
+               $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test output
+               $this->assertEquals(
+                       'In a day on July 1, 2024',
+                       $this->service->generateOccurringString($eventReader),
+                       'test patrial day recurring event in 1 day with single occurrence remaining and one exception'
+               );
+
+               /** test patrial day recurring event in 1 day with two occurrences remaining and one exception */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2');
+               $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+               // construct event reader
+               $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test output
+               $this->assertEquals(
+                       'In a day on July 1, 2024',
+                       $this->service->generateOccurringString($eventReader),
+                       'test patrial day recurring event in 1 day with two occurrences remaining and one exception'
+               );
+
+               /** test patrial day recurring event in 1 day with three occurrences remaining and one exception */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3');
+               $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+               // construct event reader
+               $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test output
+               $this->assertEquals(
+                       'In a day on July 1, 2024 then on July 5, 2024',
+                       $this->service->generateOccurringString($eventReader),
+                       'test patrial day recurring event in 1 day with three occurrences remaining and one exception'
+               );
+
+               /** test patrial day recurring event in 1 day with four occurrences remaining and one exception */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=4');
+               $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+               // construct event reader
+               $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test output
+               $this->assertEquals(
+                       'In a day on July 1, 2024 then on July 5, 2024 and July 7, 2024',
+                       $this->service->generateOccurringString($eventReader),
+                       'test patrial day recurring event in 1 day with four occurrences remaining and one exception'
+               );
+
+               /** test patrial day recurring event in 2 days with single occurrences remaining and one exception */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1');
+               $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+               // construct event reader
+               $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test output
+               $this->assertEquals(
+                       'In 2 days on July 1, 2024',
+                       $this->service->generateOccurringString($eventReader),
+                       'test patrial day recurring event in 2 days with single occurrences remaining and one exception'
+               );
+
+               /** test patrial day recurring event in 2 days with two occurrences remaining and one exception */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2');
+               $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+               // construct event reader
+               $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test output
+               $this->assertEquals(
+                       'In 2 days on July 1, 2024',
+                       $this->service->generateOccurringString($eventReader),
+                       'test patrial day recurring event in 2 days with two occurrences remaining and one exception'
+               );
+
+               /** test patrial day recurring event in 2 days with three occurrences remaining and one exception */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3');
+               $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+               // construct event reader
+               $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test output
+               $this->assertEquals(
+                       'In 2 days on July 1, 2024 then on July 5, 2024',
+                       $this->service->generateOccurringString($eventReader),
+                       'test patrial day recurring event in 2 days with three occurrences remaining and one exception'
+               );
+
+               /** test patrial day recurring event in 2 days with four occurrences remaining and one exception */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=4');
+               $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+               // construct event reader
+               $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test output
+               $this->assertEquals(
+                       'In 2 days on July 1, 2024 then on July 5, 2024 and July 7, 2024',
+                       $this->service->generateOccurringString($eventReader),
+                       'test patrial day recurring event in 2 days with four occurrences remaining and one exception'
+               );
+       }
+
+       public function testGenerateOccurringStringWithTwoExdate(): void {
+               
+               // construct l10n return(s)
+               $this->l10n->method('l')->willReturnCallback(
+                       function ($v1, $v2, $v3) {
+                               return match (true) {
+                                       $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 1, 2024',
+                                       $v1 === 'date' && $v2 == (new \DateTime('20240705T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 5, 2024',
+                                       $v1 === 'date' && $v2 == (new \DateTime('20240709T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 9, 2024'
+                               };
+                       }
+               );
+               $this->l10n->method('t')->willReturnMap([
+                       ['In a %1$s on %2$s', ['day', 'July 1, 2024'], 'In a day on July 1, 2024'],
+                       ['In a %1$s on %2$s then on %3$s', ['day', 'July 1, 2024', 'July 5, 2024'], 'In a day on July 1, 2024 then on July 5, 2024'],
+                       ['In a %1$s on %2$s then on %3$s and %4$s', ['day', 'July 1, 2024', 'July 5, 2024', 'July 9, 2024'], 'In a day on July 1, 2024 then on July 5, 2024 and July 9, 2024'],
+                       ['In %1$s %2$s on %3$s', [2, 'days', 'July 1, 2024'], 'In 2 days on July 1, 2024'],
+                       ['In %1$s %2$s on %3$s then on %4$s', [2, 'days', 'July 1, 2024', 'July 5, 2024'], 'In 2 days on July 1, 2024 then on July 5, 2024'],
+                       ['In %1$s %2$s on %3$s then on %4$s and %5$s', [2, 'days', 'July 1, 2024', 'July 5, 2024', 'July 9, 2024'], 'In 2 days on July 1, 2024 then on July 5, 2024 and July 9, 2024'],
+               ]);
+
+               // construct time factory return(s)
+               $this->timeFactory->method('getDateTime')->willReturnOnConsecutiveCalls(
+                       (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+                       (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+               );
+
+               /** test patrial day recurring event in 1 day with single occurrence remaining and two exception */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1');
+               $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+               $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000');
+               // construct event reader
+               $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test output
+               $this->assertEquals(
+                       'In a day on July 1, 2024',
+                       $this->service->generateOccurringString($eventReader),
+                       'test patrial day recurring event in 1 day with single occurrence remaining and two exception'
+               );
+
+               /** test patrial day recurring event in 1 day with two occurrences remaining and two exception */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2');
+               $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+               $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000');
+               // construct event reader
+               $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test output
+               $this->assertEquals(
+                       'In a day on July 1, 2024',
+                       $this->service->generateOccurringString($eventReader),
+                       'test patrial day recurring event in 1 day with two occurrences remaining and two exception'
+               );
+
+               /** test patrial day recurring event in 1 day with three occurrences remaining and two exception */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3');
+               $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+               $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000');
+               // construct event reader
+               $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test output
+               $this->assertEquals(
+                       'In a day on July 1, 2024 then on July 5, 2024',
+                       $this->service->generateOccurringString($eventReader),
+                       'test patrial day recurring event in 1 day with three occurrences remaining and two exception'
+               );
+
+               /** test patrial day recurring event in 1 day with four occurrences remaining and two exception */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=5');
+               $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+               $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000');
+               // construct event reader
+               $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test output
+               $this->assertEquals(
+                       'In a day on July 1, 2024 then on July 5, 2024 and July 9, 2024',
+                       $this->service->generateOccurringString($eventReader),
+                       'test patrial day recurring event in 1 day with four occurrences remaining and two exception'
+               );
+
+               /** test patrial day recurring event in 2 days with single occurrences remaining and two exception */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1');
+               $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+               $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000');
+               // construct event reader
+               $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test output
+               $this->assertEquals(
+                       'In 2 days on July 1, 2024',
+                       $this->service->generateOccurringString($eventReader),
+                       'test patrial day recurring event in 2 days with single occurrences remaining and two exception'
+               );
+
+               /** test patrial day recurring event in 2 days with two occurrences remaining and two exception */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2');
+               $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+               $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000');
+               // construct event reader
+               $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test output
+               $this->assertEquals(
+                       'In 2 days on July 1, 2024',
+                       $this->service->generateOccurringString($eventReader),
+                       'test patrial day recurring event in 2 days with two occurrences remaining and two exception'
+               );
+
+               /** test patrial day recurring event in 2 days with three occurrences remaining and two exception */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3');
+               $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+               $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000');
+               // construct event reader
+               $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test output
+               $this->assertEquals(
+                       'In 2 days on July 1, 2024 then on July 5, 2024',
+                       $this->service->generateOccurringString($eventReader),
+                       'test patrial day recurring event in 2 days with three occurrences remaining and two exception'
+               );
+
+               /** test patrial day recurring event in 2 days with five occurrences remaining and two exception */
+               $vCalendar = clone $this->vCalendar1a;
+               $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=5');
+               $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+               $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000');
+               // construct event reader
+               $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+               // test output
+               $this->assertEquals(
+                       'In 2 days on July 1, 2024 then on July 5, 2024 and July 9, 2024',
+                       $this->service->generateOccurringString($eventReader),
+                       'test patrial day recurring event in 2 days with five occurrences remaining and two exception'
+               );
+       }
+
 }