1
0

EventReader.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OCA\DAV\CalDAV;
  8. use DateTime;
  9. use DateTimeImmutable;
  10. use DateTimeInterface;
  11. use DateTimeZone;
  12. use InvalidArgumentException;
  13. use Sabre\VObject\Component\VCalendar;
  14. use Sabre\VObject\Component\VEvent;
  15. use Sabre\VObject\Reader;
  16. class EventReader {
  17. protected VEvent $baseEvent;
  18. protected DateTimeInterface $baseEventStartDate;
  19. protected DateTimeZone $baseEventStartTimeZone;
  20. protected DateTimeInterface $baseEventEndDate;
  21. protected DateTimeZone $baseEventEndTimeZone;
  22. protected bool $baseEventStartDateFloating = false;
  23. protected bool $baseEventEndDateFloating = false;
  24. protected int $baseEventDuration;
  25. protected ?EventReaderRRule $rruleIterator = null;
  26. protected ?EventReaderRDate $rdateIterator = null;
  27. protected ?EventReaderRRule $eruleIterator = null;
  28. protected ?EventReaderRDate $edateIterator = null;
  29. protected array $recurrenceModified;
  30. protected ?DateTimeInterface $recurrenceCurrentDate;
  31. protected array $dayNamesMap = [
  32. 'MO' => 'Monday', 'TU' => 'Tuesday', 'WE' => 'Wednesday', 'TH' => 'Thursday', 'FR' => 'Friday', 'SA' => 'Saturday', 'SU' => 'Sunday'
  33. ];
  34. protected array $monthNamesMap = [
  35. 1 => 'January', 2 => 'February', 3 => 'March', 4 => 'April', 5 => 'May', 6 => 'June',
  36. 7 => 'July', 8 => 'August', 9 => 'September', 10 => 'October', 11 => 'November', 12 => 'December'
  37. ];
  38. protected array $relativePositionNamesMap = [
  39. 1 => 'First', 2 => 'Second', 3 => 'Third', 4 => 'Fourth', 5 => 'Fifty',
  40. -1 => 'Last', -2 => 'Second Last', -3 => 'Third Last', -4 => 'Fourth Last', -5 => 'Fifty Last'
  41. ];
  42. /**
  43. * Initilizes the Event Reader
  44. *
  45. * There is several ways to set up the iterator.
  46. *
  47. * 1. You can pass a VCALENDAR component (as object or string) and a UID.
  48. * 2. You can pass an array of VEVENTs (all UIDS should match).
  49. * 3. You can pass a single VEVENT component (as object or string).
  50. *
  51. * Only the second method is recommended. The other 1 and 3 will be removed
  52. * at some point in the future.
  53. *
  54. * The $uid parameter is only required for the first method.
  55. *
  56. * @since 30.0.0
  57. *
  58. * @param VCalendar|VEvent|Array|String $input
  59. * @param string|null $uid
  60. * @param DateTimeZone|null $timeZone reference timezone for floating dates and times
  61. */
  62. public function __construct(VCalendar|VEvent|array|string $input, ?string $uid = null, ?DateTimeZone $timeZone = null) {
  63. // evaluate if the input is a string and convert it to and vobject if required
  64. if (is_string($input)) {
  65. $input = Reader::read($input);
  66. }
  67. // evaluate if input is a single event vobject and convert it to a collection
  68. if ($input instanceof VEvent) {
  69. $events = [$input];
  70. }
  71. // evaluate if input is a calendar vobject
  72. elseif ($input instanceof VCalendar) {
  73. // Calendar + UID mode.
  74. if ($uid === null) {
  75. throw new InvalidArgumentException('The UID argument is required when a VCALENDAR object is used');
  76. }
  77. // extract events from calendar
  78. $events = $input->getByUID($uid);
  79. // evaluate if any event where found
  80. if (count($events) === 0) {
  81. throw new InvalidArgumentException('This VCALENDAR did not have an event with UID: ' . $uid);
  82. }
  83. // extract calendar timezone
  84. if (isset($input->VTIMEZONE) && isset($input->VTIMEZONE->TZID)) {
  85. $calendarTimeZone = new DateTimeZone($input->VTIMEZONE->TZID->getValue());
  86. }
  87. }
  88. // evaluate if input is a collection of event vobjects
  89. elseif (is_array($input)) {
  90. $events = $input;
  91. } else {
  92. throw new InvalidArgumentException('Invalid input data type');
  93. }
  94. // find base event instance and remove it from events collection
  95. foreach ($events as $key => $vevent) {
  96. if (!isset($vevent->{'RECURRENCE-ID'})) {
  97. $this->baseEvent = $vevent;
  98. unset($events[$key]);
  99. }
  100. }
  101. // No base event was found. CalDAV does allow cases where only
  102. // overridden instances are stored.
  103. //
  104. // In this particular case, we're just going to grab the first
  105. // event and use that instead. This may not always give the
  106. // desired result.
  107. if (!isset($this->baseEvent) && count($events) > 0) {
  108. $this->baseEvent = array_shift($events);
  109. }
  110. // determain the event starting time zone
  111. // we require this to align all other dates times
  112. // evaluate if timezone paramater was used (treat this as a override)
  113. if ($timeZone !== null) {
  114. $this->baseEventStartTimeZone = $timeZone;
  115. }
  116. // evaluate if event start date has a timezone parameter
  117. elseif (isset($this->baseEvent->DTSTART->parameters['TZID'])) {
  118. $this->baseEventStartTimeZone = new DateTimeZone($this->baseEvent->DTSTART->parameters['TZID']->getValue());
  119. }
  120. // evaluate if event parent calendar has a time zone
  121. elseif (isset($calendarTimeZone)) {
  122. $this->baseEventStartTimeZone = clone $calendarTimeZone;
  123. }
  124. // otherwise, as a last resort use the UTC timezone
  125. else {
  126. $this->baseEventStartTimeZone = new DateTimeZone('UTC');
  127. }
  128. // determain the event end time zone
  129. // we require this to align all other dates and times
  130. // evaluate if timezone paramater was used (treat this as a override)
  131. if ($timeZone !== null) {
  132. $this->baseEventEndTimeZone = $timeZone;
  133. }
  134. // evaluate if event end date has a timezone parameter
  135. elseif (isset($this->baseEvent->DTEND->parameters['TZID'])) {
  136. $this->baseEventEndTimeZone = new DateTimeZone($this->baseEvent->DTEND->parameters['TZID']->getValue());
  137. }
  138. // evaluate if event parent calendar has a time zone
  139. elseif (isset($calendarTimeZone)) {
  140. $this->baseEventEndTimeZone = clone $calendarTimeZone;
  141. }
  142. // otherwise, as a last resort use the start date time zone
  143. else {
  144. $this->baseEventEndTimeZone = clone $this->baseEventStartTimeZone;
  145. }
  146. // extract start date and time
  147. $this->baseEventStartDate = $this->baseEvent->DTSTART->getDateTime($this->baseEventStartTimeZone);
  148. $this->baseEventStartDateFloating = $this->baseEvent->DTSTART->isFloating();
  149. // determine event end date and duration
  150. // evaluate if end date exists
  151. // extract end date and calculate duration
  152. if (isset($this->baseEvent->DTEND)) {
  153. $this->baseEventEndDate = $this->baseEvent->DTEND->getDateTime($this->baseEventEndTimeZone);
  154. $this->baseEventEndDateFloating = $this->baseEvent->DTEND->isFloating();
  155. $this->baseEventDuration =
  156. $this->baseEvent->DTEND->getDateTime($this->baseEventEndTimeZone)->getTimeStamp() -
  157. $this->baseEventStartDate->getTimeStamp();
  158. }
  159. // evaluate if duration exists
  160. // extract duration and calculate end date
  161. elseif (isset($this->baseEvent->DURATION)) {
  162. $this->baseEventEndDate = DateTimeImmutable::createFromInterface($this->baseEventStartDate)
  163. ->add($this->baseEvent->DURATION->getDateInterval());
  164. $this->baseEventDuration = $this->baseEventEndDate->getTimestamp() - $this->baseEventStartDate->getTimestamp();
  165. }
  166. // evaluate if start date is floating
  167. // set duration to 24 hours and calculate the end date
  168. // according to the rfc any event without a end date or duration is a complete day
  169. elseif ($this->baseEventStartDateFloating == true) {
  170. $this->baseEventDuration = 86400;
  171. $this->baseEventEndDate = DateTimeImmutable::createFromInterface($this->baseEventStartDate)
  172. ->setTimestamp($this->baseEventStartDate->getTimestamp() + $this->baseEventDuration);
  173. }
  174. // otherwise, set duration to zero this should never happen
  175. else {
  176. $this->baseEventDuration = 0;
  177. $this->baseEventEndDate = $this->baseEventStartDate;
  178. }
  179. // evaluate if RRULE exist and construct iterator
  180. if (isset($this->baseEvent->RRULE)) {
  181. $this->rruleIterator = new EventReaderRRule(
  182. $this->baseEvent->RRULE->getParts(),
  183. $this->baseEventStartDate
  184. );
  185. }
  186. // evaluate if RDATE exist and construct iterator
  187. if (isset($this->baseEvent->RDATE)) {
  188. $dates = [];
  189. foreach ($this->baseEvent->RDATE as $entry) {
  190. $dates[] = $entry->getValue();
  191. }
  192. $this->rdateIterator = new EventReaderRDate(
  193. implode(',', $dates),
  194. $this->baseEventStartDate
  195. );
  196. }
  197. // evaluate if EXRULE exist and construct iterator
  198. if (isset($this->baseEvent->EXRULE)) {
  199. $this->eruleIterator = new EventReaderRRule(
  200. $this->baseEvent->EXRULE->getParts(),
  201. $this->baseEventStartDate
  202. );
  203. }
  204. // evaluate if EXDATE exist and construct iterator
  205. if (isset($this->baseEvent->EXDATE)) {
  206. $dates = [];
  207. foreach ($this->baseEvent->EXDATE as $entry) {
  208. $dates[] = $entry->getValue();
  209. }
  210. $this->edateIterator = new EventReaderRDate(
  211. implode(',', $dates),
  212. $this->baseEventStartDate
  213. );
  214. }
  215. // construct collection of modified events with recurrence id as hash
  216. foreach ($events as $vevent) {
  217. $this->recurrenceModified[$vevent->{'RECURRENCE-ID'}->getDateTime($this->baseEventStartTimeZone)->getTimeStamp()] = $vevent;
  218. }
  219. $this->recurrenceCurrentDate = clone $this->baseEventStartDate;
  220. }
  221. /**
  222. * retrieve date and time of event start
  223. *
  224. * @since 30.0.0
  225. *
  226. * @return DateTime
  227. */
  228. public function startDateTime(): DateTime {
  229. return DateTime::createFromInterface($this->baseEventStartDate);
  230. }
  231. /**
  232. * retrieve time zone of event start
  233. *
  234. * @since 30.0.0
  235. *
  236. * @return DateTimeZone
  237. */
  238. public function startTimeZone(): DateTimeZone {
  239. return $this->baseEventStartTimeZone;
  240. }
  241. /**
  242. * retrieve date and time of event end
  243. *
  244. * @since 30.0.0
  245. *
  246. * @return DateTime
  247. */
  248. public function endDateTime(): DateTime {
  249. return DateTime::createFromInterface($this->baseEventEndDate);
  250. }
  251. /**
  252. * retrieve time zone of event end
  253. *
  254. * @since 30.0.0
  255. *
  256. * @return DateTimeZone
  257. */
  258. public function endTimeZone(): DateTimeZone {
  259. return $this->baseEventEndTimeZone;
  260. }
  261. /**
  262. * is this an all day event
  263. *
  264. * @since 30.0.0
  265. *
  266. * @return bool
  267. */
  268. public function entireDay(): bool {
  269. return $this->baseEventStartDateFloating;
  270. }
  271. /**
  272. * is this a recurring event
  273. *
  274. * @since 30.0.0
  275. *
  276. * @return bool
  277. */
  278. public function recurs(): bool {
  279. return ($this->rruleIterator !== null || $this->rdateIterator !== null);
  280. }
  281. /**
  282. * event recurrence pattern
  283. *
  284. * @since 30.0.0
  285. *
  286. * @return string|null R - Relative or A - Absolute
  287. */
  288. public function recurringPattern(): ?string {
  289. if ($this->rruleIterator === null && $this->rdateIterator === null) {
  290. return null;
  291. }
  292. if ($this->rruleIterator?->isRelative()) {
  293. return 'R';
  294. }
  295. return 'A';
  296. }
  297. /**
  298. * event recurrence precision
  299. *
  300. * @since 30.0.0
  301. *
  302. * @return string|null daily, weekly, monthly, yearly, fixed
  303. */
  304. public function recurringPrecision(): ?string {
  305. if ($this->rruleIterator !== null) {
  306. return $this->rruleIterator->precision();
  307. }
  308. if ($this->rdateIterator !== null) {
  309. return 'fixed';
  310. }
  311. return null;
  312. }
  313. /**
  314. * event recurrence interval
  315. *
  316. * @since 30.0.0
  317. *
  318. * @return int|null
  319. */
  320. public function recurringInterval(): ?int {
  321. return $this->rruleIterator?->interval();
  322. }
  323. /**
  324. * event recurrence conclusion
  325. *
  326. * returns true if RRULE with UNTIL or COUNT (calculated) is used
  327. * returns true RDATE is used
  328. * returns false if RRULE or RDATE are absent, or RRRULE is infinite
  329. *
  330. * @since 30.0.0
  331. *
  332. * @return bool
  333. */
  334. public function recurringConcludes(): bool {
  335. // retrieve rrule conclusions
  336. if ($this->rruleIterator?->concludesOn() !== null ||
  337. $this->rruleIterator?->concludesAfter() !== null) {
  338. return true;
  339. }
  340. // retrieve rdate conclusions
  341. if ($this->rdateIterator?->concludesAfter() !== null) {
  342. return true;
  343. }
  344. return false;
  345. }
  346. /**
  347. * event recurrence conclusion iterations
  348. *
  349. * returns the COUNT value if RRULE is used
  350. * returns the collection count if RDATE is used
  351. * returns combined count of RRULE COUNT and RDATE if both are used
  352. * returns null if RRULE and RDATE are absent
  353. *
  354. * @since 30.0.0
  355. *
  356. * @return int|null
  357. */
  358. public function recurringConcludesAfter(): ?int {
  359. // construct count place holder
  360. $count = 0;
  361. // retrieve and add RRULE iterations count
  362. $count += (int)$this->rruleIterator?->concludesAfter();
  363. // retrieve and add RDATE iterations count
  364. $count += (int)$this->rdateIterator?->concludesAfter();
  365. // return count
  366. return !empty($count) ? $count : null;
  367. }
  368. /**
  369. * event recurrence conclusion date
  370. *
  371. * returns the last date of UNTIL or COUNT (calculated) if RRULE is used
  372. * returns the last date in the collection if RDATE is used
  373. * returns the highest date if both RRULE and RDATE are used
  374. * returns null if RRULE and RDATE are absent or RRULE is infinite
  375. *
  376. * @since 30.0.0
  377. *
  378. * @return DateTime|null
  379. */
  380. public function recurringConcludesOn(): ?DateTime {
  381. if ($this->rruleIterator !== null) {
  382. // retrieve rrule conclusion date
  383. $rrule = $this->rruleIterator->concludes();
  384. // evaluate if rrule conclusion is null
  385. // if this is null that means the recurrence is infinate
  386. if ($rrule === null) {
  387. return null;
  388. }
  389. }
  390. // retrieve rdate conclusion date
  391. if ($this->rdateIterator !== null) {
  392. $rdate = $this->rdateIterator->concludes();
  393. }
  394. // evaluate if both rrule and rdate have date
  395. if (isset($rdate) && isset($rrule)) {
  396. // return the highest date
  397. return (($rdate > $rrule) ? $rdate : $rrule);
  398. } elseif (isset($rrule)) {
  399. return $rrule;
  400. } elseif (isset($rdate)) {
  401. return $rdate;
  402. }
  403. return null;
  404. }
  405. /**
  406. * event recurrence days of the week
  407. *
  408. * returns collection of RRULE BYDAY day(s) ['MO','WE','FR']
  409. * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
  410. *
  411. * @since 30.0.0
  412. *
  413. * @return array
  414. */
  415. public function recurringDaysOfWeek(): array {
  416. // evaluate if RRULE exists and return day(s) of the week
  417. return $this->rruleIterator !== null ? $this->rruleIterator->daysOfWeek() : [];
  418. }
  419. /**
  420. * event recurrence days of the week (named)
  421. *
  422. * returns collection of RRULE BYDAY day(s) ['Monday','Wednesday','Friday']
  423. * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
  424. *
  425. * @since 30.0.0
  426. *
  427. * @return array
  428. */
  429. public function recurringDaysOfWeekNamed(): array {
  430. // evaluate if RRULE exists and extract day(s) of the week
  431. $days = $this->rruleIterator !== null ? $this->rruleIterator->daysOfWeek() : [];
  432. // convert numberic month to month name
  433. foreach ($days as $key => $value) {
  434. $days[$key] = $this->dayNamesMap[$value];
  435. }
  436. // return names collection
  437. return $days;
  438. }
  439. /**
  440. * event recurrence days of the month
  441. *
  442. * returns collection of RRULE BYMONTHDAY day(s) [7, 15, 31]
  443. * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
  444. *
  445. * @since 30.0.0
  446. *
  447. * @return array
  448. */
  449. public function recurringDaysOfMonth(): array {
  450. // evaluate if RRULE exists and return day(s) of the month
  451. return $this->rruleIterator !== null ? $this->rruleIterator->daysOfMonth() : [];
  452. }
  453. /**
  454. * event recurrence days of the year
  455. *
  456. * returns collection of RRULE BYYEARDAY day(s) [57, 205, 365]
  457. * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
  458. *
  459. * @since 30.0.0
  460. *
  461. * @return array
  462. */
  463. public function recurringDaysOfYear(): array {
  464. // evaluate if RRULE exists and return day(s) of the year
  465. return $this->rruleIterator !== null ? $this->rruleIterator->daysOfYear() : [];
  466. }
  467. /**
  468. * event recurrence weeks of the month
  469. *
  470. * returns collection of RRULE SETPOS weeks(s) [1, 3, -1]
  471. * returns blank collection if RRULE is absent or SETPOS is absent, RDATE presents or absents has no affect
  472. *
  473. * @since 30.0.0
  474. *
  475. * @return array
  476. */
  477. public function recurringWeeksOfMonth(): array {
  478. // evaluate if RRULE exists and RRULE is relative return relative position(s)
  479. return $this->rruleIterator?->isRelative() ? $this->rruleIterator->relativePosition() : [];
  480. }
  481. /**
  482. * event recurrence weeks of the month (named)
  483. *
  484. * returns collection of RRULE SETPOS weeks(s) [1, 3, -1]
  485. * returns blank collection if RRULE is absent or SETPOS is absent, RDATE presents or absents has no affect
  486. *
  487. * @since 30.0.0
  488. *
  489. * @return array
  490. */
  491. public function recurringWeeksOfMonthNamed(): array {
  492. // evaluate if RRULE exists and extract relative position(s)
  493. $positions = $this->rruleIterator?->isRelative() ? $this->rruleIterator->relativePosition() : [];
  494. // convert numberic relative position to relative label
  495. foreach ($positions as $key => $value) {
  496. $positions[$key] = $this->relativePositionNamesMap[$value];
  497. }
  498. // return positions collection
  499. return $positions;
  500. }
  501. /**
  502. * event recurrence weeks of the year
  503. *
  504. * returns collection of RRULE BYWEEKNO weeks(s) [12, 32, 52]
  505. * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
  506. *
  507. * @since 30.0.0
  508. *
  509. * @return array
  510. */
  511. public function recurringWeeksOfYear(): array {
  512. // evaluate if RRULE exists and return weeks(s) of the year
  513. return $this->rruleIterator !== null ? $this->rruleIterator->weeksOfYear() : [];
  514. }
  515. /**
  516. * event recurrence months of the year
  517. *
  518. * returns collection of RRULE BYMONTH month(s) [3, 7, 12]
  519. * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
  520. *
  521. * @since 30.0.0
  522. *
  523. * @return array
  524. */
  525. public function recurringMonthsOfYear(): array {
  526. // evaluate if RRULE exists and return month(s) of the year
  527. return $this->rruleIterator !== null ? $this->rruleIterator->monthsOfYear() : [];
  528. }
  529. /**
  530. * event recurrence months of the year (named)
  531. *
  532. * returns collection of RRULE BYMONTH month(s) [3, 7, 12]
  533. * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
  534. *
  535. * @since 30.0.0
  536. *
  537. * @return array
  538. */
  539. public function recurringMonthsOfYearNamed(): array {
  540. // evaluate if RRULE exists and extract month(s) of the year
  541. $months = $this->rruleIterator !== null ? $this->rruleIterator->monthsOfYear() : [];
  542. // convert numberic month to month name
  543. foreach ($months as $key => $value) {
  544. $months[$key] = $this->monthNamesMap[$value];
  545. }
  546. // return months collection
  547. return $months;
  548. }
  549. /**
  550. * event recurrence relative positions
  551. *
  552. * returns collection of RRULE SETPOS value(s) [1, 5, -3]
  553. * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
  554. *
  555. * @since 30.0.0
  556. *
  557. * @return array
  558. */
  559. public function recurringRelativePosition(): array {
  560. // evaluate if RRULE exists and return relative position(s)
  561. return $this->rruleIterator !== null ? $this->rruleIterator->relativePosition() : [];
  562. }
  563. /**
  564. * event recurrence relative positions (named)
  565. *
  566. * returns collection of RRULE SETPOS [1, 3, -1]
  567. * returns blank collection if RRULE is absent or SETPOS is absent, RDATE presents or absents has no affect
  568. *
  569. * @since 30.0.0
  570. *
  571. * @return array
  572. */
  573. public function recurringRelativePositionNamed(): array {
  574. // evaluate if RRULE exists and extract relative position(s)
  575. $positions = $this->rruleIterator?->isRelative() ? $this->rruleIterator->relativePosition() : [];
  576. // convert numberic relative position to relative label
  577. foreach ($positions as $key => $value) {
  578. $positions[$key] = $this->relativePositionNamesMap[$value];
  579. }
  580. // return positions collection
  581. return $positions;
  582. }
  583. /**
  584. * event recurrence date
  585. *
  586. * returns date of currently selected recurrence
  587. *
  588. * @since 30.0.0
  589. *
  590. * @return DateTime
  591. */
  592. public function recurrenceDate(): ?DateTime {
  593. if ($this->recurrenceCurrentDate !== null) {
  594. return DateTime::createFromInterface($this->recurrenceCurrentDate);
  595. } else {
  596. return null;
  597. }
  598. }
  599. /**
  600. * event recurrence rewind
  601. *
  602. * sets the current recurrence to the first recurrence in the collection
  603. *
  604. * @since 30.0.0
  605. *
  606. * @return void
  607. */
  608. public function recurrenceRewind(): void {
  609. // rewind and increment rrule
  610. if ($this->rruleIterator !== null) {
  611. $this->rruleIterator->rewind();
  612. }
  613. // rewind and increment rdate
  614. if ($this->rdateIterator !== null) {
  615. $this->rdateIterator->rewind();
  616. }
  617. // rewind and increment exrule
  618. if ($this->eruleIterator !== null) {
  619. $this->eruleIterator->rewind();
  620. }
  621. // rewind and increment exdate
  622. if ($this->edateIterator !== null) {
  623. $this->edateIterator->rewind();
  624. }
  625. // set current date to event start date
  626. $this->recurrenceCurrentDate = clone $this->baseEventStartDate;
  627. }
  628. /**
  629. * event recurrence advance
  630. *
  631. * sets the current recurrence to the next recurrence in the collection
  632. *
  633. * @since 30.0.0
  634. *
  635. * @return void
  636. */
  637. public function recurrenceAdvance(): void {
  638. // place holders
  639. $nextOccurrenceDate = null;
  640. $nextExceptionDate = null;
  641. $rruleDate = null;
  642. $rdateDate = null;
  643. $eruleDate = null;
  644. $edateDate = null;
  645. // evaludate if rrule is set and advance one interation past current date
  646. if ($this->rruleIterator !== null) {
  647. // forward rrule to the next future date
  648. while ($this->rruleIterator->valid() && $this->rruleIterator->current() <= $this->recurrenceCurrentDate) {
  649. $this->rruleIterator->next();
  650. }
  651. $rruleDate = $this->rruleIterator->current();
  652. }
  653. // evaludate if rdate is set and advance one interation past current date
  654. if ($this->rdateIterator !== null) {
  655. // forward rdate to the next future date
  656. while ($this->rdateIterator->valid() && $this->rdateIterator->current() <= $this->recurrenceCurrentDate) {
  657. $this->rdateIterator->next();
  658. }
  659. $rdateDate = $this->rdateIterator->current();
  660. }
  661. if ($rruleDate !== null && $rdateDate !== null) {
  662. $nextOccurrenceDate = ($rruleDate <= $rdateDate) ? $rruleDate : $rdateDate;
  663. } elseif ($rruleDate !== null) {
  664. $nextOccurrenceDate = $rruleDate;
  665. } elseif ($rdateDate !== null) {
  666. $nextOccurrenceDate = $rdateDate;
  667. }
  668. // evaludate if exrule is set and advance one interation past current date
  669. if ($this->eruleIterator !== null) {
  670. // forward exrule to the next future date
  671. while ($this->eruleIterator->valid() && $this->eruleIterator->current() <= $this->recurrenceCurrentDate) {
  672. $this->eruleIterator->next();
  673. }
  674. $eruleDate = $this->eruleIterator->current();
  675. }
  676. // evaludate if exdate is set and advance one interation past current date
  677. if ($this->edateIterator !== null) {
  678. // forward exdate to the next future date
  679. while ($this->edateIterator->valid() && $this->edateIterator->current() <= $this->recurrenceCurrentDate) {
  680. $this->edateIterator->next();
  681. }
  682. $edateDate = $this->edateIterator->current();
  683. }
  684. // evaludate if exrule and exdate are set and set nextExDate to the first next date
  685. if ($eruleDate !== null && $edateDate !== null) {
  686. $nextExceptionDate = ($eruleDate <= $edateDate) ? $eruleDate : $edateDate;
  687. } elseif ($eruleDate !== null) {
  688. $nextExceptionDate = $eruleDate;
  689. } elseif ($edateDate !== null) {
  690. $nextExceptionDate = $edateDate;
  691. }
  692. // if the next date is part of exrule or exdate find another date
  693. if ($nextOccurrenceDate !== null && $nextExceptionDate !== null && $nextOccurrenceDate == $nextExceptionDate) {
  694. $this->recurrenceCurrentDate = $nextOccurrenceDate;
  695. $this->recurrenceAdvance();
  696. } else {
  697. $this->recurrenceCurrentDate = $nextOccurrenceDate;
  698. }
  699. }
  700. /**
  701. * event recurrence advance
  702. *
  703. * sets the current recurrence to the next recurrence in the collection after the specific date
  704. *
  705. * @since 30.0.0
  706. *
  707. * @param DateTimeInterface $dt date and time to advance
  708. *
  709. * @return void
  710. */
  711. public function recurrenceAdvanceTo(DateTimeInterface $dt): void {
  712. while ($this->recurrenceCurrentDate !== null && $this->recurrenceCurrentDate < $dt) {
  713. $this->recurrenceAdvance();
  714. }
  715. }
  716. }