/** * returns next occurrence _ignoring exceptions_ or NULL if there is none/not computable * * NOTE: an ongoing event during $from [start, end[ is considered as next * NOTE: for previous events on ongoing event is considered as previous * * NOTE: computing the next occurrence of an open end rrule can be dangerous, as it might result * in a endless loop. Therefore we only make a limited number of attempts before giving up. * * @param Calendar_Model_Event $_event * @param Tinebase_Record_RecordSet $_exceptions * @param Tinebase_DateTime $_from * @param Int $_which * @return Calendar_Model_Event|NULL */ public static function computeNextOccurrence($_event, $_exceptions, $_from, $_which = 1) { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' $from = ' . $_from->toString()); } if ($_which === 0 || $_event->dtstart >= $_from && $_event->dtend > $_from) { return $_event; } $freqMap = array(self::FREQ_DAILY => Tinebase_DateTime::MODIFIER_DAY, self::FREQ_WEEKLY => Tinebase_DateTime::MODIFIER_WEEK, self::FREQ_MONTHLY => Tinebase_DateTime::MODIFIER_MONTH, self::FREQ_YEARLY => Tinebase_DateTime::MODIFIER_YEAR); $rrule = new Calendar_Model_Rrule(NULL, TRUE); $rrule->setFromString($_event->rrule); $from = clone $_from; $until = clone $from; $interval = $_which * $rrule->interval; // we don't want to compute ourself $ownEvent = clone $_event; $ownEvent->setRecurId($_event->getId()); $exceptions = clone $_exceptions; $exceptions->addRecord($ownEvent); $recurSet = new Tinebase_Record_RecordSet('Calendar_Model_Event'); if ($_from->isEarlier($_event->dtstart)) { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' from is ealier dtstart -> given event is next occurrence'); } return $_event; } $rangeDate = $_which > 0 ? $until : $from; if (!isset($freqMap[$rrule->freq])) { if (Tinebase_Core::isLogLevel(Zend_Log::ERR)) { Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' Invalid RRULE:' . print_r($rrule->toArray(), true)); } throw new Calendar_Exception('Invalid freq in RRULE: ' . $rrule->freq); } $rangeDate->add($interval, $freqMap[$rrule->freq]); $attempts = 0; if ($_event->rrule_until instanceof DateTime && Tinebase_Core::isLogLevel(Zend_Log::TRACE)) { Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Event rrule_until: ' . $_event->rrule_until->toString()); } while (TRUE) { if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) { Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' trying to find next occurrence from ' . $from->toString()); } if ($_event->rrule_until instanceof DateTime && $from->isLater($_event->rrule_until)) { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' passed rrule_until -> no further occurrences'); } return NULL; } $until = $_event->rrule_until instanceof DateTime && $until->isLater($_event->rrule_until) ? clone $_event->rrule_until : $until; $recurSet->merge(self::computeRecurrenceSet($_event, $exceptions, $from, $until)); $attempts++; // NOTE: computeRecurrenceSet also returns events during $from in some cases, but we need // to events later than $from. $recurSet = $recurSet->filter(function ($event) use($from) { return $event->dtstart >= $from; }); if (count($recurSet) >= abs($_which)) { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " found next occurrence after {$attempts} attempt(s)"); } break; } if ($attempts > count($exceptions) + 5) { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " could not find the next occurrence after {$attempts} attempts, giving up"); } return NULL; } $from->add($interval, $freqMap[$rrule->freq]); $until->add($interval, $freqMap[$rrule->freq]); } $recurSet->sort('dtstart', $_which > 0 ? 'ASC' : 'DESC'); $nextOccurrence = $recurSet[abs($_which) - 1]; if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' $nextOccurrence->dtstart = ' . $nextOccurrence->dtstart->toString()); } return $nextOccurrence; }