ICal\ICal::processRecurrences PHP Method

processRecurrences() public method

Processes recurrence rules
public processRecurrences ( ) : void
return void or false if no Events exist
    public function processRecurrences()
    {
        $events = isset($this->cal['VEVENT']) ? $this->cal['VEVENT'] : array();
        if (empty($events)) {
            return false;
        }
        foreach ($events as $anEvent) {
            if (isset($anEvent['RRULE']) && $anEvent['RRULE'] != '') {
                if (isset($anEvent['DTSTART_array'][0]['TZID']) && $this->isValidTimeZoneId($anEvent['DTSTART_array'][0]['TZID'])) {
                    $initialStartTimeZone = $anEvent['DTSTART_array'][0]['TZID'];
                }
                $initialStart = new \DateTime($anEvent['DTSTART_array'][1], isset($initialStartTimeZone) ? new \DateTimeZone($initialStartTimeZone) : null);
                $initialStartOffset = $initialStart->getOffset();
                if (isset($anEvent['DTEND'])) {
                    if (isset($anEvent['DTEND_array'][0]['TZID']) && $this->isValidTimeZoneId($anEvent['DTSTART_array'][0]['TZID'])) {
                        $initialEndTimeZone = $anEvent['DTSTART_array'][0]['TZID'];
                    }
                    $initialEnd = new \DateTime($anEvent['DTEND_array'][1], isset($initialEndTimeZone) ? new \DateTimeZone($initialEndTimeZone) : null);
                    $initialEndOffset = $initialEnd->getOffset();
                }
                // Recurring event, parse RRULE and add appropriate duplicate events
                $rrules = array();
                $rruleStrings = explode(';', $anEvent['RRULE']);
                foreach ($rruleStrings as $s) {
                    list($k, $v) = explode('=', $s);
                    $rrules[$k] = $v;
                }
                // Get frequency
                $frequency = $rrules['FREQ'];
                // Get Start timestamp
                $startTimestamp = $initialStart->getTimeStamp();
                if (isset($anEvent['DTEND'])) {
                    $endTimestamp = $initialEnd->getTimestamp();
                } else {
                    if (isset($anEvent['DURATION'])) {
                        $duration = end($anEvent['DURATION_array']);
                        $endTimestamp = date_create($anEvent['DTSTART']);
                        $endTimestamp->modify($duration->y . ' year');
                        $endTimestamp->modify($duration->m . ' month');
                        $endTimestamp->modify($duration->d . ' day');
                        $endTimestamp->modify($duration->h . ' hour');
                        $endTimestamp->modify($duration->i . ' minute');
                        $endTimestamp->modify($duration->s . ' second');
                        $endTimestamp = date_format($endTimestamp, 'U');
                    } else {
                        $endTimestamp = $anEvent['DTSTART_array'][2];
                    }
                }
                $eventTimestampOffset = $endTimestamp - $startTimestamp;
                // Get Interval
                $interval = isset($rrules['INTERVAL']) && $rrules['INTERVAL'] != '' ? $rrules['INTERVAL'] : 1;
                $dayNumber = null;
                $weekDay = null;
                if (in_array($frequency, array('MONTHLY', 'YEARLY')) && isset($rrules['BYDAY']) && $rrules['BYDAY'] != '') {
                    // Deal with BYDAY
                    $dayNumber = intval($rrules['BYDAY']);
                    if (empty($dayNumber)) {
                        // Returns 0 when no number defined in BYDAY
                        if (!isset($rrules['BYSETPOS'])) {
                            $dayNumber = 1;
                            // Set first as default
                        } else {
                            if (is_numeric($rrules['BYSETPOS'])) {
                                $dayNumber = $rrules['BYSETPOS'];
                            }
                        }
                    }
                    $dayNumber = $dayNumber == -1 ? 6 : $dayNumber;
                    // Override for our custom key (6 => 'last')
                    $weekDay = substr($rrules['BYDAY'], -2);
                }
                $untilDefault = date_create('now');
                $untilDefault->modify($this->defaultSpan . ' year');
                $untilDefault->setTime(23, 59, 59);
                // End of the day
                if (isset($rrules['UNTIL'])) {
                    // Get Until
                    $until = strtotime($rrules['UNTIL']);
                } else {
                    if (isset($rrules['COUNT'])) {
                        $countOrig = is_numeric($rrules['COUNT']) && $rrules['COUNT'] > 1 ? $rrules['COUNT'] : 0;
                        $count = $countOrig - 1;
                        // Remove one to exclude the occurrence that initialises the rule
                        $count += $count > 0 ? $count * ($interval - 1) : 0;
                        $countNb = 1;
                        $offset = "+{$count} " . $this->frequencyConversion[$frequency];
                        $until = strtotime($offset, $startTimestamp);
                        if (in_array($frequency, array('MONTHLY', 'YEARLY')) && isset($rrules['BYDAY']) && $rrules['BYDAY'] != '') {
                            $dtstart = date_create($anEvent['DTSTART']);
                            for ($i = 1; $i <= $count; $i++) {
                                $dtstartClone = clone $dtstart;
                                $dtstartClone->modify('next ' . $this->frequencyConversion[$frequency]);
                                $offset = "{$this->dayOrdinals[$dayNumber]} {$this->weekdays[$weekDay]} of " . $dtstartClone->format('F Y H:i:01');
                                $dtstart->modify($offset);
                            }
                            /**
                             * Jumping X months forwards doesn't mean
                             * the end date will fall on the same day defined in BYDAY
                             * Use the largest of these to ensure we are going far enough
                             * in the future to capture our final end day
                             */
                            $until = max($until, $dtstart->format('U'));
                        }
                        unset($offset);
                    } else {
                        $until = $untilDefault->getTimestamp();
                    }
                }
                if (!isset($anEvent['EXDATE_array'])) {
                    $anEvent['EXDATE_array'][1] = array();
                }
                // Decide how often to add events and do so
                switch ($frequency) {
                    case 'DAILY':
                        // Simply add a new event each interval of days until UNTIL is reached
                        $offset = "+{$interval} day";
                        $recurringTimestamp = strtotime($offset, $startTimestamp);
                        while ($recurringTimestamp <= $until) {
                            $dayRecurringTimestamp = $recurringTimestamp;
                            // Adjust timezone from initial event
                            $recurringTimeZone = \DateTime::createFromFormat('U', $dayRecurringTimestamp);
                            $timezoneOffset = $initialStart->getTimezone()->getOffset($recurringTimeZone);
                            $dayRecurringTimestamp += $timezoneOffset != $initialStartOffset ? $initialStartOffset - $timezoneOffset : 0;
                            // Add event
                            $anEvent['DTSTART'] = gmdate(self::DATE_TIME_FORMAT, $dayRecurringTimestamp) . 'Z';
                            $anEvent['DTSTART_array'] = array(array(), $anEvent['DTSTART'], $dayRecurringTimestamp);
                            $anEvent['DTEND_array'] = $anEvent['DTSTART_array'];
                            $anEvent['DTEND_array'][2] += $eventTimestampOffset;
                            $anEvent['DTEND'] = gmdate(self::DATE_TIME_FORMAT, $anEvent['DTEND_array'][2]) . 'Z';
                            $anEvent['DTEND_array'][1] = $anEvent['DTEND'];
                            $searchDate = $anEvent['DTSTART'];
                            $isExcluded = array_filter($anEvent['EXDATE_array'][1], function ($val) use($searchDate) {
                                return is_string($val) && strpos($searchDate, $val) === 0;
                            });
                            if (isset($this->alteredRecurrenceInstances[$anEvent['UID']]) && in_array($dayRecurringTimestamp, $this->alteredRecurrenceInstances[$anEvent['UID']])) {
                                $isExcluded = true;
                            }
                            if (!$isExcluded) {
                                $events[] = $anEvent;
                                $this->eventCount++;
                                // If RRULE[COUNT] is reached then break
                                if (isset($rrules['COUNT'])) {
                                    $countNb++;
                                    if ($countNb >= $countOrig) {
                                        break 2;
                                    }
                                }
                            }
                            // Move forwards
                            $recurringTimestamp = strtotime($offset, $recurringTimestamp);
                        }
                        break;
                    case 'WEEKLY':
                        // Create offset
                        $offset = "+{$interval} week";
                        // Use RRULE['WKST'] setting or a default week start (UK = SU, Europe = MO)
                        $weeks = array('SA' => array('SA', 'SU', 'MO', 'TU', 'WE', 'TH', 'FR'), 'SU' => array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'), 'MO' => array('MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'));
                        $wkst = isset($rrules['WKST']) && in_array($rrules['WKST'], array('SA', 'SU', 'MO')) ? $rrules['WKST'] : $this->defaultWeekStart;
                        $aWeek = $weeks[$wkst];
                        $days = array('SA' => 'Saturday', 'SU' => 'Sunday', 'MO' => 'Monday');
                        // Build list of days of week to add events
                        $weekdays = $aWeek;
                        if (isset($rrules['BYDAY']) && $rrules['BYDAY'] != '') {
                            $bydays = explode(',', $rrules['BYDAY']);
                        } else {
                            $findDay = $weekdays[gmdate('w', $startTimestamp)];
                            $bydays = array($findDay);
                        }
                        // Get timestamp of first day of start week
                        $weekRecurringTimestamp = gmdate('w', $startTimestamp) == 0 ? $startTimestamp : strtotime("last {$days[$wkst]} " . gmdate('H:i:s\\z', $startTimestamp), $startTimestamp);
                        // Step through weeks
                        while ($weekRecurringTimestamp <= $until) {
                            $dayRecurringTimestamp = $weekRecurringTimestamp;
                            // Adjust timezone from initial event
                            $dayRecurringTimeZone = \DateTime::createFromFormat('U', $dayRecurringTimestamp, new \DateTimeZone('UTC'));
                            $timezoneOffset = $initialStart->getTimezone()->getOffset($dayRecurringTimeZone);
                            $dayRecurringTimestamp += $timezoneOffset != $initialStartOffset ? $initialStartOffset - $timezoneOffset : 0;
                            foreach ($weekdays as $day) {
                                // Check if day should be added
                                if (in_array($day, $bydays) && $dayRecurringTimestamp > $startTimestamp && $dayRecurringTimestamp <= $until) {
                                    // Add event
                                    $anEvent['DTSTART'] = gmdate(self::DATE_TIME_FORMAT, $dayRecurringTimestamp) . 'Z';
                                    $anEvent['DTSTART_array'] = array(array(), $anEvent['DTSTART'], $dayRecurringTimestamp);
                                    $anEvent['DTEND_array'] = $anEvent['DTSTART_array'];
                                    $anEvent['DTEND_array'][2] += $eventTimestampOffset;
                                    $anEvent['DTEND'] = gmdate(self::DATE_TIME_FORMAT, $anEvent['DTEND_array'][2]) . 'Z';
                                    $anEvent['DTEND_array'][1] = $anEvent['DTEND'];
                                    $searchDate = $anEvent['DTSTART'];
                                    $isExcluded = array_filter($anEvent['EXDATE_array'][1], function ($val) use($searchDate) {
                                        return is_string($val) && strpos($searchDate, $val) === 0;
                                    });
                                    if (isset($this->alteredRecurrenceInstances[$anEvent['UID']]) && in_array($dayRecurringTimestamp, $this->alteredRecurrenceInstances[$anEvent['UID']])) {
                                        $isExcluded = true;
                                    }
                                    if (!$isExcluded) {
                                        $events[] = $anEvent;
                                        $this->eventCount++;
                                        // If RRULE[COUNT] is reached then break
                                        if (isset($rrules['COUNT'])) {
                                            $countNb++;
                                            if ($countNb >= $countOrig) {
                                                break 2;
                                            }
                                        }
                                    }
                                }
                                // Move forwards a day
                                $dayRecurringTimestamp = strtotime('+1 day', $dayRecurringTimestamp);
                            }
                            // Move forwards $interval weeks
                            $weekRecurringTimestamp = strtotime($offset, $weekRecurringTimestamp);
                        }
                        break;
                    case 'MONTHLY':
                        // Create offset
                        $offset = "+{$interval} month";
                        $recurringTimestamp = strtotime($offset, $startTimestamp);
                        if (isset($rrules['BYMONTHDAY']) && $rrules['BYMONTHDAY'] != '') {
                            // Deal with BYMONTHDAY
                            $monthdays = explode(',', $rrules['BYMONTHDAY']);
                            while ($recurringTimestamp <= $until) {
                                foreach ($monthdays as $key => $monthday) {
                                    if ($key === 0) {
                                        // Ensure original event conforms to monthday rule
                                        $events[0]['DTSTART'] = gmdate('Ym' . sprintf('%02d', $monthday) . '\\T' . self::TIME_FORMAT, strtotime($events[0]['DTSTART'])) . 'Z';
                                        $events[0]['DTEND'] = gmdate('Ym' . sprintf('%02d', $monthday) . '\\T' . self::TIME_FORMAT, strtotime($events[0]['DTEND'])) . 'Z';
                                        $events[0]['DTSTART_array'][1] = $events[0]['DTSTART'];
                                        $events[0]['DTSTART_array'][2] = $this->iCalDateToUnixTimestamp($events[0]['DTSTART']);
                                        $events[0]['DTEND_array'][1] = $events[0]['DTEND'];
                                        $events[0]['DTEND_array'][2] = $this->iCalDateToUnixTimestamp($events[0]['DTEND']);
                                        // Ensure recurring timestamp confirms to monthday rule
                                        $monthRecurringTimestamp = $this->iCalDateToUnixTimestamp(gmdate('Ym' . sprintf('%02d', $monthday) . '\\T' . self::TIME_FORMAT, $recurringTimestamp) . 'Z');
                                    }
                                    // Adjust timezone from initial event
                                    $recurringTimeZone = \DateTime::createFromFormat('U', $monthRecurringTimestamp);
                                    $timezoneOffset = $initialStart->getTimezone()->getOffset($recurringTimeZone);
                                    $monthRecurringTimestamp += $timezoneOffset != $initialStartOffset ? $initialStartOffset - $timezoneOffset : 0;
                                    // Add event
                                    $anEvent['DTSTART'] = gmdate('Ym' . sprintf('%02d', $monthday) . '\\T' . self::TIME_FORMAT, $monthRecurringTimestamp) . 'Z';
                                    $anEvent['DTSTART_array'] = array(array(), $anEvent['DTSTART'], $monthRecurringTimestamp);
                                    $anEvent['DTEND_array'] = $anEvent['DTSTART_array'];
                                    $anEvent['DTEND_array'][2] += $eventTimestampOffset;
                                    $anEvent['DTEND'] = gmdate(self::DATE_TIME_FORMAT, $anEvent['DTEND_array'][2]) . 'Z';
                                    $anEvent['DTEND_array'][1] = $anEvent['DTEND'];
                                    $searchDate = $anEvent['DTSTART'];
                                    $isExcluded = array_filter($anEvent['EXDATE_array'][1], function ($val) use($searchDate) {
                                        return is_string($val) && strpos($searchDate, $val) === 0;
                                    });
                                    if (isset($this->alteredRecurrenceInstances[$anEvent['UID']]) && in_array($monthRecurringTimestamp, $this->alteredRecurrenceInstances[$anEvent['UID']])) {
                                        $isExcluded = true;
                                    }
                                    if (!$isExcluded) {
                                        $events[] = $anEvent;
                                        $this->eventCount++;
                                        // If RRULE[COUNT] is reached then break
                                        if (isset($rrules['COUNT'])) {
                                            $countNb++;
                                            if ($countNb >= $countOrig) {
                                                break 2;
                                            }
                                        }
                                    }
                                }
                                // Move forwards
                                $recurringTimestamp = strtotime($offset, $recurringTimestamp);
                            }
                        } else {
                            if (isset($rrules['BYDAY']) && $rrules['BYDAY'] != '') {
                                while ($recurringTimestamp <= $until) {
                                    $monthRecurringTimestamp = $recurringTimestamp;
                                    // Adjust timezone from initial event
                                    $recurringTimeZone = \DateTime::createFromFormat('U', $monthRecurringTimestamp);
                                    $timezoneOffset = $initialStart->getTimezone()->getOffset($recurringTimeZone);
                                    $monthRecurringTimestamp += $timezoneOffset != $initialStartOffset ? $initialStartOffset - $timezoneOffset : 0;
                                    $eventStartDesc = "{$this->dayOrdinals[$dayNumber]} {$this->weekdays[$weekDay]} of " . gmdate('F Y H:i:s', $monthRecurringTimestamp);
                                    $eventStartTimestamp = strtotime($eventStartDesc);
                                    // Prevent 5th day of a month from showing up on the next month
                                    // If BYDAY and the event falls outside the current month, skip the event
                                    $compareCurrentMonth = date('F', $monthRecurringTimestamp);
                                    $compareEventMonth = date('F', $eventStartTimestamp);
                                    if ($compareCurrentMonth != $compareEventMonth) {
                                        $monthRecurringTimestamp = strtotime($offset, $monthRecurringTimestamp);
                                        continue;
                                    }
                                    if ($eventStartTimestamp > $startTimestamp && $eventStartTimestamp < $until) {
                                        $anEvent['DTSTART'] = gmdate(self::DATE_TIME_FORMAT, $eventStartTimestamp) . 'Z';
                                        $anEvent['DTSTART_array'] = array(array(), $anEvent['DTSTART'], $eventStartTimestamp);
                                        $anEvent['DTEND_array'] = $anEvent['DTSTART_array'];
                                        $anEvent['DTEND_array'][2] += $eventTimestampOffset;
                                        $anEvent['DTEND'] = gmdate(self::DATE_TIME_FORMAT, $anEvent['DTEND_array'][2]) . 'Z';
                                        $anEvent['DTEND_array'][1] = $anEvent['DTEND'];
                                        $searchDate = $anEvent['DTSTART'];
                                        $isExcluded = array_filter($anEvent['EXDATE_array'][1], function ($val) use($searchDate) {
                                            return is_string($val) && strpos($searchDate, $val) === 0;
                                        });
                                        if (isset($this->alteredRecurrenceInstances[$anEvent['UID']]) && in_array($monthRecurringTimestamp, $this->alteredRecurrenceInstances[$anEvent['UID']])) {
                                            $isExcluded = true;
                                        }
                                        if (!$isExcluded) {
                                            $events[] = $anEvent;
                                            $this->eventCount++;
                                            // If RRULE[COUNT] is reached then break
                                            if (isset($rrules['COUNT'])) {
                                                $countNb++;
                                                if ($countNb >= $countOrig) {
                                                    break 2;
                                                }
                                            }
                                        }
                                    }
                                    // Move forwards
                                    $recurringTimestamp = strtotime($offset, $recurringTimestamp);
                                }
                            }
                        }
                        break;
                    case 'YEARLY':
                        // Create offset
                        $offset = "+{$interval} year";
                        $recurringTimestamp = strtotime($offset, $startTimestamp);
                        // Check if BYDAY rule exists
                        if (isset($rrules['BYDAY']) && $rrules['BYDAY'] != '') {
                            while ($recurringTimestamp <= $until) {
                                $yearRecurringTimestamp = $recurringTimestamp;
                                // Adjust timezone from initial event
                                $recurringTimeZone = \DateTime::createFromFormat('U', $yearRecurringTimestamp);
                                $timezoneOffset = $initialStart->getTimezone()->getOffset($recurringTimeZone);
                                $yearRecurringTimestamp += $timezoneOffset != $initialStartOffset ? $initialStartOffset - $timezoneOffset : 0;
                                $eventStartDesc = "{$this->dayOrdinals[$dayNumber]} {$this->weekdays[$weekDay]}" . " of {$this->monthNames[$rrules['BYMONTH']]} " . gmdate('Y H:i:s', $yearRecurringTimestamp);
                                $eventStartTimestamp = strtotime($eventStartDesc);
                                if ($eventStartTimestamp > $startTimestamp && $eventStartTimestamp < $until) {
                                    $anEvent['DTSTART'] = gmdate(self::DATE_TIME_FORMAT, $eventStartTimestamp) . 'Z';
                                    $anEvent['DTSTART_array'] = array(array(), $anEvent['DTSTART'], $eventStartTimestamp);
                                    $anEvent['DTEND_array'] = $anEvent['DTSTART_array'];
                                    $anEvent['DTEND_array'][2] += $eventTimestampOffset;
                                    $anEvent['DTEND'] = gmdate(self::DATE_TIME_FORMAT, $anEvent['DTEND_array'][2]) . 'Z';
                                    $anEvent['DTEND_array'][1] = $anEvent['DTEND'];
                                    $searchDate = $anEvent['DTSTART'];
                                    $isExcluded = array_filter($anEvent['EXDATE_array'][1], function ($val) use($searchDate) {
                                        return is_string($val) && strpos($searchDate, $val) === 0;
                                    });
                                    if (isset($this->alteredRecurrenceInstances[$anEvent['UID']]) && in_array($yearRecurringTimestamp, $this->alteredRecurrenceInstances[$anEvent['UID']])) {
                                        $isExcluded = true;
                                    }
                                    if (!$isExcluded) {
                                        $events[] = $anEvent;
                                        $this->eventCount++;
                                        // If RRULE[COUNT] is reached then break
                                        if (isset($rrules['COUNT'])) {
                                            $countNb++;
                                            if ($countNb >= $countOrig) {
                                                break 2;
                                            }
                                        }
                                    }
                                }
                                // Move forwards
                                $recurringTimestamp = strtotime($offset, $recurringTimestamp);
                            }
                        } else {
                            $day = gmdate('d', $startTimestamp);
                            // Step through years
                            while ($recurringTimestamp <= $until) {
                                $yearRecurringTimestamp = $recurringTimestamp;
                                // Adjust timezone from initial event
                                $recurringTimeZone = \DateTime::createFromFormat('U', $yearRecurringTimestamp);
                                $timezoneOffset = $initialStart->getTimezone()->getOffset($recurringTimeZone);
                                $yearRecurringTimestamp += $timezoneOffset != $initialStartOffset ? $initialStartOffset - $timezoneOffset : 0;
                                // Add specific month dates
                                if (isset($rrules['BYMONTH']) && $rrules['BYMONTH'] != '') {
                                    $eventStartDesc = "{$day} {$this->monthNames[$rrules['BYMONTH']]} " . gmdate('Y H:i:s', $yearRecurringTimestamp);
                                } else {
                                    $eventStartDesc = $day . gmdate('F Y H:i:s', $yearRecurringTimestamp);
                                }
                                $eventStartTimestamp = strtotime($eventStartDesc);
                                if ($eventStartTimestamp > $startTimestamp && $eventStartTimestamp < $until) {
                                    $anEvent['DTSTART'] = gmdate(self::DATE_TIME_FORMAT, $eventStartTimestamp) . 'Z';
                                    $anEvent['DTSTART_array'] = array(array(), $anEvent['DTSTART'], $eventStartTimestamp);
                                    $anEvent['DTEND_array'] = $anEvent['DTSTART_array'];
                                    $anEvent['DTEND_array'][2] += $eventTimestampOffset;
                                    $anEvent['DTEND'] = gmdate(self::DATE_TIME_FORMAT, $anEvent['DTEND_array'][2]) . 'Z';
                                    $anEvent['DTEND_array'][1] = $anEvent['DTEND'];
                                    $searchDate = $anEvent['DTSTART'];
                                    $isExcluded = array_filter($anEvent['EXDATE_array'][1], function ($val) use($searchDate) {
                                        return is_string($val) && strpos($searchDate, $val) === 0;
                                    });
                                    if (isset($this->alteredRecurrenceInstances[$anEvent['UID']]) && in_array($yearRecurringTimestamp, $this->alteredRecurrenceInstances[$anEvent['UID']])) {
                                        $isExcluded = true;
                                    }
                                    if (!$isExcluded) {
                                        $events[] = $anEvent;
                                        $this->eventCount++;
                                        // If RRULE[COUNT] is reached then break
                                        if (isset($rrules['COUNT'])) {
                                            $countNb++;
                                            if ($countNb >= $countOrig) {
                                                break 2;
                                            }
                                        }
                                    }
                                }
                                // Move forwards
                                $recurringTimestamp = strtotime($offset, $recurringTimestamp);
                            }
                        }
                        break;
                        $events = isset($countOrig) && sizeof($events) > $countOrig ? array_slice($events, 0, $countOrig) : $events;
                        // Ensure we abide by COUNT if defined
                }
            }
        }
        $this->cal['VEVENT'] = $events;
    }