aboutsummaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
Diffstat (limited to 'apps')
-rw-r--r--apps/dav/composer/composer/autoload_classmap.php3
-rw-r--r--apps/dav/composer/composer/autoload_static.php3
-rw-r--r--apps/dav/lib/CalDAV/EventReader.php758
-rw-r--r--apps/dav/lib/CalDAV/EventReaderRDate.php35
-rw-r--r--apps/dav/lib/CalDAV/EventReaderRRule.php87
-rw-r--r--apps/dav/lib/CalDAV/Schedule/IMipService.php515
-rw-r--r--apps/dav/tests/unit/CalDAV/EventReaderTest.php1025
-rw-r--r--apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php1307
8 files changed, 3506 insertions, 227 deletions
diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php
index ff1fb0637ad..90b09822c33 100644
--- a/apps/dav/composer/composer/autoload_classmap.php
+++ b/apps/dav/composer/composer/autoload_classmap.php
@@ -59,6 +59,9 @@ return array(
'OCA\\DAV\\CalDAV\\CalendarProvider' => $baseDir . '/../lib/CalDAV/CalendarProvider.php',
'OCA\\DAV\\CalDAV\\CalendarRoot' => $baseDir . '/../lib/CalDAV/CalendarRoot.php',
'OCA\\DAV\\CalDAV\\EventComparisonService' => $baseDir . '/../lib/CalDAV/EventComparisonService.php',
+ 'OCA\\DAV\\CalDAV\\EventReader' => $baseDir . '/../lib/CalDAV/EventReader.php',
+ 'OCA\\DAV\\CalDAV\\EventReaderRDate' => $baseDir . '/../lib/CalDAV/EventReaderRDate.php',
+ 'OCA\\DAV\\CalDAV\\EventReaderRRule' => $baseDir . '/../lib/CalDAV/EventReaderRRule.php',
'OCA\\DAV\\CalDAV\\FreeBusy\\FreeBusyGenerator' => $baseDir . '/../lib/CalDAV/FreeBusy/FreeBusyGenerator.php',
'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => $baseDir . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php',
'OCA\\DAV\\CalDAV\\IRestorable' => $baseDir . '/../lib/CalDAV/IRestorable.php',
diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php
index 081915e95f2..aa9eef72659 100644
--- a/apps/dav/composer/composer/autoload_static.php
+++ b/apps/dav/composer/composer/autoload_static.php
@@ -74,6 +74,9 @@ class ComposerStaticInitDAV
'OCA\\DAV\\CalDAV\\CalendarProvider' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarProvider.php',
'OCA\\DAV\\CalDAV\\CalendarRoot' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarRoot.php',
'OCA\\DAV\\CalDAV\\EventComparisonService' => __DIR__ . '/..' . '/../lib/CalDAV/EventComparisonService.php',
+ 'OCA\\DAV\\CalDAV\\EventReader' => __DIR__ . '/..' . '/../lib/CalDAV/EventReader.php',
+ 'OCA\\DAV\\CalDAV\\EventReaderRDate' => __DIR__ . '/..' . '/../lib/CalDAV/EventReaderRDate.php',
+ 'OCA\\DAV\\CalDAV\\EventReaderRRule' => __DIR__ . '/..' . '/../lib/CalDAV/EventReaderRRule.php',
'OCA\\DAV\\CalDAV\\FreeBusy\\FreeBusyGenerator' => __DIR__ . '/..' . '/../lib/CalDAV/FreeBusy/FreeBusyGenerator.php',
'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php',
'OCA\\DAV\\CalDAV\\IRestorable' => __DIR__ . '/..' . '/../lib/CalDAV/IRestorable.php',
diff --git a/apps/dav/lib/CalDAV/EventReader.php b/apps/dav/lib/CalDAV/EventReader.php
new file mode 100644
index 00000000000..99e5677d432
--- /dev/null
+++ b/apps/dav/lib/CalDAV/EventReader.php
@@ -0,0 +1,758 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use DateTime;
+use DateTimeInterface;
+use DateTimeZone;
+use InvalidArgumentException;
+
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Component\VEvent;
+use Sabre\VObject\Reader;
+
+class EventReader {
+
+ protected VEvent $baseEvent;
+ protected DateTimeInterface $baseEventStartDate;
+ protected DateTimeZone $baseEventStartTimeZone;
+ protected DateTimeInterface $baseEventEndDate;
+ protected DateTimeZone $baseEventEndTimeZone;
+ protected bool $baseEventStartDateFloating = false;
+ protected bool $baseEventEndDateFloating = false;
+ protected int $baseEventDuration;
+
+ protected ?EventReaderRRule $rruleIterator = null;
+ protected ?EventReaderRDate $rdateIterator = null;
+ protected ?EventReaderRRule $eruleIterator = null;
+ protected ?EventReaderRDate $edateIterator = null;
+
+ protected array $recurrenceModified;
+ protected ?DateTimeInterface $recurrenceCurrentDate;
+
+ protected array $dayNamesMap = [
+ 'MO' => 'Monday', 'TU' => 'Tuesday', 'WE' => 'Wednesday', 'TH' => 'Thursday', 'FR' => 'Friday', 'SA' => 'Saturday', 'SU' => 'Sunday'
+ ];
+ protected array $monthNamesMap = [
+ 1 => 'January', 2 => 'February', 3 => 'March', 4 => 'April', 5 => 'May', 6 => 'June',
+ 7 => 'July', 8 => 'August', 9 => 'September', 10 => 'October', 11 => 'November', 12 => 'December'
+ ];
+ protected array $relativePositionNamesMap = [
+ 1 => 'First', 2 => 'Second', 3 => 'Third', 4 => 'Fourth', 5 => 'Fifty',
+ -1 => 'Last', -2 => 'Second Last', -3 => 'Third Last', -4 => 'Fourth Last', -5 => 'Fifty Last'
+ ];
+
+ /**
+ * Initilizes the Event Reader
+ *
+ * There is several ways to set up the iterator.
+ *
+ * 1. You can pass a VCALENDAR component (as object or string) and a UID.
+ * 2. You can pass an array of VEVENTs (all UIDS should match).
+ * 3. You can pass a single VEVENT component (as object or string).
+ *
+ * Only the second method is recommended. The other 1 and 3 will be removed
+ * at some point in the future.
+ *
+ * The $uid parameter is only required for the first method.
+ *
+ * @since 30.0.0
+ *
+ * @param VCalendar|VEvent|Array|String $input
+ * @param string|null $uid
+ * @param DateTimeZone|null $timeZone reference timezone for floating dates and times
+ */
+ public function __construct(VCalendar|VEvent|array|string $input, ?string $uid = null, ?DateTimeZone $timeZone = null) {
+
+ // evaluate if the input is a string and convert it to and vobject if required
+ if (is_string($input)) {
+ $input = Reader::read($input);
+ }
+ // evaluate if input is a single event vobject and convert it to a collection
+ if ($input instanceof VEvent) {
+ $events = [$input];
+ }
+ // evaluate if input is a calendar vobject
+ elseif ($input instanceof VCalendar) {
+ // Calendar + UID mode.
+ if ($uid === null) {
+ throw new InvalidArgumentException('The UID argument is required when a VCALENDAR object is used');
+ }
+ // extract events from calendar
+ $events = $input->getByUID($uid);
+ // evaluate if any event where found
+ if (count($events) === 0) {
+ throw new InvalidArgumentException('This VCALENDAR did not have an event with UID: '.$uid);
+ }
+ // extract calendar timezone
+ if (isset($input->VTIMEZONE) && isset($input->VTIMEZONE->TZID)) {
+ $calendarTimeZone = new DateTimeZone($input->VTIMEZONE->TZID->getValue());
+ }
+ }
+ // evaluate if input is a collection of event vobjects
+ elseif (is_array($input)) {
+ $events = $input;
+ } else {
+ throw new InvalidArgumentException('Invalid input data type');
+ }
+ // find base event instance and remove it from events collection
+ foreach ($events as $key => $vevent) {
+ if (!isset($vevent->{'RECURRENCE-ID'})) {
+ $this->baseEvent = $vevent;
+ unset($events[$key]);
+ }
+ }
+
+ // No base event was found. CalDAV does allow cases where only
+ // overridden instances are stored.
+ //
+ // In this particular case, we're just going to grab the first
+ // event and use that instead. This may not always give the
+ // desired result.
+ if (!isset($this->baseEvent) && count($events) > 0) {
+ $this->baseEvent = array_shift($events);
+ }
+
+ // determain the event starting time zone
+ // we require this to align all other dates times
+ // evaluate if timezone paramater was used (treat this as a override)
+ if ($timeZone !== null) {
+ $this->baseEventStartTimeZone = $timeZone;
+ }
+ // evaluate if event start date has a timezone parameter
+ elseif (isset($this->baseEvent->DTSTART->parameters['TZID'])) {
+ $this->baseEventStartTimeZone = new DateTimeZone($this->baseEvent->DTSTART->parameters['TZID']->getValue());
+ }
+ // evaluate if event parent calendar has a time zone
+ elseif (isset($calendarTimeZone)) {
+ $this->baseEventStartTimeZone = clone $calendarTimeZone;
+ }
+ // otherwise, as a last resort use the UTC timezone
+ else {
+ $this->baseEventStartTimeZone = new DateTimeZone('UTC');
+ }
+
+ // determain the event end time zone
+ // we require this to align all other dates and times
+ // evaluate if timezone paramater was used (treat this as a override)
+ if ($timeZone !== null) {
+ $this->baseEventEndTimeZone = $timeZone;
+ }
+ // evaluate if event end date has a timezone parameter
+ elseif (isset($this->baseEvent->DTEND->parameters['TZID'])) {
+ $this->baseEventEndTimeZone = new DateTimeZone($this->baseEvent->DTEND->parameters['TZID']->getValue());
+ }
+ // evaluate if event parent calendar has a time zone
+ elseif (isset($calendarTimeZone)) {
+ $this->baseEventEndTimeZone = clone $calendarTimeZone;
+ }
+ // otherwise, as a last resort use the start date time zone
+ else {
+ $this->baseEventEndTimeZone = clone $this->baseEventStartTimeZone;
+ }
+ // extract start date and time
+ $this->baseEventStartDate = $this->baseEvent->DTSTART->getDateTime($this->baseEventStartTimeZone);
+ $this->baseEventStartDateFloating = $this->baseEvent->DTSTART->isFloating();
+ // determine event end date and duration
+ // evaluate if end date exists
+ // extract end date and calculate duration
+ if (isset($this->baseEvent->DTEND)) {
+ $this->baseEventEndDate = $this->baseEvent->DTEND->getDateTime($this->baseEventEndTimeZone);
+ $this->baseEventEndDateFloating = $this->baseEvent->DTEND->isFloating();
+ $this->baseEventDuration =
+ $this->baseEvent->DTEND->getDateTime($this->baseEventEndTimeZone)->getTimeStamp() -
+ $this->baseEventStartDate->getTimeStamp();
+ }
+ // evaluate if duration exists
+ // extract duration and calculate end date
+ elseif (isset($this->baseEvent->DURATION)) {
+ $this->baseEventDuration = $this->baseEvent->DURATION->getDateInterval();
+ $this->baseEventEndDate = ((clone $this->baseEventStartDate)->add($this->baseEventDuration));
+ }
+ // evaluate if start date is floating
+ // set duration to 24 hours and calculate the end date
+ // according to the rfc any event without a end date or duration is a complete day
+ elseif ($this->baseEventStartDateFloating == true) {
+ $this->baseEventDuration = 86400;
+ $this->baseEventEndDate = ((clone $this->baseEventStartDate)->add($this->baseEventDuration));
+ }
+ // otherwise, set duration to zero this should never happen
+ else {
+ $this->baseEventDuration = 0;
+ $this->baseEventEndDate = $this->baseEventStartDate;
+ }
+ // evaluate if RRULE exist and construct iterator
+ if (isset($this->baseEvent->RRULE)) {
+ $this->rruleIterator = new EventReaderRRule(
+ $this->baseEvent->RRULE->getParts(),
+ $this->baseEventStartDate
+ );
+ }
+ // evaluate if RDATE exist and construct iterator
+ if (isset($this->baseEvent->RDATE)) {
+ $this->rdateIterator = new EventReaderRDate(
+ $this->baseEvent->RDATE->getValue(),
+ $this->baseEventStartDate
+ );
+ }
+ // evaluate if EXRULE exist and construct iterator
+ if (isset($this->baseEvent->EXRULE)) {
+ $this->eruleIterator = new EventReaderRRule(
+ $this->baseEvent->EXRULE->getParts(),
+ $this->baseEventStartDate
+ );
+ }
+ // evaluate if EXDATE exist and construct iterator
+ if (isset($this->baseEvent->EXDATE)) {
+ $this->edateIterator = new EventReaderRDate(
+ $this->baseEvent->EXDATE->getValue(),
+ $this->baseEventStartDate
+ );
+ }
+ // construct collection of modified events with recurrence id as hash
+ foreach ($events as $vevent) {
+ $this->recurrenceModified[$vevent->{'RECURRENCE-ID'}->getDateTime($this->baseEventStartTimeZone)->getTimeStamp()] = $vevent;
+ }
+
+ $this->recurrenceCurrentDate = clone $this->baseEventStartDate;
+ }
+
+ /**
+ * retrieve date and time of event start
+ *
+ * @since 30.0.0
+ *
+ * @return DateTime
+ */
+ public function startDateTime(): DateTime {
+ return DateTime::createFromInterface($this->baseEventStartDate);
+ }
+
+ /**
+ * retrieve time zone of event start
+ *
+ * @since 30.0.0
+ *
+ * @return DateTimeZone
+ */
+ public function startTimeZone(): DateTimeZone {
+ return $this->baseEventStartTimeZone;
+ }
+
+ /**
+ * retrieve date and time of event end
+ *
+ * @since 30.0.0
+ *
+ * @return DateTime
+ */
+ public function endDateTime(): DateTime {
+ return DateTime::createFromInterface($this->baseEventEndDate);
+ }
+
+ /**
+ * retrieve time zone of event end
+ *
+ * @since 30.0.0
+ *
+ * @return DateTimeZone
+ */
+ public function endTimeZone(): DateTimeZone {
+ return $this->baseEventEndTimeZone;
+ }
+
+ /**
+ * is this an all day event
+ *
+ * @since 30.0.0
+ *
+ * @return bool
+ */
+ public function entireDay(): bool {
+ return $this->baseEventStartDateFloating;
+ }
+
+ /**
+ * is this a recurring event
+ *
+ * @since 30.0.0
+ *
+ * @return bool
+ */
+ public function recurs(): bool {
+ return ($this->rruleIterator !== null || $this->rdateIterator !== null);
+ }
+
+ /**
+ * event recurrence pattern
+ *
+ * @since 30.0.0
+ *
+ * @return string|null R - Relative or A - Absolute
+ */
+ public function recurringPattern(): string | null {
+ if ($this->rruleIterator === null && $this->rdateIterator === null) {
+ return null;
+ }
+ if ($this->rruleIterator?->isRelative()) {
+ return 'R';
+ }
+ return 'A';
+ }
+
+ /**
+ * event recurrence precision
+ *
+ * @since 30.0.0
+ *
+ * @return string|null daily, weekly, monthly, yearly, fixed
+ */
+ public function recurringPrecision(): string | null {
+ if ($this->rruleIterator !== null) {
+ return $this->rruleIterator->precision();
+ }
+ if ($this->rdateIterator !== null) {
+ return 'fixed';
+ }
+ return null;
+ }
+
+ /**
+ * event recurrence interval
+ *
+ * @since 30.0.0
+ *
+ * @return int|null
+ */
+ public function recurringInterval(): int | null {
+ return $this->rruleIterator?->interval();
+ }
+
+ /**
+ * event recurrence conclusion
+ *
+ * returns true if RRULE with UNTIL or COUNT (calculated) is used
+ * returns true RDATE is used
+ * returns false if RRULE or RDATE are absent, or RRRULE is infinite
+ *
+ * @since 30.0.0
+ *
+ * @return bool
+ */
+ public function recurringConcludes(): bool {
+
+ // retrieve rrule conclusions
+ if ($this->rruleIterator?->concludesOn() !== null ||
+ $this->rruleIterator?->concludesAfter() !== null) {
+ return true;
+ }
+ // retrieve rdate conclusions
+ if ($this->rdateIterator?->concludesAfter() !== null) {
+ return true;
+ }
+
+ return false;
+
+ }
+
+ /**
+ * event recurrence conclusion iterations
+ *
+ * returns the COUNT value if RRULE is used
+ * returns the collection count if RDATE is used
+ * returns combined count of RRULE COUNT and RDATE if both are used
+ * returns null if RRULE and RDATE are absent
+ *
+ * @since 30.0.0
+ *
+ * @return int|null
+ */
+ public function recurringConcludesAfter(): int | null {
+
+ // construct count place holder
+ $count = 0;
+ // retrieve and add RRULE iterations count
+ $count += (int) $this->rruleIterator?->concludesAfter();
+ // retrieve and add RDATE iterations count
+ $count += (int) $this->rdateIterator?->concludesAfter();
+ // return count
+ return !empty($count) ? $count : null;
+
+ }
+
+ /**
+ * event recurrence conclusion date
+ *
+ * returns the last date of UNTIL or COUNT (calculated) if RRULE is used
+ * returns the last date in the collection if RDATE is used
+ * returns the highest date if both RRULE and RDATE are used
+ * returns null if RRULE and RDATE are absent or RRULE is infinite
+ *
+ * @since 30.0.0
+ *
+ * @return DateTime|null
+ */
+ public function recurringConcludesOn(): DateTime | null {
+
+ if ($this->rruleIterator !== null) {
+ // retrieve rrule conclusion date
+ $rrule = $this->rruleIterator->concludes();
+ // evaluate if rrule conclusion is null
+ // if this is null that means the recurrence is infinate
+ if ($rrule === null) {
+ return null;
+ }
+ }
+ // retrieve rdate conclusion date
+ if ($this->rdateIterator !== null) {
+ $rdate = $this->rdateIterator->concludes();
+ }
+ // evaluate if both rrule and rdate have date
+ if (isset($rdate) && isset($rrule)) {
+ // return the highest date
+ return (($rdate > $rrule) ? $rdate : $rrule);
+ } elseif (isset($rrule)) {
+ return $rrule;
+ } elseif (isset($rdate)) {
+ return $rdate;
+ }
+
+ return null;
+
+ }
+
+ /**
+ * event recurrence days of the week
+ *
+ * returns collection of RRULE BYDAY day(s) ['MO','WE','FR']
+ * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringDaysOfWeek(): array {
+ // evaluate if RRULE exists and return day(s) of the week
+ return $this->rruleIterator !== null ? $this->rruleIterator->daysOfWeek() : [];
+ }
+
+ /**
+ * event recurrence days of the week (named)
+ *
+ * returns collection of RRULE BYDAY day(s) ['Monday','Wednesday','Friday']
+ * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringDaysOfWeekNamed(): array {
+ // evaluate if RRULE exists and extract day(s) of the week
+ $days = $this->rruleIterator !== null ? $this->rruleIterator->daysOfWeek() : [];
+ // convert numberic month to month name
+ foreach ($days as $key => $value) {
+ $days[$key] = $this->dayNamesMap[$value];
+ }
+ // return names collection
+ return $days;
+ }
+
+ /**
+ * event recurrence days of the month
+ *
+ * returns collection of RRULE BYMONTHDAY day(s) [7, 15, 31]
+ * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringDaysOfMonth(): array {
+ // evaluate if RRULE exists and return day(s) of the month
+ return $this->rruleIterator !== null ? $this->rruleIterator->daysOfMonth() : [];
+ }
+
+ /**
+ * event recurrence days of the year
+ *
+ * returns collection of RRULE BYYEARDAY day(s) [57, 205, 365]
+ * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringDaysOfYear(): array {
+ // evaluate if RRULE exists and return day(s) of the year
+ return $this->rruleIterator !== null ? $this->rruleIterator->daysOfYear() : [];
+ }
+
+ /**
+ * event recurrence weeks of the month
+ *
+ * returns collection of RRULE SETPOS weeks(s) [1, 3, -1]
+ * returns blank collection if RRULE is absent or SETPOS is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringWeeksOfMonth(): array {
+ // evaluate if RRULE exists and RRULE is relative return relative position(s)
+ return $this->rruleIterator?->isRelative() ? $this->rruleIterator->relativePosition() : [];
+ }
+
+ /**
+ * event recurrence weeks of the month (named)
+ *
+ * returns collection of RRULE SETPOS weeks(s) [1, 3, -1]
+ * returns blank collection if RRULE is absent or SETPOS is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringWeeksOfMonthNamed(): array {
+ // evaluate if RRULE exists and extract relative position(s)
+ $positions = $this->rruleIterator?->isRelative() ? $this->rruleIterator->relativePosition() : [];
+ // convert numberic relative position to relative label
+ foreach ($positions as $key => $value) {
+ $positions[$key] = $this->relativePositionNamesMap[$value];
+ }
+ // return positions collection
+ return $positions;
+ }
+
+ /**
+ * event recurrence weeks of the year
+ *
+ * returns collection of RRULE BYWEEKNO weeks(s) [12, 32, 52]
+ * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringWeeksOfYear(): array {
+ // evaluate if RRULE exists and return weeks(s) of the year
+ return $this->rruleIterator !== null ? $this->rruleIterator->weeksOfYear() : [];
+ }
+
+ /**
+ * event recurrence months of the year
+ *
+ * returns collection of RRULE BYMONTH month(s) [3, 7, 12]
+ * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringMonthsOfYear(): array {
+ // evaluate if RRULE exists and return month(s) of the year
+ return $this->rruleIterator !== null ? $this->rruleIterator->monthsOfYear() : [];
+ }
+
+ /**
+ * event recurrence months of the year (named)
+ *
+ * returns collection of RRULE BYMONTH month(s) [3, 7, 12]
+ * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringMonthsOfYearNamed(): array {
+ // evaluate if RRULE exists and extract month(s) of the year
+ $months = $this->rruleIterator !== null ? $this->rruleIterator->monthsOfYear() : [];
+ // convert numberic month to month name
+ foreach ($months as $key => $value) {
+ $months[$key] = $this->monthNamesMap[$value];
+ }
+ // return months collection
+ return $months;
+ }
+
+ /**
+ * event recurrence relative positions
+ *
+ * returns collection of RRULE SETPOS value(s) [1, 5, -3]
+ * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringRelativePosition(): array {
+ // evaluate if RRULE exists and return relative position(s)
+ return $this->rruleIterator !== null ? $this->rruleIterator->relativePosition() : [];
+ }
+
+ /**
+ * event recurrence relative positions (named)
+ *
+ * returns collection of RRULE SETPOS [1, 3, -1]
+ * returns blank collection if RRULE is absent or SETPOS is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringRelativePositionNamed(): array {
+ // evaluate if RRULE exists and extract relative position(s)
+ $positions = $this->rruleIterator?->isRelative() ? $this->rruleIterator->relativePosition() : [];
+ // convert numberic relative position to relative label
+ foreach ($positions as $key => $value) {
+ $positions[$key] = $this->relativePositionNamesMap[$value];
+ }
+ // return positions collection
+ return $positions;
+ }
+
+ /**
+ * event recurrence date
+ *
+ * returns date of currently selected recurrence
+ *
+ * @since 30.0.0
+ *
+ * @return DateTime
+ */
+ public function recurrenceDate(): DateTime | null {
+ if ($this->recurrenceCurrentDate !== null) {
+ return DateTime::createFromInterface($this->recurrenceCurrentDate);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * event recurrence rewind
+ *
+ * sets the current recurrence to the first recurrence in the collection
+ *
+ * @since 30.0.0
+ *
+ * @return void
+ */
+ public function recurrenceRewind(): void {
+ // rewind and increment rrule
+ if ($this->rruleIterator !== null) {
+ $this->rruleIterator->rewind();
+ }
+ // rewind and increment rdate
+ if ($this->rdateIterator !== null) {
+ $this->rdateIterator->rewind();
+ }
+ // rewind and increment exrule
+ if ($this->eruleIterator !== null) {
+ $this->eruleIterator->rewind();
+ }
+ // rewind and increment exdate
+ if ($this->edateIterator !== null) {
+ $this->edateIterator->rewind();
+ }
+ // set current date to event start date
+ $this->recurrenceCurrentDate = clone $this->baseEventStartDate;
+ }
+
+ /**
+ * event recurrence advance
+ *
+ * sets the current recurrence to the next recurrence in the collection
+ *
+ * @since 30.0.0
+ *
+ * @return void
+ */
+ public function recurrenceAdvance(): void {
+ // place holders
+ $nextOccurrenceDate = null;
+ $nextExceptionDate = null;
+ $rruleDate = null;
+ $rdateDate = null;
+ $eruleDate = null;
+ $edateDate = null;
+ // evaludate if rrule is set and advance one interation past current date
+ if ($this->rruleIterator !== null) {
+ // forward rrule to the next future date
+ while ($this->rruleIterator->valid() && $this->rruleIterator->current() <= $this->recurrenceCurrentDate) {
+ $this->rruleIterator->next();
+ }
+ $rruleDate = $this->rruleIterator->current();
+ }
+ // evaludate if rdate is set and advance one interation past current date
+ if ($this->rdateIterator !== null) {
+ // forward rdate to the next future date
+ while ($this->rdateIterator->valid() && $this->rdateIterator->current() <= $this->recurrenceCurrentDate) {
+ $this->rdateIterator->next();
+ }
+ $rdateDate = $this->rdateIterator->current();
+ }
+ if ($rruleDate !== null && $rdateDate !== null) {
+ $nextOccurrenceDate = ($rruleDate <= $rdateDate) ? $rruleDate : $rdateDate;
+ } elseif ($rruleDate !== null) {
+ $nextOccurrenceDate = $rruleDate;
+ } elseif ($rdateDate !== null) {
+ $nextOccurrenceDate = $rdateDate;
+ }
+
+ // evaludate if exrule is set and advance one interation past current date
+ if ($this->eruleIterator !== null) {
+ // forward exrule to the next future date
+ while ($this->eruleIterator->valid() && $this->eruleIterator->current() <= $this->recurrenceCurrentDate) {
+ $this->eruleIterator->next();
+ }
+ $eruleDate = $this->eruleIterator->current();
+ }
+ // evaludate if exdate is set and advance one interation past current date
+ if ($this->edateIterator !== null) {
+ // forward exdate to the next future date
+ while ($this->edateIterator->valid() && $this->edateIterator->current() <= $this->recurrenceCurrentDate) {
+ $this->edateIterator->next();
+ }
+ $edateDate = $this->edateIterator->current();
+ }
+ // evaludate if exrule and exdate are set and set nextExDate to the first next date
+ if ($eruleDate !== null && $edateDate !== null) {
+ $nextExceptionDate = ($eruleDate <= $edateDate) ? $eruleDate : $edateDate;
+ } elseif ($eruleDate !== null) {
+ $nextExceptionDate = $eruleDate;
+ } elseif ($edateDate !== null) {
+ $nextExceptionDate = $edateDate;
+ }
+ // if the next date is part of exrule or exdate find another date
+ if ($nextOccurrenceDate !== null && $nextExceptionDate !== null && $nextOccurrenceDate == $nextExceptionDate) {
+ $this->recurrenceCurrentDate = $nextOccurrenceDate;
+ $this->recurrenceAdvance();
+ } else {
+ $this->recurrenceCurrentDate = $nextOccurrenceDate;
+ }
+ }
+
+ /**
+ * event recurrence advance
+ *
+ * sets the current recurrence to the next recurrence in the collection after the specific date
+ *
+ * @since 30.0.0
+ *
+ * @param DateTimeInterface $dt date and time to advance
+ *
+ * @return void
+ */
+ public function recurrenceAdvanceTo(DateTimeInterface $dt): void {
+ while ($this->recurrenceCurrentDate !== null && $this->recurrenceCurrentDate < $dt) {
+ $this->recurrenceAdvance();
+ }
+ }
+
+}
diff --git a/apps/dav/lib/CalDAV/EventReaderRDate.php b/apps/dav/lib/CalDAV/EventReaderRDate.php
new file mode 100644
index 00000000000..65362be4b07
--- /dev/null
+++ b/apps/dav/lib/CalDAV/EventReaderRDate.php
@@ -0,0 +1,35 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use DateTime;
+
+class EventReaderRDate extends \Sabre\VObject\Recur\RDateIterator {
+
+ public function concludes(): DateTime | null {
+ return $this->concludesOn();
+ }
+
+ public function concludesAfter(): int | null {
+ return !empty($this->dates) ? count($this->dates) : null;
+ }
+
+ public function concludesOn(): DateTime | null {
+ if (count($this->dates) > 0) {
+ return new DateTime(
+ $this->dates[array_key_last($this->dates)],
+ $this->startDate->getTimezone()
+ );
+ }
+
+ return null;
+ }
+
+}
diff --git a/apps/dav/lib/CalDAV/EventReaderRRule.php b/apps/dav/lib/CalDAV/EventReaderRRule.php
new file mode 100644
index 00000000000..965abb4c9cd
--- /dev/null
+++ b/apps/dav/lib/CalDAV/EventReaderRRule.php
@@ -0,0 +1,87 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use DateTime;
+use DateTimeInterface;
+
+class EventReaderRRule extends \Sabre\VObject\Recur\RRuleIterator {
+
+ public function precision(): string {
+ return $this->frequency;
+ }
+
+ public function interval(): int {
+ return $this->interval;
+ }
+
+ public function concludes(): DateTime | null {
+ // evaluate if until value is a date
+ if ($this->until instanceof DateTimeInterface) {
+ return DateTime::createFromInterface($this->until);
+ }
+ // evaluate if count value is higher than 0
+ if ($this->count > 0) {
+ // temporarily store current recurrence date and counter
+ $currentReccuranceDate = $this->currentDate;
+ $currentCounter = $this->counter;
+ // iterate over occurrences until last one (subtract 2 from count for start and end occurrence)
+ while ($this->counter <= ($this->count - 2)) {
+ $this->next();
+ }
+ // temporarly store last reccurance date
+ $lastReccuranceDate = $this->currentDate;
+ // restore current recurrence date and counter
+ $this->currentDate = $currentReccuranceDate;
+ $this->counter = $currentCounter;
+ // return last recurrence date
+ return DateTime::createFromInterface($lastReccuranceDate);
+ }
+
+ return null;
+ }
+
+ public function concludesAfter(): int | null {
+ return !empty($this->count) ? $this->count : null;
+ }
+
+ public function concludesOn(): DateTime | null {
+ return isset($this->until) ? DateTime::createFromInterface($this->until) : null;
+ }
+
+ public function daysOfWeek(): array {
+ return $this->byDay;
+ }
+
+ public function daysOfMonth(): array {
+ return $this->byMonthDay;
+ }
+
+ public function daysOfYear(): array {
+ return $this->byYearDay;
+ }
+
+ public function weeksOfYear(): array {
+ return $this->byWeekNo;
+ }
+
+ public function monthsOfYear(): array {
+ return $this->byMonth;
+ }
+
+ public function isRelative(): bool {
+ return isset($this->bySetPos);
+ }
+
+ public function relativePosition(): array {
+ return $this->bySetPos;
+ }
+
+}
diff --git a/apps/dav/lib/CalDAV/Schedule/IMipService.php b/apps/dav/lib/CalDAV/Schedule/IMipService.php
index 5141f519588..d3ad9a79254 100644
--- a/apps/dav/lib/CalDAV/Schedule/IMipService.php
+++ b/apps/dav/lib/CalDAV/Schedule/IMipService.php
@@ -9,6 +9,8 @@ declare(strict_types=1);
namespace OCA\DAV\CalDAV\Schedule;
use OC\URLGenerator;
+use OCA\DAV\CalDAV\EventReader;
+use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IL10N;
@@ -31,6 +33,7 @@ class IMipService {
private ISecureRandom $random;
private L10NFactory $l10nFactory;
private IL10N $l10n;
+ private ITimeFactory $timeFactory;
/** @var string[] */
private const STRING_DIFF = [
@@ -44,7 +47,8 @@ class IMipService {
IConfig $config,
IDBConnection $db,
ISecureRandom $random,
- L10NFactory $l10nFactory) {
+ L10NFactory $l10nFactory,
+ ITimeFactory $timeFactory) {
$this->urlGenerator = $urlGenerator;
$this->config = $config;
$this->db = $db;
@@ -52,6 +56,7 @@ class IMipService {
$this->l10nFactory = $l10nFactory;
$default = $this->l10nFactory->findGenericLanguage();
$this->l10n = $this->l10nFactory->get('dav', $default);
+ $this->timeFactory = $timeFactory;
}
/**
@@ -130,9 +135,13 @@ class IMipService {
* @return array
*/
public function buildBodyData(VEvent $vEvent, ?VEvent $oldVEvent): array {
+
+ // construct event reader
+ $eventReaderCurrent = new EventReader($vEvent);
+ $eventReaderPrevious = !empty($oldVEvent) ? new EventReader($oldVEvent) : null;
$defaultVal = '';
$data = [];
- $data['meeting_when'] = $this->generateWhenString($vEvent);
+ $data['meeting_when'] = $this->generateWhenString($eventReaderCurrent);
foreach(self::STRING_DIFF as $key => $property) {
$data[$key] = self::readPropertyWithDefault($vEvent, $property, $defaultVal);
@@ -145,7 +154,7 @@ class IMipService {
}
if(!empty($oldVEvent)) {
- $oldMeetingWhen = $this->generateWhenString($oldVEvent);
+ $oldMeetingWhen = $this->generateWhenString($eventReaderPrevious);
$data['meeting_title_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'SUMMARY', $data['meeting_title']);
$data['meeting_description_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'DESCRIPTION', $data['meeting_description']);
$data['meeting_location_html'] = $this->generateLinkifiedDiffString($vEvent, $oldVEvent, 'LOCATION', $data['meeting_location']);
@@ -153,107 +162,334 @@ class IMipService {
$oldUrl = self::readPropertyWithDefault($oldVEvent, 'URL', $defaultVal);
$data['meeting_url_html'] = !empty($oldUrl) && $oldUrl !== $data['meeting_url'] ? sprintf('<a href="%1$s">%1$s</a>', $oldUrl) : $data['meeting_url'];
- $data['meeting_when_html'] =
- ($oldMeetingWhen !== $data['meeting_when'] && $oldMeetingWhen !== null)
- ? sprintf("<span style='text-decoration: line-through'>%s</span><br />%s", $oldMeetingWhen, $data['meeting_when'])
- : $data['meeting_when'];
+ $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
+ if ($eventReaderCurrent->recurs()) {
+ $data['meeting_occurring'] = $this->generateOccurringString($eventReaderCurrent);
}
+
return $data;
}
/**
- * @param IL10N $this->l10n
- * @param VEvent $vevent
- * @return false|int|string
+ * genarates a when string based on if a event has an recurrence or not
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
*/
- public function generateWhenString(VEvent $vevent) {
- /** @var Property\ICalendar\DateTime $dtstart */
- $dtstart = $vevent->DTSTART;
- if (isset($vevent->DTEND)) {
- /** @var Property\ICalendar\DateTime $dtend */
- $dtend = $vevent->DTEND;
- } elseif (isset($vevent->DURATION)) {
- $isFloating = $dtstart->isFloating();
- $dtend = clone $dtstart;
- $endDateTime = $dtend->getDateTime();
- $endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
- $dtend->setDateTime($endDateTime, $isFloating);
- } elseif (!$dtstart->hasTime()) {
- $isFloating = $dtstart->isFloating();
- $dtend = clone $dtstart;
- $endDateTime = $dtend->getDateTime();
- $endDateTime = $endDateTime->modify('+1 day');
- $dtend->setDateTime($endDateTime, $isFloating);
- } else {
- $dtend = clone $dtstart;
- }
-
- /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
- /** @var \DateTimeImmutable $dtstartDt */
- $dtstartDt = $dtstart->getDateTime();
+ public function generateWhenString(EventReader $er): string {
+ return match ($er->recurs()) {
+ true => $this->generateWhenStringRecurring($er),
+ false => $this->generateWhenStringSingular($er)
+ };
+ }
- /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
- /** @var \DateTimeImmutable $dtendDt */
- $dtendDt = $dtend->getDateTime();
+ /**
+ * genarates a when string for a non recurring event
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @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()));
+ // extract start date
+ $startDate = $this->l10n->l('date', $er->startDateTime(), ['width' => 'full']);
+ // time of the day
+ if (!$er->entireDay()) {
+ $startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']);
+ $startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : '';
+ $endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')';
+ }
+ // generate localized when string
+ 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]),
+ default => $this->l10n->t('Could not generate when statement')
+ };
+ }
- $diff = $dtstartDt->diff($dtendDt);
+ /**
+ * genarates a when string based on recurrance precision/frequency
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateWhenStringRecurring(EventReader $er): string {
+ return match ($er->recurringPrecision()) {
+ 'daily' => $this->generateWhenStringRecurringDaily($er),
+ 'weekly' => $this->generateWhenStringRecurringWeekly($er),
+ 'monthly' => $this->generateWhenStringRecurringMonthly($er),
+ 'yearly' => $this->generateWhenStringRecurringYearly($er),
+ 'fixed' => $this->generateWhenStringRecurringFixed($er),
+ };
+ }
- $dtstartDt = new \DateTime($dtstartDt->format(\DateTimeInterface::ATOM));
- $dtendDt = new \DateTime($dtendDt->format(\DateTimeInterface::ATOM));
+ /**
+ * genarates a when string for a daily precision/frequency
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateWhenStringRecurringDaily(EventReader $er): string {
+
+ // initialize
+ $interval = (int) $er->recurringInterval();
+ $startTime = '';
+ $endTime = '';
+ $conclusion = '';
+ // time of the day
+ if (!$er->entireDay()) {
+ $startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']);
+ $startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : '';
+ $endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')';
+ }
+ // conclusion
+ if ($er->recurringConcludes()) {
+ $conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']);
+ }
+ // generate localized when string
+ return match ([($interval > 1), !empty($startTime), !empty($conclusion)]) {
+ [false, false, false] => $this->l10n->t('Every Day for the entire day'),
+ [false, false, true] => $this->l10n->t('Every Day for the entire day until %1$s', [$conclusion]),
+ [false, true, false] => $this->l10n->t('Every Day between %1$s - %2$s', [$startTime, $endTime]),
+ [false, true, true] => $this->l10n->t('Every Day between %1$s - %2$s until %3$s', [$startTime, $endTime, $conclusion]),
+ [true, false, false] => $this->l10n->t('Every %1$d Days for the entire day', [$interval]),
+ [true, false, true] => $this->l10n->t('Every %1$d Days for the entire day until %2$s', [$interval, $conclusion]),
+ [true, true, false] => $this->l10n->t('Every %1$d Days between %2$s - %3$s', [$interval, $startTime, $endTime]),
+ [true, true, true] => $this->l10n->t('Every %1$d Days between %2$s - %3$s until %4$s', [$interval, $startTime, $endTime, $conclusion]),
+ default => $this->l10n->t('Could not generate event recurrence statement')
+ };
- if ($dtstart instanceof Property\ICalendar\Date) {
- // One day event
- if ($diff->days === 1) {
- return $this->l10n->l('date', $dtstartDt, ['width' => 'medium']);
- }
+ }
- // DTEND is exclusive, so if the ics data says 2020-01-01 to 2020-01-05,
- // the email should show 2020-01-01 to 2020-01-04.
- $dtendDt->modify('-1 day');
+ /**
+ * genarates a when string for a weekly precision/frequency
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateWhenStringRecurringWeekly(EventReader $er): string {
+
+ // initialize
+ $interval = (int) $er->recurringInterval();
+ $startTime = '';
+ $endTime = '';
+ $conclusion = '';
+ // days of the week
+ $days = implode(', ', array_map(function ($value) { return $this->localizeDayName($value); }, $er->recurringDaysOfWeekNamed()));
+ // time of the day
+ if (!$er->entireDay()) {
+ $startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']);
+ $startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : '';
+ $endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')';
+ }
+ // conclusion
+ if ($er->recurringConcludes()) {
+ $conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']);
+ }
+ // generate localized when string
+ return match ([($interval > 1), !empty($startTime), !empty($conclusion)]) {
+ [false, false, false] => $this->l10n->t('Every Week on %1$s for the entire day', [$days]),
+ [false, false, true] => $this->l10n->t('Every Week on %1$s for the entire day until %2$s', [$days, $conclusion]),
+ [false, true, false] => $this->l10n->t('Every Week on %1$s between %2$s - %3$s', [$days, $startTime, $endTime]),
+ [false, true, true] => $this->l10n->t('Every Week on %1$s between %2$s - %3$s until %4$s', [$days, $startTime, $endTime, $conclusion]),
+ [true, false, false] => $this->l10n->t('Every %1$d Weeks on %2$s for the entire day', [$interval, $days]),
+ [true, false, true] => $this->l10n->t('Every %1$d Weeks on %2$s for the entire day until %3$s', [$interval, $days, $conclusion]),
+ [true, true, false] => $this->l10n->t('Every %1$d Weeks on %2$s between %3$s - %4$s', [$interval, $days, $startTime, $endTime]),
+ [true, true, true] => $this->l10n->t('Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s', [$interval, $days, $startTime, $endTime, $conclusion]),
+ default => $this->l10n->t('Could not generate event recurrence statement')
+ };
- //event that spans over multiple days
- $localeStart = $this->l10n->l('date', $dtstartDt, ['width' => 'medium']);
- $localeEnd = $this->l10n->l('date', $dtendDt, ['width' => 'medium']);
+ }
- return $localeStart . ' - ' . $localeEnd;
- }
+ /**
+ * genarates a when string for a monthly precision/frequency
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateWhenStringRecurringMonthly(EventReader $er): string {
+
+ // initialize
+ $interval = (int) $er->recurringInterval();
+ $startTime = '';
+ $endTime = '';
+ $conclusion = '';
+ // days of month
+ if ($er->recurringPattern() === 'R') {
+ $days = implode(', ', array_map(function ($value) { return $this->localizeRelativePositionName($value); }, $er->recurringRelativePositionNamed())) . ' ' .
+ implode(', ', array_map(function ($value) { return $this->localizeDayName($value); }, $er->recurringDaysOfWeekNamed()));
+ } else {
+ $days = implode(', ', $er->recurringDaysOfMonth());
+ }
+ // time of the day
+ if (!$er->entireDay()) {
+ $startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']);
+ $startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : '';
+ $endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')';
+ }
+ // conclusion
+ if ($er->recurringConcludes()) {
+ $conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']);
+ }
+ // generate localized when string
+ return match ([($interval > 1), !empty($startTime), !empty($conclusion)]) {
+ [false, false, false] => $this->l10n->t('Every Month on the %1$s for the entire day', [$days]),
+ [false, false, true] => $this->l10n->t('Every Month on the %1$s for the entire day until %2$s', [$days, $conclusion]),
+ [false, true, false] => $this->l10n->t('Every Month on the %1$s between %2$s - %3$s', [$days, $startTime, $endTime]),
+ [false, true, true] => $this->l10n->t('Every Month on the %1$s between %2$s - %3$s until %4$s', [$days, $startTime, $endTime, $conclusion]),
+ [true, false, false] => $this->l10n->t('Every %1$d Months on the %2$s for the entire day', [$interval, $days]),
+ [true, false, true] => $this->l10n->t('Every %1$d Months on the %2$s for the entire day until %3$s', [$interval, $days, $conclusion]),
+ [true, true, false] => $this->l10n->t('Every %1$d Months on the %2$s between %3$s - %4$s', [$interval, $days, $startTime, $endTime]),
+ [true, true, true] => $this->l10n->t('Every %1$d Months on the %2$s between %3$s - %4$s until %5$s', [$interval, $days, $startTime, $endTime, $conclusion]),
+ default => $this->l10n->t('Could not generate event recurrence statement')
+ };
+ }
- /** @var Property\ICalendar\DateTime $dtstart */
- /** @var Property\ICalendar\DateTime $dtend */
- $isFloating = $dtstart->isFloating();
- $startTimezone = $endTimezone = null;
- if (!$isFloating) {
- $prop = $dtstart->offsetGet('TZID');
- if ($prop instanceof Parameter) {
- $startTimezone = $prop->getValue();
- }
+ /**
+ * genarates a when string for a yearly precision/frequency
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateWhenStringRecurringYearly(EventReader $er): string {
+
+ // initialize
+ $interval = (int) $er->recurringInterval();
+ $startTime = '';
+ $endTime = '';
+ $conclusion = '';
+ // months of year
+ $months = implode(', ', array_map(function ($value) { return $this->localizeMonthName($value); }, $er->recurringMonthsOfYearNamed()));
+ // days of month
+ if ($er->recurringPattern() === 'R') {
+ $days = implode(', ', array_map(function ($value) { return $this->localizeRelativePositionName($value); }, $er->recurringRelativePositionNamed())) . ' ' .
+ implode(', ', array_map(function ($value) { return $this->localizeDayName($value); }, $er->recurringDaysOfWeekNamed()));
+ } else {
+ $days = $er->startDateTime()->format('jS');
+ }
+ // time of the day
+ if (!$er->entireDay()) {
+ $startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']);
+ $startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : '';
+ $endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')';
+ }
+ // conclusion
+ if ($er->recurringConcludes()) {
+ $conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']);
+ }
+ // generate localized when string
+ return match ([($interval > 1), !empty($startTime), !empty($conclusion)]) {
+ [false, false, false] => $this->l10n->t('Every Year in %1$s on the %2$s for the entire day', [$months, $days]),
+ [false, false, true] => $this->l10n->t('Every Year in %1$s on the %2$s for the entire day until %3$s', [$months, $days, $conclusion]),
+ [false, true, false] => $this->l10n->t('Every Year in %1$s on the %2$s between %3$s - %4$s', [$months, $days, $startTime, $endTime]),
+ [false, true, true] => $this->l10n->t('Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s', [$months, $days, $startTime, $endTime, $conclusion]),
+ [true, false, false] => $this->l10n->t('Every %1$d Years in %2$s on the %3$s for the entire day', [$interval, $months, $days]),
+ [true, false, true] => $this->l10n->t('Every %1$d Years in %2$s on the %3$s for the entire day until %4$s', [$interval, $months, $days, $conclusion]),
+ [true, true, false] => $this->l10n->t('Every %1$d Years in %2$s on the %3$s between %4$s - %5$s', [$interval, $months, $days, $startTime, $endTime]),
+ [true, true, true] => $this->l10n->t('Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s', [$interval, $months, $days, $startTime, $endTime, $conclusion]),
+ default => $this->l10n->t('Could not generate event recurrence statement')
+ };
+ }
- $prop = $dtend->offsetGet('TZID');
- if ($prop instanceof Parameter) {
- $endTimezone = $prop->getValue();
+ /**
+ * genarates a when string for a fixed precision/frequency
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateWhenStringRecurringFixed(EventReader $er): string {
+ // initialize
+ $startTime = '';
+ $endTime = '';
+ $conclusion = '';
+ // time of the day
+ if (!$er->entireDay()) {
+ $startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']);
+ $startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : '';
+ $endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')';
+ }
+ // conclusion
+ $conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']);
+ // generate localized when string
+ return match (!empty($startTime)) {
+ false => $this->l10n->t('On specific dates for the entire day until %1$s', [$conclusion]),
+ true => $this->l10n->t('On specific dates between %1$s - %2$s until %3$s', [$startTime, $endTime, $conclusion]),
+ };
+ }
+
+ /**
+ * genarates a occurring next string for a recurring event
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateOccurringString(EventReader $er): string {
+
+ // reset to initial occurance
+ $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
+ $er->recurrenceAdvance();
+ // evaluate if occurance is valid
+ if ($er->recurrenceDate() !== null) {
+ // store following occurance value
+ $occurance2 = $this->l10n->l('date', $er->recurrenceDate(), ['width' => 'long']);
+ // forward one occurance
+ $er->recurrenceAdvance();
+ // evaluate if occurance is valid
+ if ($er->recurrenceDate()) {
+ // store following occurance value
+ $occurance3 = $this->l10n->l('date', $er->recurrenceDate(), ['width' => 'long']);
}
}
+ // generate occurance string
+ 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]),
+ default => $this->l10n->t('Could not generate next recurrence statement')
+ };
- $localeStart = $this->l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' .
- $this->l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']);
-
- // always show full date with timezone if timezones are different
- if ($startTimezone !== $endTimezone) {
- $localeEnd = $this->l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
-
- return $localeStart . ' (' . $startTimezone . ') - ' .
- $localeEnd . ' (' . $endTimezone . ')';
- }
-
- // show only end time if date is the same
- if ($dtstartDt->format('Y-m-d') === $dtendDt->format('Y-m-d')) {
- $localeEnd = $this->l10n->l('time', $dtendDt, ['width' => 'short']);
- } else {
- $localeEnd = $this->l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' .
- $this->l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
- }
-
- return $localeStart . ' - ' . $localeEnd . ' (' . $startTimezone . ')';
}
/**
@@ -261,12 +497,13 @@ class IMipService {
* @return array
*/
public function buildCancelledBodyData(VEvent $vEvent): array {
+ // construct event reader
+ $eventReaderCurrent = new EventReader($vEvent);
$defaultVal = '';
$strikethrough = "<span style='text-decoration: line-through'>%s</span>";
- $newMeetingWhen = $this->generateWhenString($vEvent);
+ $newMeetingWhen = $this->generateWhenString($eventReaderCurrent);
$newSummary = isset($vEvent->SUMMARY) && (string)$vEvent->SUMMARY !== '' ? (string)$vEvent->SUMMARY : $this->l10n->t('Untitled event');
- ;
$newDescription = isset($vEvent->DESCRIPTION) && (string)$vEvent->DESCRIPTION !== '' ? (string)$vEvent->DESCRIPTION : $defaultVal;
$newUrl = isset($vEvent->URL) && (string)$vEvent->URL !== '' ? sprintf('<a href="%1$s">%1$s</a>', $vEvent->URL) : $defaultVal;
$newLocation = isset($vEvent->LOCATION) && (string)$vEvent->LOCATION !== '' ? (string)$vEvent->LOCATION : $defaultVal;
@@ -522,7 +759,7 @@ class IMipService {
$data['meeting_title_html'] ?? $data['meeting_title'], $this->l10n->t('Title:'),
$this->getAbsoluteImagePath('caldav/title.png'), $data['meeting_title'], '', IMipPlugin::IMIP_INDENT);
if ($data['meeting_when'] !== '') {
- $template->addBodyListItem($data['meeting_when_html'] ?? $data['meeting_when'], $this->l10n->t('Date and time:'),
+ $template->addBodyListItem($data['meeting_when_html'] ?? $data['meeting_when'], $this->l10n->t('When:'),
$this->getAbsoluteImagePath('caldav/time.png'), $data['meeting_when'], '', IMipPlugin::IMIP_INDENT);
}
if ($data['meeting_location'] !== '') {
@@ -533,6 +770,10 @@ class IMipService {
$template->addBodyListItem($data['meeting_url_html'] ?? $data['meeting_url'], $this->l10n->t('Link:'),
$this->getAbsoluteImagePath('caldav/link.png'), $data['meeting_url'], '', IMipPlugin::IMIP_INDENT);
}
+ if (isset($data['meeting_occurring'])) {
+ $template->addBodyListItem($data['meeting_occurring_html'] ?? $data['meeting_occurring'], $this->l10n->t('Occurring:'),
+ $this->getAbsoluteImagePath('caldav/time.png'), $data['meeting_occurring'], '', IMipPlugin::IMIP_INDENT);
+ }
$this->addAttendees($template, $vevent);
@@ -643,10 +884,104 @@ class IMipService {
return false;
}
$type = $cuType->getValue() ?? 'INDIVIDUAL';
- if (\in_array(strtoupper($type), ['RESOURCE', 'ROOM'], true)) {
+ if (\in_array(strtoupper($type), ['RESOURCE', 'ROOM', 'UNKNOWN'], true)) {
// Don't send emails to things
return true;
}
return false;
}
+
+ public function minimizeInterval(\DateInterval $dateInterval): array {
+ // evaluate if time interval is in the past
+ if ($dateInterval->invert == 1) {
+ return [1, 'the past'];
+ }
+ // evaluate interval parts and return smallest time period
+ if ($dateInterval->y > 0) {
+ $interval = $dateInterval->y;
+ $scale = ($dateInterval->y > 1) ? 'years' : 'year';
+ } elseif ($dateInterval->m > 0) {
+ $interval = $dateInterval->m;
+ $scale = ($dateInterval->m > 1) ? 'months' : 'month';
+ } elseif ($dateInterval->d >= 7) {
+ $interval = (int)($dateInterval->d / 7);
+ $scale = ((int)($dateInterval->d / 7) > 1) ? 'weeks' : 'week';
+ } elseif ($dateInterval->d > 0) {
+ $interval = $dateInterval->d;
+ $scale = ($dateInterval->d > 1) ? 'days' : 'day';
+ } elseif ($dateInterval->h > 0) {
+ $interval = $dateInterval->h;
+ $scale = ($dateInterval->h > 1) ? 'hours' : 'hour';
+ } else {
+ $interval = $dateInterval->i;
+ $scale = 'minutes';
+ }
+
+ return [$interval, $scale];
+ }
+
+ /**
+ * Localizes week day names to another language
+ *
+ * @param string $value
+ *
+ * @return string
+ */
+ public function localizeDayName(string $value): string {
+ return match ($value) {
+ 'Monday' => $this->l10n->t('Monday'),
+ 'Tuesday' => $this->l10n->t('Tuesday'),
+ 'Wednesday' => $this->l10n->t('Wednesday'),
+ 'Thursday' => $this->l10n->t('Thursday'),
+ 'Friday' => $this->l10n->t('Friday'),
+ 'Saturday' => $this->l10n->t('Saturday'),
+ 'Sunday' => $this->l10n->t('Sunday'),
+ };
+ }
+
+ /**
+ * Localizes month names to another language
+ *
+ * @param string $value
+ *
+ * @return string
+ */
+ public function localizeMonthName(string $value): string {
+ return match ($value) {
+ 'January' => $this->l10n->t('January'),
+ 'February' => $this->l10n->t('February'),
+ 'March' => $this->l10n->t('March'),
+ 'April' => $this->l10n->t('April'),
+ 'May' => $this->l10n->t('May'),
+ 'June' => $this->l10n->t('June'),
+ 'July' => $this->l10n->t('July'),
+ 'August' => $this->l10n->t('August'),
+ 'September' => $this->l10n->t('September'),
+ 'October' => $this->l10n->t('October'),
+ 'November' => $this->l10n->t('November'),
+ 'December' => $this->l10n->t('December'),
+ };
+ }
+
+ /**
+ * Localizes relative position names to another language
+ *
+ * @param string $value
+ *
+ * @return string
+ */
+ public function localizeRelativePositionName(string $value): string {
+ return match ($value) {
+ 'First' => $this->l10n->t('First'),
+ 'Second' => $this->l10n->t('Second'),
+ 'Third' => $this->l10n->t('Third'),
+ 'Fourth' => $this->l10n->t('Fourth'),
+ 'Fifty' => $this->l10n->t('Fifty'),
+ 'Last' => $this->l10n->t('Last'),
+ 'Second Last' => $this->l10n->t('Second Last'),
+ 'Third Last' => $this->l10n->t('Third Last'),
+ 'Fourth Last' => $this->l10n->t('Fourth Last'),
+ 'Fifty Last' => $this->l10n->t('Fifty Last'),
+ };
+ }
}
diff --git a/apps/dav/tests/unit/CalDAV/EventReaderTest.php b/apps/dav/tests/unit/CalDAV/EventReaderTest.php
new file mode 100644
index 00000000000..23f0172131d
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/EventReaderTest.php
@@ -0,0 +1,1025 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use DateTimeZone;
+use OCA\DAV\CalDAV\EventReader;
+use Sabre\VObject\Component\VCalendar;
+use Test\TestCase;
+
+class EventReaderTest extends TestCase {
+
+ /** @var VCalendar*/
+ private $vCalendar1a;
+ /** @var VCalendar*/
+ private $vCalendar1b;
+ /** @var VCalendar*/
+ private $vCalendar1c;
+ /** @var VCalendar*/
+ private $vCalendar1d;
+ /** @var VCalendar*/
+ private $vCalendar2;
+ /** @var VCalendar*/
+ private $vCalendar3;
+
+ protected function setUp(): void {
+
+ parent::setUp();
+
+ // construct calendar with a 1 hour event and same start/end time zones
+ $this->vCalendar1a = new VCalendar();
+ $vEvent = $this->vCalendar1a->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Test Recurrance Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a 1 hour event and different start/end time zones
+ $this->vCalendar1b = new VCalendar();
+ $vEvent = $this->vCalendar1b->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Vancouver']);
+ $vEvent->add('SUMMARY', 'Test Recurrance Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a 1 hour event and global time zone
+ $this->vCalendar1c = new VCalendar();
+ // time zone component
+ $vTimeZone = $this->vCalendar1c->add('VTIMEZONE');
+ $vTimeZone->add('TZID', 'America/Toronto');
+ // event component
+ $vEvent = $this->vCalendar1c->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000');
+ $vEvent->add('DTEND', '20240701T090000');
+ $vEvent->add('SUMMARY', 'Test Recurrance Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a 1 hour event and no time zone
+ $this->vCalendar1d = new VCalendar();
+ $vEvent = $this->vCalendar1d->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000');
+ $vEvent->add('DTEND', '20240701T090000');
+ $vEvent->add('SUMMARY', 'Test Recurrance Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a full day event
+ $this->vCalendar2 = new VCalendar();
+ // time zone component
+ $vTimeZone = $this->vCalendar2->add('VTIMEZONE');
+ $vTimeZone->add('TZID', 'America/Toronto');
+ // event component
+ $vEvent = $this->vCalendar2->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701');
+ $vEvent->add('DTEND', '20240702');
+ $vEvent->add('SUMMARY', 'Test Recurrance Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a multi day event
+ $this->vCalendar3 = new VCalendar();
+ // time zone component
+ $vTimeZone = $this->vCalendar3->add('VTIMEZONE');
+ $vTimeZone->add('TZID', 'America/Toronto');
+ // event component
+ $vEvent = $this->vCalendar3->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701');
+ $vEvent->add('DTEND', '20240706');
+ $vEvent->add('SUMMARY', 'Test Recurrance Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ }
+
+ public function testConstructFromCalendarString(): void {
+
+ // construct event reader
+ $er = new EventReader($this->vCalendar1a->serialize(), '96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ // test object creation
+ $this->assertInstanceOf(EventReader::class, $er);
+
+ }
+
+ public function testConstructFromCalendarObject(): void {
+
+ // construct event reader
+ $er = new EventReader($this->vCalendar1a, '96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ // test object creation
+ $this->assertInstanceOf(EventReader::class, $er);
+
+ }
+
+ public function testConstructFromEventObject(): void {
+
+ // construct event reader
+ $er = new EventReader($this->vCalendar1a->VEVENT[0]);
+ // test object creation
+ $this->assertInstanceOf(EventReader::class, $er);
+
+ }
+
+ public function testStartDateTime(): void {
+
+ /** test day part event with same start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1a, $this->vCalendar1a->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->startDateTime());
+
+ /** test day part event with different start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1b, $this->vCalendar1b->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->startDateTime());
+
+ /** test day part event with global time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1c, $this->vCalendar1c->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->startDateTime());
+
+ /** test day part event with no time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1d, $this->vCalendar1d->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('UTC')))), $er->startDateTime());
+
+ /** test full day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar2, $this->vCalendar2->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T000000', (new DateTimeZone('America/Toronto')))), $er->startDateTime());
+
+ /** test multi day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar3, $this->vCalendar3->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T000000', (new DateTimeZone('America/Toronto')))), $er->startDateTime());
+
+ }
+
+ public function testStartTimeZone(): void {
+
+ /** test day part event with same start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1a, $this->vCalendar1a->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->startTimeZone());
+
+ /** test day part event with different start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1b, $this->vCalendar1b->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->startTimeZone());
+
+ /** test day part event with global time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1c, $this->vCalendar1c->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->startTimeZone());
+
+ /** test day part event with no time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1d, $this->vCalendar1d->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('UTC')), $er->startTimeZone());
+
+ /** test full day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar2, $this->vCalendar2->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->startTimeZone());
+
+ /** test multi day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar3, $this->vCalendar3->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->startTimeZone());
+
+ }
+
+ public function testEndDate(): void {
+
+ /** test day part event with same start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1a, $this->vCalendar1a->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T090000', (new DateTimeZone('America/Toronto')))), $er->endDateTime());
+
+ /** test day part event with different start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1b, $this->vCalendar1b->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T090000', (new DateTimeZone('America/Vancouver')))), $er->endDateTime());
+
+ /** test day part event with global time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1c, $this->vCalendar1c->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T090000', (new DateTimeZone('America/Toronto')))), $er->endDateTime());
+
+ /** test day part event with no time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1d, $this->vCalendar1d->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T090000', (new DateTimeZone('UTC')))), $er->endDateTime());
+
+ /** test full day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar2, $this->vCalendar2->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240702T000000', (new DateTimeZone('America/Toronto')))), $er->endDateTime());
+
+ /** test multi day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar3, $this->vCalendar3->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240706T000000', (new DateTimeZone('America/Toronto')))), $er->endDateTime());
+
+ }
+
+ public function testEndTimeZone(): void {
+
+ /** test day part event with same start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1a, $this->vCalendar1a->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->endTimeZone());
+
+ /** test day part event with different start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1b, $this->vCalendar1b->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Vancouver')), $er->endTimeZone());
+
+ /** test day part event with global time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1c, $this->vCalendar1c->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->endTimeZone());
+
+ /** test day part event with no time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1d, $this->vCalendar1d->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('UTC')), $er->endTimeZone());
+
+ /** test full day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar2, $this->vCalendar2->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->endTimeZone());
+
+ /** test multi day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar3, $this->vCalendar3->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->endTimeZone());
+
+ }
+
+ public function testEntireDay(): void {
+
+ /** test day part event with same start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1a, $this->vCalendar1a->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertFalse($er->entireDay());
+
+ /** test full day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar2, $this->vCalendar2->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->entireDay());
+
+ /** test multi day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar3, $this->vCalendar3->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->entireDay());
+
+ }
+
+ public function testRecurs(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertFalse($er->recurs());
+
+ /** test rrule recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->recurs());
+
+ /** test rdate recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->recurs());
+
+ }
+
+ public function testRecurringPattern(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertNull($er->recurringPattern());
+
+ /** test absolute rrule recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals('A', $er->recurringPattern());
+
+ /** test relative rrule recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=MO;BYSETPOS=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals('R', $er->recurringPattern());
+
+ /** test rdate recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals('A', $er->recurringPattern());
+
+ }
+
+ public function testRecurringPrecision(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertNull($er->recurringPrecision());
+
+ /** test daily rrule recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals('daily', $er->recurringPrecision());
+
+ /** test weekly rrule recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals('weekly', $er->recurringPrecision());
+
+ /** test monthly rrule recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8,15');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals('monthly', $er->recurringPrecision());
+
+ /** test yearly rrule recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYMONTHDAY=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals('yearly', $er->recurringPrecision());
+
+ /** test rdate recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals('fixed', $er->recurringPrecision());
+
+ }
+
+ public function testRecurringInterval(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertNull($er->recurringInterval());
+
+ /** test daily rrule recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(2, $er->recurringInterval());
+
+ /** test rdate recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertNull($er->recurringInterval());
+
+ }
+
+ public function testRecurringConcludes(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertFalse($er->recurringConcludes());
+
+ /** test rrule recurrance with no end */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertFalse($er->recurringConcludes());
+
+ /** test rrule recurrance with until date end */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;UNTIL=20240712T080000Z;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->recurringConcludes());
+
+ /** test rrule recurrance with iteration end */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->recurringConcludes());
+
+ /** test rdate recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703,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');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240706,20240715');
+ // 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 rrule as last date */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=7;BYDAY=MO,WE,FR');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240706,20240713');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->recurringConcludes());
+
+ }
+
+ public function testRecurringConcludesAfter(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertNull($er->recurringConcludesAfter());
+
+ /** test rrule recurrance with count */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(6, $er->recurringConcludesAfter());
+
+ /** test rdate recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703,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');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240706,20240715');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(8, $er->recurringConcludesAfter());
+
+ }
+
+ public function testRecurringConcludesOn(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertNull($er->recurringConcludesOn());
+
+ /** test rrule recurrance with no end */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertNull($er->recurringConcludesOn());
+
+ /** test rrule recurrance with until date end */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;UNTIL=20240712T080000Z;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+
+ // TODO: Fix until time zone
+ //$this->assertEquals((new \DateTime('20240712T080000', (new DateTimeZone('America/Toronto')))), $er->recurringConcludesOn());
+
+ /** test rdate recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703,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');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240706,20240715');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240715T000000', (new DateTimeZone('America/Toronto')))), $er->recurringConcludesOn());
+
+ /** test rrule and rdate recurrance with rrule as last date */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=7;BYDAY=MO,WE,FR');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240706,20240713');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240715T080000', (new DateTimeZone('America/Toronto')))), $er->recurringConcludesOn());
+
+ }
+
+ public function testRecurringDaysOfWeek(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringDaysOfWeek());
+
+ /** test rrule recurrance with weekly days*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;UNTIL=20240712T080000Z;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(['MO','WE','FR'], $er->recurringDaysOfWeek());
+
+ }
+
+ public function testRecurringDaysOfWeekNamed(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringDaysOfWeekNamed());
+
+ /** test rrule recurrance with weekly days*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;UNTIL=20240712T080000Z;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(['Monday','Wednesday','Friday'], $er->recurringDaysOfWeekNamed());
+
+ }
+
+ public function testRecurringDaysOfMonth(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringDaysOfMonth());
+
+ /** test rrule recurrance with monthly absolute dates*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=6,13,20,27');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([6,13,20,27], $er->recurringDaysOfMonth());
+
+ }
+
+ public function testRecurringDaysOfYear(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringDaysOfYear());
+
+ /** test rrule recurrance with monthly absolute dates*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYYEARDAY=1,30,180,365');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([1,30,180,365], $er->recurringDaysOfYear());
+
+ }
+
+ public function testRecurringWeeksOfMonth(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringWeeksOfMonth());
+
+ /** test rrule recurrance with monthly days*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=MO;BYSETPOS=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([1], $er->recurringWeeksOfMonth());
+
+ }
+
+ public function testRecurringWeeksOfMonthNamed(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringWeeksOfMonthNamed());
+
+ /** test rrule recurrance with weekly days*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=MO;BYSETPOS=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(['First'], $er->recurringWeeksOfMonthNamed());
+
+ }
+
+ public function testRecurringWeeksOfYear(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringWeeksOfYear());
+
+ /** test rrule recurrance with monthly days*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;INTERVAL=1;BYWEEKNO=35,42;BYDAY=TU');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([35,42], $er->recurringWeeksOfYear());
+
+ }
+
+ public function testRecurringMonthsOfYear(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringMonthsOfYear());
+
+ /** test rrule recurrance with monthly days*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;INTERVAL=1;BYMONTH=7;BYMONTHDAY=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([7], $er->recurringMonthsOfYear());
+
+ }
+
+ public function testRecurringMonthsOfYearNamed(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringMonthsOfYearNamed());
+
+ /** test rrule recurrance with weekly days*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;INTERVAL=1;BYMONTH=7;BYMONTHDAY=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(['July'], $er->recurringMonthsOfYearNamed());
+
+ }
+
+ public function testRecurringIterationDaily(): void {
+
+ /** test rrule recurrance with daily frequency*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=3;UNTIL=20240714T040000Z');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test initial recurrance
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240704T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240707T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240710T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240713T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance (This is past the last recurrance and should return null)
+ $er->recurrenceAdvance();
+ $this->assertNull($er->recurrenceDate());
+ // test rewind to initial recurrance
+ $er->recurrenceRewind();
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvanceTo((new \DateTime('20240709T080000')));
+ $this->assertEquals((new \DateTime('20240710T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+
+ }
+
+ public function testRecurringIterationWeekly(): void {
+
+ /** test rrule recurrance with weekly frequency*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20240713T040000Z');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test initial recurrance
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240703T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240705T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240708T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240710T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240712T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance (This is past the last recurrance and should return null)
+ $er->recurrenceAdvance();
+ $this->assertNull($er->recurrenceDate());
+ // test rewind to initial recurrance
+ $er->recurrenceRewind();
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvanceTo((new \DateTime('20240709T080000')));
+ $this->assertEquals((new \DateTime('20240710T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+
+ }
+
+ public function testRecurringIterationMonthlyAbsolute(): void {
+
+ /** test rrule recurrance with monthly absolute frequency on the 1st of each month*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;COUNT=3;BYMONTHDAY=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test initial recurrance
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240801T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240901T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance (This is past the last recurrance and should return null)
+ $er->recurrenceAdvance();
+ $this->assertNull($er->recurrenceDate());
+ // test rewind to initial recurrance
+ $er->recurrenceRewind();
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvanceTo((new \DateTime('20240809T080000')));
+ $this->assertEquals((new \DateTime('20240901T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+
+ }
+
+ public function testRecurringIterationMonthlyRelative(): void {
+
+ /** test rrule recurrance with monthly relative frequency on the first monday of each month*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;COUNT=3;BYDAY=MO;BYSETPOS=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test initial recurrance
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240805T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240902T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance (This is past the last recurrance and should return null)
+ $er->recurrenceAdvance();
+ $this->assertNull($er->recurrenceDate());
+ // test rewind to initial recurrance
+ $er->recurrenceRewind();
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvanceTo((new \DateTime('20240809T080000')));
+ $this->assertEquals((new \DateTime('20240902T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+
+ }
+
+ public function testRecurringIterationYearlyAbsolute(): void {
+
+ /** test rrule recurrance with yearly absolute frequency on the 1st of july*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;COUNT=3;BYMONTH=7');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test initial recurrance
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20250701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20260701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance (This is past the last recurrance and should return null)
+ $er->recurrenceAdvance();
+ $this->assertNull($er->recurrenceDate());
+ // test rewind to initial recurrance
+ $er->recurrenceRewind();
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvanceTo((new \DateTime('20250809T080000')));
+ $this->assertEquals((new \DateTime('20260701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+
+ }
+
+ public function testRecurringIterationYearlyRelative(): void {
+
+ /** test rrule recurrance with yearly relative frequency on the first monday of july*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;COUNT=3;BYMONTH=7;BYDAY=MO;BYSETPOS=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test initial recurrance
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20250707T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20260706T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance (This is past the last recurrance and should return null)
+ $er->recurrenceAdvance();
+ $this->assertNull($er->recurrenceDate());
+ // test rewind to initial recurrance
+ $er->recurrenceRewind();
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvanceTo((new \DateTime('20250809T080000')));
+ $this->assertEquals((new \DateTime('20260706T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+
+ }
+
+ public function testRecurringIterationFixed(): void {
+
+ /** test rrule recurrance with yearly relative frequency on the first monday of july*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703T080000,20240905T080000,20241231T080000');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test initial recurrance
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240703T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240905T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20241231T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance (This is past the last recurrance and should return null)
+ $er->recurrenceAdvance();
+ $this->assertNull($er->recurrenceDate());
+ // test rewind to initial recurrance
+ $er->recurrenceRewind();
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvanceTo((new \DateTime('20240809T080000')));
+ $this->assertEquals((new \DateTime('20240905T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+
+ }
+
+}
diff --git a/apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php b/apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php
index e4a9f1b75d0..667604f9b3e 100644
--- a/apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php
+++ b/apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php
@@ -10,15 +10,15 @@ namespace OCA\DAV\Tests\unit\CalDAV\Schedule;
use OC\L10N\L10N;
use OC\L10N\LazyL10N;
use OC\URLGenerator;
+use OCA\DAV\CalDAV\EventReader;
use OCA\DAV\CalDAV\Schedule\IMipService;
+use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\L10N\IFactory as L10NFactory;
use OCP\Security\ISecureRandom;
use PHPUnit\Framework\MockObject\MockObject;
use Sabre\VObject\Component\VCalendar;
-use Sabre\VObject\Component\VEvent;
-use Sabre\VObject\ITip\Message;
use Sabre\VObject\Property\ICalendar\DateTime;
use Test\TestCase;
@@ -41,9 +41,23 @@ class IMipServiceTest extends TestCase {
/** @var L10N|MockObject */
private $l10n;
+ /** @var ITimeFactory|MockObject */
+ private $timeFactory;
+
/** @var IMipService */
private $service;
+ /** @var VCalendar*/
+ private $vCalendar1a;
+ /** @var VCalendar*/
+ private $vCalendar1b;
+ /** @var VCalendar*/
+ private $vCalendar2;
+ /** @var VCalendar*/
+ private $vCalendar3;
+ /** @var DateTime DateTime object that will be returned by DateTime() or DateTime('now') */
+ public static $datetimeNow;
+
protected function setUp(): void {
$this->urlGenerator = $this->createMock(URLGenerator::class);
$this->config = $this->createMock(IConfig::class);
@@ -51,6 +65,7 @@ class IMipServiceTest extends TestCase {
$this->random = $this->createMock(ISecureRandom::class);
$this->l10nFactory = $this->createMock(L10NFactory::class);
$this->l10n = $this->createMock(LazyL10N::class);
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
$this->l10nFactory->expects(self::once())
->method('findGenericLanguage')
->willReturn('en');
@@ -63,8 +78,81 @@ class IMipServiceTest extends TestCase {
$this->config,
$this->db,
$this->random,
- $this->l10nFactory
+ $this->l10nFactory,
+ $this->timeFactory
);
+
+ // construct calendar with a 1 hour event and same start/end time zones
+ $this->vCalendar1a = new VCalendar();
+ $vEvent = $this->vCalendar1a->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Testing Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a 1 hour event and different start/end time zones
+ $this->vCalendar1b = new VCalendar();
+ $vEvent = $this->vCalendar1b->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Vancouver']);
+ $vEvent->add('SUMMARY', 'Testing Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a full day event
+ $this->vCalendar2 = new VCalendar();
+ // time zone component
+ $vTimeZone = $this->vCalendar2->add('VTIMEZONE');
+ $vTimeZone->add('TZID', 'America/Toronto');
+ // event component
+ $vEvent = $this->vCalendar2->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701');
+ $vEvent->add('DTEND', '20240702');
+ $vEvent->add('SUMMARY', 'Testing Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a multi day event
+ $this->vCalendar3 = new VCalendar();
+ // time zone component
+ $vTimeZone = $this->vCalendar3->add('VTIMEZONE');
+ $vTimeZone->add('TZID', 'America/Toronto');
+ // event component
+ $vEvent = $this->vCalendar3->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701');
+ $vEvent->add('DTEND', '20240706');
+ $vEvent->add('SUMMARY', 'Testing Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
}
public function testGetFrom(): void {
@@ -81,96 +169,93 @@ class IMipServiceTest extends TestCase {
}
public function testBuildBodyDataCreated(): void {
- $vCalendar = new VCalendar();
- $oldVevent = null;
- $newVevent = new VEvent($vCalendar, 'two', [
- 'UID' => 'uid-1234',
- 'SEQUENCE' => 3,
- 'LAST-MODIFIED' => 789456,
- 'SUMMARY' => 'Second Breakfast',
- 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
- 'RECURRENCE-ID' => new \DateTime('2016-01-01 00:00:00')
+
+ // construct l10n return(s)
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM',
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM',
+ $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'full'] => 'July 1, 2024'
+ };
+ }
+ );
+ $this->l10n->method('t')->willReturnMap([
+ ['In a %1$s on %2$s between %3$s - %4$s', ['day', 'July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], 'In a day on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)']
]);
-
+ // construct time factory return(s)
+ $this->timeFactory->method('getDateTime')->willReturnCallback(
+ function ($v1, $v2) {
+ return match (true) {
+ $v1 == 'now' && $v2 == null => (new \DateTime('20240630T000000'))
+ };
+ }
+ );
+ /** test singleton partial day event*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // define expected output
$expected = [
- 'meeting_when' => $this->service->generateWhenString($newVevent),
+ 'meeting_when' => $this->service->generateWhenString($eventReader),
'meeting_description' => '',
- 'meeting_title' => 'Second Breakfast',
+ 'meeting_title' => 'Testing Event',
'meeting_location' => '',
'meeting_url' => '',
'meeting_url_html' => '',
];
-
- $actual = $this->service->buildBodyData($newVevent, $oldVevent);
-
+ // generate actual output
+ $actual = $this->service->buildBodyData($vCalendar->VEVENT[0], null);
+ // test output
$this->assertEquals($expected, $actual);
}
public function testBuildBodyDataUpdate(): void {
- $vCalendar = new VCalendar();
- $oldVevent = new VEvent($vCalendar, 'two', [
- 'UID' => 'uid-1234',
- 'SEQUENCE' => 1,
- 'LAST-MODIFIED' => 456789,
- 'SUMMARY' => 'Elevenses',
- 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
- 'RECURRENCE-ID' => new \DateTime('2016-01-01 00:00:00')
- ]);
- $oldVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
- $oldVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
- $newVevent = new VEvent($vCalendar, 'two', [
- 'UID' => 'uid-1234',
- 'SEQUENCE' => 3,
- 'LAST-MODIFIED' => 789456,
- 'SUMMARY' => 'Second Breakfast',
- 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
- 'RECURRENCE-ID' => new \DateTime('2016-01-01 00:00:00')
+
+ // construct l10n return(s)
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM',
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM',
+ $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'full'] => 'July 1, 2024'
+ };
+ }
+ );
+ $this->l10n->method('t')->willReturnMap([
+ ['In a %1$s on %2$s between %3$s - %4$s', ['day', 'July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], 'In a day on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)']
]);
-
+ // construct time factory return(s)
+ $this->timeFactory->method('getDateTime')->willReturnCallback(
+ function ($v1, $v2) {
+ return match (true) {
+ $v1 == 'now' && $v2 == null => (new \DateTime('20240630T000000'))
+ };
+ }
+ );
+ /** test singleton partial day event*/
+ $vCalendarNew = clone $this->vCalendar1a;
+ $vCalendarOld = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReaderNew = new EventReader($vCalendarNew, $vCalendarNew->VEVENT[0]->UID->getValue());
+ // alter old event label/title
+ $vCalendarOld->VEVENT[0]->SUMMARY->setValue('Testing Singleton Event');
+ // define expected output
$expected = [
- 'meeting_when' => $this->service->generateWhenString($newVevent),
+ 'meeting_when' => $this->service->generateWhenString($eventReaderNew),
'meeting_description' => '',
- 'meeting_title' => 'Second Breakfast',
+ 'meeting_title' => 'Testing Event',
'meeting_location' => '',
'meeting_url' => '',
'meeting_url_html' => '',
- 'meeting_when_html' => $this->service->generateWhenString($newVevent),
- 'meeting_title_html' => sprintf("<span style='text-decoration: line-through'>%s</span><br />%s", 'Elevenses', 'Second Breakfast'),
+ 'meeting_when_html' => $this->service->generateWhenString($eventReaderNew),
+ 'meeting_title_html' => sprintf("<span style='text-decoration: line-through'>%s</span><br />%s", 'Testing Singleton Event', 'Testing Event'),
'meeting_description_html' => '',
'meeting_location_html' => ''
];
-
- $actual = $this->service->buildBodyData($newVevent, $oldVevent);
-
- $this->assertEquals($expected, $actual);
- }
-
- public function testGenerateWhenStringHourlyEvent(): void {
- $vCalendar = new VCalendar();
- $vevent = new VEvent($vCalendar, 'two', [
- 'UID' => 'uid-1234',
- 'SEQUENCE' => 1,
- 'LAST-MODIFIED' => 456789,
- 'SUMMARY' => 'Elevenses',
- 'TZID' => 'Europe/Vienna',
- 'DTSTART' => (new \DateTime('2016-01-01 08:00:00'))->setTimezone(new \DateTimeZone('Europe/Vienna')),
- 'DTEND' => (new \DateTime('2016-01-01 09:00:00'))->setTimezone(new \DateTimeZone('Europe/Vienna')),
- ]);
-
- $this->l10n->expects(self::exactly(3))
- ->method('l')
- ->withConsecutive(
- ['weekdayName', (new \DateTime('2016-01-01 08:00:00'))->setTimezone(new \DateTimeZone('Europe/Vienna')), ['width' => 'abbreviated']],
- ['datetime', (new \DateTime('2016-01-01 08:00:00'))->setTimezone(new \DateTimeZone('Europe/Vienna')), ['width' => 'medium|short']],
- ['time', (new \DateTime('2016-01-01 09:00:00'))->setTimezone(new \DateTimeZone('Europe/Vienna')), ['width' => 'short']]
- )->willReturnOnConsecutiveCalls(
- 'Fr.',
- '01.01. 08:00',
- '09:00'
- );
-
- $expected = 'Fr., 01.01. 08:00 - 09:00 (Europe/Vienna)';
- $actual = $this->service->generateWhenString($vevent);
+ // generate actual output
+ $actual = $this->service->buildBodyData($vCalendarNew->VEVENT[0], $vCalendarOld->VEVENT[0]);
+ // test output
$this->assertEquals($expected, $actual);
}
@@ -250,73 +335,1021 @@ class IMipServiceTest extends TestCase {
$this->assertEquals(1451606400, $occurrence);
}
- public function testGetCurrentAttendeeRequest(): void {
- // Construct ITip Message
- $message = new Message();
- $message->method = 'REQUEST';
- $message->sequence = 1;
- $message->sender = 'mailto:organizer@example.com';
- $message->senderName = 'The Organizer';
- $message->recipient = 'mailto:attendee@example.com';
- $message->recipientName = 'The Attendee';
- $message->significantChange = true;
- $message->message = new VCalendar();
- $message->message->add('VEVENT', ['UID' => '82496785-1915-4604-a5ce-4e2091639c9a', 'SEQUENCE' => 1]);
- $message->message->VEVENT->add('SUMMARY', 'Fellowship meeting');
- $message->message->VEVENT->add('DTSTART', (new \DateTime('NOW'))->modify('+1 hour'));
- $message->message->VEVENT->add('DTEND', (new \DateTime('NOW'))->modify('+2 hour'));
- $message->message->VEVENT->add('ORGANIZER', 'mailto:organizer@example.com', ['CN' => 'The Organizer']);
- $message->message->VEVENT->add('ATTENDEE', 'mailto:attendee@example.com', ['CN' => 'The Attendee']);
- // Test getCurrentAttendee
- $result = $this->service->getCurrentAttendee($message);
- // Evaluate Result
- $this->assertEquals($message->message->VEVENT->ATTENDEE, $result);
+ public function testGenerateWhenStringSingular(): void {
+
+ // construct l10n return(s)
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM',
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM',
+ $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'full'] => 'July 1, 2024',
+ $v1 === 'date' && $v2 == (new \DateTime('20240701T000000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'full'] => 'July 1, 2024'
+ };
+ }
+ );
+ $this->l10n->method('t')->willReturnMap([
+ ['In a %1$s on %2$s for the entire day', ['day', 'July 1, 2024'], 'In a day on July 1, 2024 for the entire day'],
+ ['In a %1$s on %2$s between %3$s - %4$s', ['day', 'July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], 'In a day on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['In %1$s %2$s on %3$s for the entire day', [2, 'days', 'July 1, 2024'], 'In 2 days on July 1, 2024 for the entire day'],
+ ['In %1$s %2$s on %3$s between %4$s - %5$s', [2, 'days', 'July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], 'In 2 days on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['In a %1$s on %2$s for the entire day', ['week', 'July 1, 2024'], 'In a week on July 1, 2024 for the entire day'],
+ ['In a %1$s on %2$s between %3$s - %4$s', ['week', 'July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], 'In a week on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['In %1$s %2$s on %3$s for the entire day', [2, 'weeks', 'July 1, 2024'], 'In 2 weeks on July 1, 2024 for the entire day'],
+ ['In %1$s %2$s on %3$s between %4$s - %5$s', [2, 'weeks', 'July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], 'In 2 weeks on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['In a %1$s on %2$s for the entire day', ['month', 'July 1, 2024'], 'In a month on July 1, 2024 for the entire day'],
+ ['In a %1$s on %2$s between %3$s - %4$s', ['month', 'July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], 'In a month on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['In %1$s %2$s on %3$s for the entire day', [2, 'months', 'July 1, 2024'], 'In 2 months on July 1, 2024 for the entire day'],
+ ['In %1$s %2$s on %3$s between %4$s - %5$s', [2, 'months', 'July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], 'In 2 months on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['In a %1$s on %2$s for the entire day', ['year', 'July 1, 2024'], 'In a year on July 1, 2024 for the entire day'],
+ ['In a %1$s on %2$s between %3$s - %4$s', ['year', 'July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], 'In a year on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['In %1$s %2$s on %3$s for the entire day', [2, 'years', 'July 1, 2024'], 'In 2 years on July 1, 2024 for the entire day'],
+ ['In %1$s %2$s on %3$s between %4$s - %5$s', [2, 'years', 'July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], 'In 2 years on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)']
+ ]);
+
+ // 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('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240621T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240621T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240614T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240614T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240530T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240530T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240430T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240430T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20230630T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20230630T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20220630T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20220630T170000', (new \DateTimeZone('America/Toronto'))))
+ );
+
+ /** test patrial day event in 1 day*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 1 day*/
+ $vCalendar = clone $this->vCalendar2;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test patrial day event in 2 days*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 2 days*/
+ $vCalendar = clone $this->vCalendar2;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test patrial day event in 1 week*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a week on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 1 week*/
+ $vCalendar = clone $this->vCalendar2;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a week on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test patrial day event in 2 weeks*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 weeks on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 2 weeks*/
+ $vCalendar = clone $this->vCalendar2;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 weeks on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test patrial day event in 1 month*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a month on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 1 month*/
+ $vCalendar = clone $this->vCalendar2;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a month on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test patrial day event in 2 months*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 months on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 2 months*/
+ $vCalendar = clone $this->vCalendar2;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 months on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test patrial day event in 1 year*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a year on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 1 year*/
+ $vCalendar = clone $this->vCalendar2;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a year on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test patrial day event in 2 years*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 years on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 2 years*/
+ $vCalendar = clone $this->vCalendar2;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 years on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ }
+
+ public function testGenerateWhenStringRecurringDaily(): void {
+
+ // construct l10n return maps
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM',
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM',
+ $v1 === 'date' && $v2 == (new \DateTime('20240713T080000', (new \DateTimeZone('UTC')))) && $v3 == ['width' => 'long'] => 'July 13, 2024'
+ };
+ }
+ );
+ $this->l10n->method('t')->willReturnMap([
+ ['Every Day for the entire day', [], 'Every Day for the entire day'],
+ ['Every Day for the entire day until %1$s', ['July 13, 2024'], 'Every Day for the entire day until July 13, 2024'],
+ ['Every Day between %1$s - %2$s', ['8:00 AM', '9:00 AM (America/Toronto)'], 'Every Day between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every Day between %1$s - %2$s until %3$s', ['8:00 AM', '9:00 AM (America/Toronto)', 'July 13, 2024'], 'Every Day between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024'],
+ ['Every %1$d Days for the entire day', [3], 'Every 3 Days for the entire day'],
+ ['Every %1$d Days for the entire day until %2$s', [3, 'July 13, 2024'], 'Every 3 Days for the entire day until July 13, 2024'],
+ ['Every %1$d Days between %2$s - %3$s', [3, '8:00 AM', '9:00 AM (America/Toronto)'], 'Every 3 Days between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every %1$d Days between %2$s - %3$s until %4$s', [3, '8:00 AM', '9:00 AM (America/Toronto)', 'July 13, 2024'], 'Every 3 Days between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024'],
+ ['Could not generate event recurrence statement', [], 'Could not generate event recurrence statement'],
+ ]);
+
+ /** test partial day event with every day interval and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=1;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Day between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event with every day interval and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=1;UNTIL=20240713T080000Z');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Day between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event every 3rd day interval and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=3;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 3 Days between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event with every 3rd day interval and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=3;UNTIL=20240713T080000Z');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 3 Days between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every day interval and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=1;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Day for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every day interval and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=1;UNTIL=20240713T080000Z');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Day for the entire day until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every 3rd day interval and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=3;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 3 Days for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every 3rd day interval and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=3;UNTIL=20240713T080000Z');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 3 Days for the entire day until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ }
+
+ public function testGenerateWhenStringRecurringWeekly(): void {
+
+ // construct l10n return maps
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM',
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM',
+ $v1 === 'date' && $v2 == (new \DateTime('20240722T080000', (new \DateTimeZone('UTC')))) && $v3 == ['width' => 'long'] => 'July 13, 2024'
+ };
+ }
+ );
+ $this->l10n->method('t')->willReturnMap([
+ ['Every Week on %1$s for the entire day', ['Monday, Wednesday, Friday'], 'Every Week on Monday, Wednesday, Friday for the entire day'],
+ ['Every Week on %1$s for the entire day until %2$s', ['Monday, Wednesday, Friday', 'July 13, 2024'], 'Every Week on Monday, Wednesday, Friday for the entire day until July 13, 2024'],
+ ['Every Week on %1$s between %2$s - %3$s', ['Monday, Wednesday, Friday', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every Week on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every Week on %1$s between %2$s - %3$s until %4$s', ['Monday, Wednesday, Friday', '8:00 AM', '9:00 AM (America/Toronto)', 'July 13, 2024'], 'Every Week on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024'],
+ ['Every %1$d Weeks on %2$s for the entire day', [2, 'Monday, Wednesday, Friday'], 'Every 2 Weeks on Monday, Wednesday, Friday for the entire day'],
+ ['Every %1$d Weeks on %2$s for the entire day until %3$s', [2, 'Monday, Wednesday, Friday', 'July 13, 2024'], 'Every 2 Weeks on Monday, Wednesday, Friday for the entire day until July 13, 2024'],
+ ['Every %1$d Weeks on %2$s between %3$s - %4$s', [2, 'Monday, Wednesday, Friday', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every 2 Weeks on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s', [2, 'Monday, Wednesday, Friday', '8:00 AM', '9:00 AM (America/Toronto)', 'July 13, 2024'], 'Every 2 Weeks on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024'],
+ ['Could not generate event recurrence statement', [], 'Could not generate event recurrence statement'],
+ ['Monday', [], 'Monday'],
+ ['Wednesday', [], 'Wednesday'],
+ ['Friday', [], 'Friday'],
+ ]);
+
+ /** test partial day event with every week interval on Mon, Wed, Fri and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Week on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event with every week interval on Mon, Wed, Fri and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20240722T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Week on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event with every 2nd week interval on Mon, Wed, Fri and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Weeks on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event with every 2nd week interval on Mon, Wed, Fri and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;INTERVAL=2;UNTIL=20240722T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Weeks on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every week interval on Mon, Wed, Fri and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Week on Monday, Wednesday, Friday for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every week interval on Mon, Wed, Fri and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20240722T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Week on Monday, Wednesday, Friday for the entire day until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every 2nd week interval on Mon, Wed, Fri and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Weeks on Monday, Wednesday, Friday for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every 2nd week interval on Mon, Wed, Fri and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;INTERVAL=2;UNTIL=20240722T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Weeks on Monday, Wednesday, Friday for the entire day until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ }
+
+ public function testGenerateWhenStringRecurringMonthly(): void {
+
+ // construct l10n return maps
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM',
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM',
+ $v1 === 'date' && $v2 == (new \DateTime('20241231T080000', (new \DateTimeZone('UTC')))) && $v3 == ['width' => 'long'] => 'December 31, 2024'
+ };
+ }
+ );
+ $this->l10n->method('t')->willReturnMap([
+ ['Every Month on the %1$s for the entire day', ['1, 8'], 'Every Month on the 1, 8 for the entire day'],
+ ['Every Month on the %1$s for the entire day until %2$s', ['1, 8', 'December 31, 2024'], 'Every Month on the 1, 8 for the entire day until December 31, 2024'],
+ ['Every Month on the %1$s between %2$s - %3$s', ['1, 8', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every Month on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every Month on the %1$s between %2$s - %3$s until %4$s', ['1, 8', '8:00 AM', '9:00 AM (America/Toronto)', 'December 31, 2024'], 'Every Month on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024'],
+ ['Every %1$d Months on the %2$s for the entire day', [2, '1, 8'], 'Every 2 Months on the 1, 8 for the entire day'],
+ ['Every %1$d Months on the %2$s for the entire day until %3$s', [2, '1, 8', 'December 31, 2024'], 'Every 2 Months on the 1, 8 for the entire day until December 31, 2024'],
+ ['Every %1$d Months on the %2$s between %3$s - %4$s', [2, '1, 8', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every 2 Months on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every %1$d Months on the %2$s between %3$s - %4$s until %5$s', [2, '1, 8', '8:00 AM', '9:00 AM (America/Toronto)', 'December 31, 2024'], 'Every 2 Months on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024'],
+ ['Every Month on the %1$s for the entire day', ['First Sunday, Saturday'], 'Every Month on the First Sunday, Saturday for the entire day'],
+ ['Every Month on the %1$s for the entire day until %2$s', ['First Sunday, Saturday', 'December 31, 2024'], 'Every Month on the First Sunday, Saturday for the entire day until December 31, 2024'],
+ ['Every Month on the %1$s between %2$s - %3$s', ['First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every Month on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every Month on the %1$s between %2$s - %3$s until %4$s', ['First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)', 'December 31, 2024'], 'Every Month on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024'],
+ ['Every %1$d Months on the %2$s for the entire day', [2, 'First Sunday, Saturday'], 'Every 2 Months on the First Sunday, Saturday for the entire day'],
+ ['Every %1$d Months on the %2$s for the entire day until %3$s', [2, 'First Sunday, Saturday', 'December 31, 2024'], 'Every 2 Months on the First Sunday, Saturday for the entire day until December 31, 2024'],
+ ['Every %1$d Months on the %2$s between %3$s - %4$s', [2, 'First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every 2 Months on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every %1$d Months on the %2$s between %3$s - %4$s until %5$s', [2, 'First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)', 'December 31, 2024'], 'Every 2 Months on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024'],
+ ['Could not generate event recurrence statement', [], 'Could not generate event recurrence statement'],
+ ['Saturday', [], 'Saturday'],
+ ['Sunday', [], 'Sunday'],
+ ['First', [], 'First'],
+ ]);
+
+ /** test absolute partial day event with every month interval on 1st, 8th and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Month on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute partial day event with every Month interval on 1st, 8th and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;UNTIL=20241231T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Month on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute partial day event with every 2nd Month interval on 1st, 8th and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Months on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute partial day event with every 2nd Month interval on 1st, 8th and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;INTERVAL=2;UNTIL=20241231T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Months on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute entire day event with every Month interval on 1st, 8th and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Month on the 1, 8 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute entire day event with every Month interval on 1st, 8th and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;UNTIL=20241231T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Month on the 1, 8 for the entire day until December 31, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute entire day event with every 2nd Month interval on 1st, 8th and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Months on the 1, 8 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute entire day event with every 2nd Month interval on 1st, 8th and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;INTERVAL=2;UNTIL=20241231T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Months on the 1, 8 for the entire day until December 31, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative partial day event with every month interval on the 1st Saturday, Sunday and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Month on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative partial day event with every Month interval on the 1st Saturday, Sunday and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;UNTIL=20241231T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Month on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative partial day event with every 2nd Month interval on the 1st Saturday, Sunday and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Months on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative partial day event with every 2nd Month interval on the 1st Saturday, Sunday and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;UNTIL=20241231T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Months on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative entire day event with every Month interval on the 1st Saturday, Sunday and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Month on the First Sunday, Saturday for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative entire day event with every Month interval on the 1st Saturday, Sunday and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;UNTIL=20241231T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Month on the First Sunday, Saturday for the entire day until December 31, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative entire day event with every 2nd Month interval on the 1st Saturday, Sunday and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Months on the First Sunday, Saturday for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative entire day event with every 2nd Month interval on the 1st Saturday, Sunday and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;UNTIL=20241231T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Months on the First Sunday, Saturday for the entire day until December 31, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ }
+
+ public function testGenerateWhenStringRecurringYearly(): void {
+
+ // construct l10n return maps
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM',
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM',
+ $v1 === 'date' && $v2 == (new \DateTime('20260731T040000', (new \DateTimeZone('UTC')))) && $v3 == ['width' => 'long'] => 'July 31, 2026'
+ };
+ }
+ );
+ $this->l10n->method('t')->willReturnMap([
+ ['Every Year in %1$s on the %2$s for the entire day', ['July', '1st'], 'Every Year in July on the 1st for the entire day'],
+ ['Every Year in %1$s on the %2$s for the entire day until %3$s', ['July', '1st', 'July 31, 2026'], 'Every Year in July on the 1st for the entire day until July 31, 2026'],
+ ['Every Year in %1$s on the %2$s between %3$s - %4$s', ['July', '1st', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every Year in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s', ['July', '1st', '8:00 AM', '9:00 AM (America/Toronto)', 'July 31, 2026'], 'Every Year in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026'],
+ ['Every %1$d Years in %2$s on the %3$s for the entire day', [2, 'July', '1st'], 'Every 2 Years in July on the 1st for the entire day'],
+ ['Every %1$d Years in %2$s on the %3$s for the entire day until %4$s', [2, 'July', '1st', 'July 31, 2026'], 'Every 2 Years in July on the 1st for the entire day until July 31, 2026'],
+ ['Every %1$d Years in %2$s on the %3$s between %4$s - %5$s', [2, 'July', '1st', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every 2 Years in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s', [2, 'July', '1st', '8:00 AM', '9:00 AM (America/Toronto)', 'July 31, 2026'], 'Every 2 Years in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026'],
+ ['Every Year in %1$s on the %2$s for the entire day', ['July', 'First Sunday, Saturday'], 'Every Year in July on the First Sunday, Saturday for the entire day'],
+ ['Every Year in %1$s on the %2$s for the entire day until %3$s', ['July', 'First Sunday, Saturday', 'July 31, 2026'], 'Every Year in July on the First Sunday, Saturday for the entire day until July 31, 2026'],
+ ['Every Year in %1$s on the %2$s between %3$s - %4$s', ['July', 'First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every Year in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s', ['July', 'First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)', 'July 31, 2026'], 'Every Year in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026'],
+ ['Every %1$d Years in %2$s on the %3$s for the entire day', [2, 'July', 'First Sunday, Saturday'], 'Every 2 Years in July on the First Sunday, Saturday for the entire day'],
+ ['Every %1$d Years in %2$s on the %3$s for the entire day until %4$s', [2, 'July', 'First Sunday, Saturday', 'July 31, 2026'], 'Every 2 Years in July on the First Sunday, Saturday for the entire day until July 31, 2026'],
+ ['Every %1$d Years in %2$s on the %3$s between %4$s - %5$s', [2, 'July', 'First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every 2 Years in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s', [2, 'July', 'First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)', 'July 31, 2026'], 'Every 2 Years in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026'],
+ ['Could not generate event recurrence statement', [], 'Could not generate event recurrence statement'],
+ ['July', [], 'July'],
+ ['Saturday', [], 'Saturday'],
+ ['Sunday', [], 'Sunday'],
+ ['First', [], 'First'],
+ ]);
+
+ /** test absolute partial day event with every year interval on July 1 and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Year in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute partial day event with every year interval on July 1 and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;UNTIL=20260731T040000Z');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Year in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute partial day event with every 2nd year interval on July 1 and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Years in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute partial day event with every 2nd year interval on July 1 and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;INTERVAL=2;UNTIL=20260731T040000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Years in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute entire day event with every year interval on July 1 and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Year in July on the 1st for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute entire day event with every year interval on July 1 and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;UNTIL=20260731T040000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Year in July on the 1st for the entire day until July 31, 2026',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute entire day event with every 2nd year interval on July 1 and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Years in July on the 1st for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute entire day event with every 2nd year interval on July 1 and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;INTERVAL=2;UNTIL=20260731T040000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Years in July on the 1st for the entire day until July 31, 2026',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative partial day event with every year interval on the 1st Saturday, Sunday in July and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Year in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative partial day event with every year interval on the 1st Saturday, Sunday in July and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;UNTIL=20260731T040000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Year in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative partial day event with every 2nd year interval on the 1st Saturday, Sunday in July and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Years in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative partial day event with every 2nd year interval on the 1st Saturday, Sunday in July and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;UNTIL=20260731T040000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Years in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative entire day event with every year interval on the 1st Saturday, Sunday in July and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Year in July on the First Sunday, Saturday for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative entire day event with every year interval on the 1st Saturday, Sunday in July and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;UNTIL=20260731T040000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Year in July on the First Sunday, Saturday for the entire day until July 31, 2026',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative entire day event with every 2nd year interval on the 1st Saturday, Sunday in July and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Years in July on the First Sunday, Saturday for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative entire day event with every 2nd year interval on the 1st Saturday, Sunday in July and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;UNTIL=20260731T040000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Years in July on the First Sunday, Saturday for the entire day until July 31, 2026',
+ $this->service->generateWhenString($eventReader)
+ );
+
}
- public function testGetCurrentAttendeeReply(): void {
- // Construct ITip Message
- $message = new Message();
- $message->method = 'REPLY';
- $message->sequence = 2;
- $message->sender = 'mailto:attendee@example.com';
- $message->senderName = 'The Attendee';
- $message->recipient = 'mailto:organizer@example.com';
- $message->recipientName = 'The Organizer';
- $message->significantChange = true;
- $message->message = new VCalendar();
- $message->message->add('METHOD', 'REPLY');
- $message->message->add('VEVENT', ['UID' => '82496785-1915-4604-a5ce-4e2091639c9a', 'SEQUENCE' => 2]);
- $message->message->VEVENT->add('SUMMARY', 'Fellowship meeting');
- $message->message->VEVENT->add('DTSTART', (new \DateTime('NOW'))->modify('+1 hour'));
- $message->message->VEVENT->add('DTEND', (new \DateTime('NOW'))->modify('+2 hour'));
- $message->message->VEVENT->add('ORGANIZER', 'mailto:organizer@example.com', ['CN' => 'The Organizer']);
- $message->message->VEVENT->add('ATTENDEE', 'mailto:attendee@example.com', ['CN' => 'The Attendee']);
- // Test getCurrentAttendee
- $result = $this->service->getCurrentAttendee($message);
- // Evaluate Result
- $this->assertEquals($message->message->VEVENT->ATTENDEE, $result);
+ public function testGenerateWhenStringRecurringFixed(): void {
+
+ // construct l10n return maps
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM',
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM',
+ $v1 === 'date' && $v2 == (new \DateTime('20240713T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 13, 2024'
+ };
+ }
+ );
+ $this->l10n->method('t')->willReturnMap([
+ ['On specific dates for the entire day until %1$s', ['July 13, 2024'], 'On specific dates for the entire day until July 13, 2024'],
+ ['On specific dates between %1$s - %2$s until %3$s', ['8:00 AM', '9:00 AM (America/Toronto)', 'July 13, 2024'], 'On specific dates between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024'],
+ ]);
+
+ /** test partial day event with every day interval and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703T080000,20240709T080000,20240713T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'On specific dates between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every day interval and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703T080000,20240709T080000,20240713T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'On specific dates for the entire day until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
}
- public function testGetCurrentAttendeeMismatch(): void {
- // Construct ITip Message
- $message = new Message();
- $message->method = 'REQUEST';
- $message->sequence = 1;
- $message->sender = 'mailto:organizer@example.com';
- $message->senderName = 'The Organizer';
- $message->recipient = 'mailto:mismatch@example.com';
- $message->recipientName = 'The Mismatch';
- $message->significantChange = true;
- $message->message = new VCalendar();
- $message->message->add('VEVENT', ['UID' => '82496785-1915-4604-a5ce-4e2091639c9a', 'SEQUENCE' => 1]);
- $message->message->VEVENT->add('SUMMARY', 'Fellowship meeting');
- $message->message->VEVENT->add('DTSTART', (new \DateTime('NOW'))->modify('+1 hour'));
- $message->message->VEVENT->add('DTEND', (new \DateTime('NOW'))->modify('+2 hour'));
- $message->message->VEVENT->add('ORGANIZER', 'mailto:organizer@example.com', ['CN' => 'The Organizer']);
- $message->message->VEVENT->add('ATTENDEE', 'mailto:attendee@example.com', ['CN' => 'The Attendee']);
- // Test getCurrentAttendee
- $result = $this->service->getCurrentAttendee($message);
- // Evaluate Result
- $this->assertEquals(null, $result);
+ public function testGenerateOccurringString(): 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 occurance remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1');
+ // 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 occurances remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2');
+ // 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 three occurances remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3');
+ // 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 occurance remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1');
+ // 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 occurances remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2');
+ // 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 occurances remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3');
+ // 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)
+ );
}
+
}