Copyright 2003-2016 Horde LLC (http://www.horde.org/)
See the enclosed file COPYING for license information (LGPL). If you
did not receive this file, see http://www.horde.org/licenses/lgpl21.
/** * Adds rules from this ruleset to a VTIMEZONE component. * * @param Horde_Icalendar_Vtimezone $tz A VTIMEZONE component. * @param string $tzid The timezone ID of the component. * @param string $name A timezone name abbreviation. * May contain a placeholder that is * replaced the Rules' "Letter(s)" * entry. * @param array $startOffset An offset hash describing the * base offset of a timezone. * @param Horde_Date $start Start of the period to add rules * for. * @param Horde_Date $end End of the period to add rules * for. */ public function addRules(Horde_Icalendar_Vtimezone $tz, $tzid, $name, $startOffset, Horde_Date $start, Horde_Date $end = null) { $offset = $startOffset; foreach ($this->_rules as $rule) { $year = $rule[3]; if ($year[0] == 'o') { // TO is "only" $rule[3] = $rule[2]; } if ($rule[3][0] != 'm' && $rule[3] < $start->year) { // TO is not maximum and is before the searched period continue; } if ($end && $rule[2][0] != 'm' && $rule[2] > $end->year) { // FROM is not "minimum" and is after the searched period break; } if ($rule[2][0] != 'm' && $rule[2] < $start->year) { $rule[2] = $start->year; } if ($rule[8] == 0) { $component = new Horde_Icalendar_Standard(); $component->setAttribute('TZOFFSETFROM', $offset); $component->setAttribute('TZOFFSETTO', $startOffset); $offset = $startOffset; } else { $component = new Horde_Icalendar_Daylight(); $component->setAttribute('TZOFFSETFROM', $offset); $offset = $this->_getOffset($startOffset, $rule[8]); $component->setAttribute('TZOFFSETTO', $offset); } $month = Horde_Timezone::getMonth($rule[5]); // Retrieve time of rule start. preg_match('/(\\d+)(?::(\\d+))?(?::(\\d+))?(w|s|u)?/', $rule[7], $match); if (!isset($match[2])) { $match[2] = 0; } if ($rule[2] == $rule[3] && preg_match('/^\\d+$/', $rule[6])) { // Rule lasts only for a single year and starts on a specific // date. $rdate = new Horde_Date(array('year' => $rule[2], 'month' => Horde_Timezone::getMonth($rule[5]), 'mday' => $rule[6], 'hour' => $match[1], 'min' => $match[2], 'sec' => 0)); $component->setAttribute('DTSTART', $rdate); } elseif (substr($rule[6], 0, 4) == 'last') { // Rule starts on the last of a certain weekday of the month. $weekday = $this->_weekdays[substr($rule[6], 4, 3)]; $last = new Horde_Date(array('year' => $rule[2], 'month' => $month, 'mday' => Horde_Date_Utils::daysInMonth($month, $rule[2]), 'hour' => $match[1], 'min' => $match[2], 'sec' => 0)); while ($last->dayOfWeek() != $weekday) { $last->mday--; } $component->setAttribute('DTSTART', $last); if ($rule[3][0] == 'm') { $until = ''; } else { $last = new Horde_Date(array('year' => $rule[3], 'month' => $month, 'mday' => Horde_Date_Utils::daysInMonth($month, $rule[2]), 'hour' => $match[1], 'min' => $match[2], 'sec' => 0), $tzid); while ($last->dayOfWeek() != $weekday) { $last->mday--; } $last->setTimezone('UTC'); $until = ';UNTIL=' . $last->format('Ymd\\THIs') . 'Z'; } $component->setAttribute('RRULE', 'FREQ=YEARLY;BYDAY=-1' . Horde_String::upper(substr($rule[6], 4, 2)) . ';BYMONTH=' . $month . $until); } elseif (strpos($rule[6], '>=')) { // Rule starts on a certain weekday after a certain day of // month. list($weekday, $day) = explode('>=', $rule[6]); $weekdayInt = $this->_weekdays[substr($weekday, 0, 3)]; $first = new Horde_Date(array('year' => $rule[2], 'month' => $month, 'mday' => $day, 'hour' => $match[1], 'min' => $match[2], 'sec' => 0)); while ($first->dayOfWeek() != $weekdayInt) { $first->mday++; } $component->setAttribute('DTSTART', $first); if ($rule[3][0] == 'm') { $until = ''; } else { $last = new Horde_Date(array('year' => $rule[3], 'month' => $month, 'mday' => $day, 'hour' => $match[1], 'min' => $match[2], 'sec' => 0), $tzid); while ($last->dayOfWeek() != $weekday) { $last->mday++; } $last->setTimezone('UTC'); $until = ';UNTIL=' . $last->format('Ymd\\THIs') . 'Z'; } for ($days = array(), $i = $day, $lastDay = min(Horde_Date_Utils::daysInMonth($month, $rule[2]), $i + 6); $day > 1 && $i <= $lastDay; $i++) { $days[] = $i; } $component->setAttribute('RRULE', 'FREQ=YEARLY;BYMONTH=' . $month . ($days ? ';BYMONTHDAY=' . implode(',', $days) : '') . ';BYDAY=1' . Horde_String::upper(substr($weekday, 0, 2)) . $until); } elseif (strpos($rule[6], '<=')) { // Rule starts on a certain weekday before a certain day of // month. list($weekday, $day) = explode('>=', $rule[6]); $weekdayInt = $this->_weekdays[substr($weekday, 0, 3)]; $last = new Horde_Date(array('year' => $rule[2], 'month' => $month, 'mday' => $day, 'hour' => $match[1], 'min' => $match[2], 'sec' => 0)); while ($last->dayOfWeek() != $weekdayInt) { $last->mday--; } $component->setAttribute('DTSTART', $last); if ($rule[3][0] == 'm') { $until = ''; } else { $last = new Horde_Date(array('year' => $rule[3], 'month' => $month, 'mday' => $day, 'hour' => $match[1], 'min' => $match[2], 'sec' => 0), $tzid); while ($last->dayOfWeek() != $weekday) { $last->mday--; } $last->setTimezone('UTC'); $until = ';UNTIL=' . $last->format('Ymd\\THIs') . 'Z'; } for ($days = array(), $i = 1; $i <= $day; $i++) { $days[] = $i; } $component->setAttribute('RRULE', 'FREQ=YEARLY;BYMONTH=' . $month . ';BYMONTHDAY=' . implode(',', $days) . ';BYDAY=-1' . Horde_String::upper(substr($weekday, 0, 2)) . $until); } $component->setAttribute('TZNAME', sprintf($name, $rule[9])); $tz->addComponent($component); } }
/** * Parses child components of vTimezone component. * * Returns an array with the exact time of the time change as well as the * 'from' and 'to' offsets around the change. Time is arbitrarily based on * UTC for comparison. * * @param Horde_Icalendar_Standard|Horde_Icalendar_Daylight $child * A timezone component. * @param integer $year * The latest year we are interested in. * * @return array A list of hashes with "time", "from", and "to" elements. */ public function parseChild($child, $year) { $result['time'] = $result['end'] = 0; try { $t = $child->getAttribute('TZOFFSETFROM'); } catch (Horde_Icalendar_Exception $e) { return array(); } $result['from'] = ($t['hour'] * 60 * 60 + $t['minute'] * 60) * ($t['ahead'] ? 1 : -1); try { $t = $child->getAttribute('TZOFFSETTO'); } catch (Horde_Icalendar_Exception $e) { return array(); } $result['to'] = ($t['hour'] * 60 * 60 + $t['minute'] * 60) * ($t['ahead'] ? 1 : -1); try { $start = $child->getAttribute('DTSTART'); } catch (Horde_Icalendar_Exception $e) { return array(); } if (!is_int($start)) { return array(); } $start = getdate($start); if ($start['year'] > $year) { return array(); } $results = array(); try { $rdates = $child->getAttributeValues('RDATE'); foreach ($rdates as $rdate) { if ($rdate['year'] == $year || $rdate['year'] == $year - 1) { $result['time'] = $result['end'] = gmmktime($start['hours'], $start['minutes'], $start['seconds'], $rdate['month'], $rdate['mday'], $rdate['year']); $results[] = $result; } } } catch (Horde_Icalendar_Exception $e) { } try { $rrules = $child->getAttribute('RRULE'); } catch (Horde_Icalendar_Exception $e) { if (!$results) { $result['time'] = $result['end'] = $start[0]; $results[] = $result; } return $results; } $rrules = explode(';', $rrules); foreach ($rrules as $rrule) { $t = explode('=', $rrule); switch ($t[0]) { case 'FREQ': if ($t[1] != 'YEARLY') { return array(); } break; case 'INTERVAL': if ($t[1] != '1') { return array(); } break; case 'BYMONTH': $month = intval($t[1]); break; case 'BYDAY': $len = strspn($t[1], '1234567890-+'); if ($len == 0) { return array(); } $weekday = substr($t[1], $len); $weekdays = array('SU' => 0, 'MO' => 1, 'TU' => 2, 'WE' => 3, 'TH' => 4, 'FR' => 5, 'SA' => 6); $weekday = $weekdays[$weekday]; $which = intval(substr($t[1], 0, $len)); break; case 'UNTIL': $result['end'] = intval(substr($t[1], 0, 4)); break; } } if (empty($month) || !isset($weekday)) { return array(); } // Get the timestamp for the first day of $month. $when = gmmktime($start['hours'], $start['minutes'], $start['seconds'], $month, 1, $year); // Get the day of the week for the first day of $month. $first_of_month_weekday = intval(gmstrftime('%w', $when)); // Go to the first $weekday before first day of $month. if ($weekday >= $first_of_month_weekday) { $weekday -= 7; } $when -= ($first_of_month_weekday - $weekday) * 60 * 60 * 24; // If going backwards go to the first $weekday after last day // of $month. if ($which < 0) { do { $when += 60 * 60 * 24 * 7; } while (intval(gmstrftime('%m', $when)) == $month); } // Calculate $weekday number $which. $when += $which * 60 * 60 * 24 * 7; $result['time'] = $when; $results[] = $result; return $results; }
/** * Exports this zone to a VTIMEZONE component. * * @return Horde_Icalendar_Vtimezone A VTIMEZONE component representing * this timezone. * @throws Horde_Timezone_Exception */ public function toVtimezone() { if (!count($this->_info)) { throw new Horde_Timezone_Exception('No rules found for timezone ' . $this->_name); } $tz = new Horde_Icalendar_Vtimezone(); $tz->setAttribute('TZID', $this->_name); if (count($this->_info[0]) <= 3) { // Zone has no start or end date, but DAYLIGHT and STANDARD // components are required to have a DTSTART attribute [RFC // 5545 3.8.2.4]. return $tz; } $startDate = $this->_getDate(0); $startOffset = $this->_getOffset(0); for ($i = 1, $c = count($this->_info); $i < $c; $i++) { $name = $this->_info[$i][2]; $endDate = count($this->_info[$i]) > 3 ? $this->_getDate($i) : null; if ($this->_info[$i][1] == '-') { // Standard time. $component = new Horde_Icalendar_Standard(); } elseif (preg_match('/\\d+(:(\\d+))?/', $this->_info[$i][1])) { // Indiviual rule not matching any ruleset. $component = new Horde_Icalendar_Daylight(); } else { // Represented by a ruleset. $startOffset = $this->_getOffset($i); $this->_tz->getRule($this->_info[$i][1])->addRules($tz, $this->_name, $name, $startOffset, $startDate, $endDate); $startDate = $endDate; // Continue, because addRules() already adds the // component to $tz. continue; } $component->setAttribute('DTSTART', $startDate); $component->setAttribute('TZOFFSETFROM', $startOffset); $startOffset = $this->_getOffset($i); $component->setAttribute('TZOFFSETTO', $startOffset); $component->setAttribute('TZNAME', $name); $tz->addComponent($component); } return $tz; }
/** * Adds rules from this ruleset to a VTIMEZONE component. * * @param Horde_Icalendar_Vtimezone $tz A VTIMEZONE component. * @param string $tzid The timezone ID of the component. * @param string $name A timezone name abbreviation. * May contain a placeholder that is * replaced the Rules' "Letter(s)" * entry. * @param array $startOffset An offset hash describing the * base offset of a timezone. * @param Horde_Date $start Start of the period to add rules * for. * @param Horde_Date $end End of the period to add rules * for. */ public function addRules(Horde_Icalendar_Vtimezone $tz, $tzid, $name, $startOffset, Horde_Date $start, Horde_Date $end = null) { foreach ($this->_rules as $ruleNo => $rule) { // The rule items are: // 0: "Rule" // 1: The rule name // 2: The start year or "minimum" // 3: The end year or "only" if only at the start year or "maximum" // 4: Type, whatever that means (unused) // 5: The start month // 6: The start day, either specific or as a rule // 7: The start time // 8: The offset to the base time // 9: The time name abbreviation $year = $rule[3]; if ($year[0] == 'o') { // TO is "only" $rule[3] = $rule[2]; } if ($rule[3][0] != 'm' && $rule[3] < $start->year) { // TO is not "maximum" and is before the searched period continue; } if ($end && $rule[2][0] != 'm' && $rule[2] > $end->year) { // FROM is not "minimum" and is after the searched period break; } if ($rule[2][0] != 'm' && $rule[2] < $start->year) { $rule[2] = $start->year; } // The month of rule start. $month = Horde_Timezone::getMonth($rule[5]); // The time of rule start. preg_match('/(\\d+)(?::(\\d+))?(?::(\\d+))?([wsguz])?/', $rule[7], $match); $hour = $match[1]; $minute = isset($match[2]) ? $match[2] : 0; if (!isset($match[4])) { $modifier = 'w'; } elseif ($match[4] == 'g' || $match[4] == 'z') { $modifier = 'u'; } else { $modifier = $match[4]; } // Find the start date. $first = $this->_getFirstMatch($rule, $rule[2]); $first->hour = $hour; $first->min = $minute; $previousOffset = $this->_findPreviousOffset($first, $ruleNo, $startOffset); if ($rule[8] == 0) { $component = new Horde_Icalendar_Standard(); $component->setAttribute('TZOFFSETFROM', $previousOffset); $component->setAttribute('TZOFFSETTO', $startOffset); } else { $component = new Horde_Icalendar_Daylight(); $component->setAttribute('TZOFFSETFROM', $previousOffset); $component->setAttribute('TZOFFSETTO', $this->_getOffset($startOffset, $rule[8])); } switch ($modifier) { case 's': $first->hour += ($previousOffset['ahead'] ? 1 : -1) * $previousOffset['hour'] - ($startOffset['ahead'] ? 1 : -1) * $startOffset['hour']; $first->min += ($previousOffset['ahead'] ? 1 : -1) * $previousOffset['minute'] - ($startOffset['ahead'] ? 1 : -1) * $startOffset['minute']; break; case 'u': $first->hour += ($previousOffset['ahead'] ? 1 : -1) * $previousOffset['hour']; $first->min += ($previousOffset['ahead'] ? 1 : -1) * $previousOffset['minute']; break; } $component->setAttribute('DTSTART', $first); // Find the end date. if ($rule[3][0] == 'm') { $until = ''; } else { $last = $this->_getFirstMatch($rule, $rule[3]); $last->hour = $hour; $last->min = $minute; switch ($modifier) { case 's': $last->hour -= ($startOffset['ahead'] ? 1 : -1) * $startOffset['hour']; $last->min -= ($startOffset['ahead'] ? 1 : -1) * $startOffset['minute']; break; case 'w': $last->hour -= ($previousOffset['ahead'] ? 1 : -1) * $previousOffset['hour']; $last->min -= ($previousOffset['ahead'] ? 1 : -1) * $previousOffset['minute']; break; } $until = ';UNTIL=' . $last->format('Ymd\\THis') . 'Z'; } if ($rule[2] != $rule[3]) { if (preg_match('/^\\d+$/', $rule[6])) { // Rule starts on a specific date. $component->setAttribute('RRULE', 'FREQ=YEARLY;BYMONTH=' . $month . ';BYMONTHDAY=' . $rule[6] . $until); } elseif (substr($rule[6], 0, 4) == 'last') { // Rule starts on the last of a certain weekday of the month. $component->setAttribute('RRULE', 'FREQ=YEARLY;BYDAY=-1' . Horde_String::upper(substr($rule[6], 4, 2)) . ';BYMONTH=' . $month . $until); } elseif (strpos($rule[6], '>=')) { // Rule starts on a certain weekday after a certain day of // month. list($weekday, $day) = explode('>=', $rule[6]); for ($days = array(), $i = $day, $lastDay = min(Horde_Date_Utils::daysInMonth($month, $rule[2]), $i + 6); $day > 1 && $i <= $lastDay; $i++) { $days[] = $i; } $component->setAttribute('RRULE', 'FREQ=YEARLY;BYMONTH=' . $month . ($days ? ';BYMONTHDAY=' . implode(',', $days) : '') . ';BYDAY=1' . Horde_String::upper(substr($weekday, 0, 2)) . $until); } elseif (strpos($rule[6], '<=')) { // Rule starts on a certain weekday before a certain day of // month. for ($days = array(), $i = 1; $i <= $day; $i++) { $days[] = $i; } $component->setAttribute('RRULE', 'FREQ=YEARLY;BYMONTH=' . $month . ';BYMONTHDAY=' . implode(',', $days) . ';BYDAY=-1' . Horde_String::upper(substr($weekday, 0, 2)) . $until); } else { continue; } } $component->setAttribute('TZNAME', sprintf($name, $rule[9])); $tz->addComponent($component); } }