diff --git a/README.md b/README.md index 542b238..d85ea0a 100644 --- a/README.md +++ b/README.md @@ -64,52 +64,59 @@ #### Functions -| Function | Parameter(s) | Visibility | Description | -|-------------------------------|-------------------------------------------------|-------------|-------------------------------------------------------------------------------------------------------------------------------| -| `__construct` | `$files = false`, `$options = array()` | `public` | Creates the ICal object | -| `initFile` | `$file` | `protected` | Initialises lines from a file | -| `initLines` | `$lines` | `protected` | Initialises the parser using an array containing each line of iCal content | -| `initString` | `$string` | `protected` | Initialises lines from a string | -| `initUrl` | `$url` | `protected` | Initialises lines from a URL | -| `cleanData` | `$data` | `protected` | Replace curly quotes and other special characters with their standard equivalents | -| `convertDayOrdinalToPositive` | `$dayNumber`, `$weekday`, `$timestamp` | `protected` | Convert a negative day ordinal to its equivalent positive form | -| `fileOrUrl` | `$filename` | `protected` | Reads an entire file or URL into an array | -| `isFileOrUrl` | `$filename` | `protected` | Check if filename exists as a file or URL | -| `isValidTimeZoneId` | `$timeZone` | `protected` | Check if a time zone is valid | -| `mb_str_replace` | `$search`, `$replace`, `$subject`, `$count = 0` | `protected` | Replace all occurrences of the search string with the replacement string. Multibyte safe. | -| `numberOfDays` | `$days`, `$start`, `$end` | `protected` | Get the number of days between a start and end date | -| `parseDuration` | `$date`, `$duration` | `protected` | Parse a duration and apply it to a date | -| `processDateConversions` | - | `protected` | Add fields `DTSTART_tz` and `DTEND_tz` to each Event | -| `processEvents` | - | `protected` | Performs some admin tasks on all events as taken straight from the ics file. | -| `processRecurrences` | - | `protected` | Processes recurrence rules | -| `removeUnprintableChars` | `$data` | `protected` | Remove unprintable ASCII and UTF-8 characters | -| `unfold` | `$lines` | `protected` | Unfold an iCal file in preparation for parsing | -| `calendarDescription` | - | `public` | Returns the calendar description | -| `calendarName` | - | `public` | Returns the calendar name | -| `calendarTimeZone` | - | `public` | Returns the calendar time zone | -| `events` | - | `public` | Returns an array of Events. Every event is a class with the event details being properties within it. | -| `eventsFromInterval` | `$interval` | `public` | Returns a sorted array of the events following a given string, or false if no events exist in the range. | -| `eventsFromRange` | `$rangeStart = false`, `$rangeEnd = false` | `public` | Returns a sorted array of the events in a given range, or an empty array if no events exist in the range. | -| `freeBusyEvents` | - | `public` | Returns an array of arrays with all free/busy events. Every event is an associative array and each property is an element it. | -| `hasEvents` | - | `public` | Returns a boolean value whether the current calendar has events or not | -| `iCalDateToUnixTimestamp` | `$icalDate`, `$forceTimeZone = false` | `public` | Return Unix timestamp from iCal date time format | -| `iCalDateWithTimeZone` | `$event`, `$key`, `$forceTimeZone` | `public` | Return a date adapted to the calendar time zone depending on the event TZID | -| `isValidDate` | `$value` | `public` | Check if a date string is a valid date | -| `parseExdates` | `$event` | `public` | Parse a list of excluded dates to be applied to an Event | -| `sortEventsWithOrder` | `$events`, `$sortOrder = SORT_ASC` | `public` | Sort events based on a given sort order | +| Function | Parameter(s) | Visibility | Description | +|-------------------------------|------------------------------------------------------------|-------------|-------------------------------------------------------------------------------------------------------------------------------| +| `__construct` | `$files = false`, `$options = array()` | `public` | Creates the ICal object | +| `initFile` | `$file` | `protected` | Initialises lines from a file | +| `initLines` | `$lines` | `protected` | Initialises the parser using an array containing each line of iCal content | +| `initString` | `$string` | `protected` | Initialises lines from a string | +| `initUrl` | `$url` | `protected` | Initialises lines from a URL | +| `cleanData` | `$data` | `protected` | Replace curly quotes and other special characters with their standard equivalents | +| `convertDayOrdinalToPositive` | `$dayNumber`, `$weekday`, `$timestamp` | `protected` | Convert a negative day ordinal to its equivalent positive form | +| `fileOrUrl` | `$filename` | `protected` | Reads an entire file or URL into an array | +| `isFileOrUrl` | `$filename` | `protected` | Check if filename exists as a file or URL | +| `isValidTimeZoneId` | `$timeZone` | `protected` | Check if a time zone is valid | +| `mb_str_replace` | `$search`, `$replace`, `$subject`, `$count = 0` | `protected` | Replace all occurrences of the search string with the replacement string. Multibyte safe. | +| `numberOfDays` | `$days`, `$start`, `$end` | `protected` | Get the number of days between a start and end date | +| `parseDuration` | `$date`, `$duration` | `protected` | Parse a duration and apply it to a date | +| `processDateConversions` | - | `protected` | Add fields `DTSTART_tz` and `DTEND_tz` to each Event | +| `processEvents` | - | `protected` | Performs some admin tasks on all events as taken straight from the ics file. | +| `processRecurrences` | - | `protected` | Processes recurrence rules | +| `removeUnprintableChars` | `$data` | `protected` | Remove unprintable ASCII and UTF-8 characters | +| `unfold` | `$lines` | `protected` | Unfold an iCal file in preparation for parsing | +| `calendarDescription` | - | `public` | Returns the calendar description | +| `calendarName` | - | `public` | Returns the calendar name | +| `calendarTimeZone` | `$ignoreUtc` | `public` | Returns the calendar time zone | +| `events` | - | `public` | Returns an array of Events. Every event is a class with the event details being properties within it. | +| `eventsFromInterval` | `$interval` | `public` | Returns a sorted array of the events following a given string, or false if no events exist in the range. | +| `eventsFromRange` | `$rangeStart = false`, `$rangeEnd = false` | `public` | Returns a sorted array of the events in a given range, or an empty array if no events exist in the range. | +| `freeBusyEvents` | - | `public` | Returns an array of arrays with all free/busy events. Every event is an associative array and each property is an element it. | +| `hasEvents` | - | `public` | Returns a boolean value whether the current calendar has events or not | +| `iCalDateToUnixTimestamp` | `$icalDate`, `$forceTimeZone = false`, `$forceUtc = false` | `public` | Return Unix timestamp from iCal date time format | +| `iCalDateWithTimeZone` | `$event`, `$key`, `$forceTimeZone` | `public` | Return a date adapted to the calendar time zone depending on the event TZID | +| `isValidDate` | `$value` | `public` | Check if a date string is a valid date | +| `parseExdates` | `$event` | `public` | Parse a list of excluded dates to be applied to an Event | +| `sortEventsWithOrder` | `$events`, `$sortOrder = SORT_ASC` | `public` | Sort events based on a given sort order | --- ### `Event` API +#### Constants + +| Name | Description | +|---------------------|---------------------------------------------------------------| +| `TIMEZONE_TEMPLATE` | `sprintf` template for use with `updateEventTimeZoneString()` | + #### Functions -| Function | Parameter(s) | Description | -|---------------|---------------------------------------------|--------------------------------------------------------------------| -| `__construct` | `$data = array()` | Creates the Event object | -| `prepareDate` | `$value` | Prepares the data for output | -| `printData` | `$html = '

%s: %s

'` | Return Event data excluding anything blank within an HTML template | -| `snakeCase` | `$input`, `$glue = '_'`, `$separator = '-'` | Convert the given input to snake_case | +| Function | Parameter(s) | Description | +|-----------------------------|---------------------------------------------|---------------------------------------------------------------------------------------------------------| +| `__construct` | `$ical`, `$data = array()` | Creates the Event object | +| `prepareDate` | `$value` | Prepares the data for output | +| `printData` | `$html = '

%s: %s

'` | Return Event data excluding anything blank within an HTML template | +| `snakeCase` | `$input`, `$glue = '_'`, `$separator = '-'` | Convert the given input to snake_case | +| `updateEventTimeZoneString` | | Extend `{DTSTART|DTEND|RECURRENCE-ID}_array` to include `TZID=Timezone:YYYYMMDD[T]HHMMSS` of each event | --- diff --git a/examples/index.php b/examples/index.php index 717a623..7021701 100644 --- a/examples/index.php +++ b/examples/index.php @@ -16,6 +16,8 @@ } catch (\Exception $e) { die($e); } + +$forceTimeZone = false; ?> @@ -65,7 +67,8 @@

iCalDateToUnixTimestamp($event->dtstart)); + $unix = $ical->iCalDateToUnixTimestamp($event->dtstart_array[3], $forceTimeZone); + $dtstart = new \DateTime('@' . (int) $unix); echo $event->summary . ' (' . $dtstart->format('d-m-Y H:i') . ')'; ?>

printData() ?> @@ -93,7 +96,8 @@

iCalDateToUnixTimestamp($event->dtstart)); + $unix = $ical->iCalDateToUnixTimestamp($event->dtstart_array[3], $forceTimeZone); + $dtstart = new \DateTime('@' . (int) $unix); echo $event->summary . ' (' . $dtstart->format('d-m-Y H:i') . ')'; ?>

printData() ?> @@ -121,7 +125,8 @@

iCalDateToUnixTimestamp($event->dtstart)); + $unix = $ical->iCalDateToUnixTimestamp($event->dtstart_array[3], $forceTimeZone); + $dtstart = new \DateTime('@' . (int) $unix); echo $event->summary . ' (' . $dtstart->format('d-m-Y H:i') . ')'; ?>

printData() ?> diff --git a/src/ICal/Event.php b/src/ICal/Event.php index 4d24474..ca85026 100644 --- a/src/ICal/Event.php +++ b/src/ICal/Event.php @@ -8,6 +8,8 @@ class Event { + const TIMEZONE_TEMPLATE = 'TZID=%s:'; + /** * http://www.kanzaki.com/docs/ical/summary.html * @@ -113,19 +115,31 @@ class Event */ public $attendee; + /** + * The ICal instance + * + * @var ICal + */ + public $ical; + /** * Creates the Event object * + * @param ICal $ical * @param array $data * @return void */ - public function __construct(array $data = array()) + public function __construct($ical, array $data = array()) { + $this->ical = $ical; + if (!empty($data)) { foreach ($data as $key => $value) { $variable = self::snakeCase($key); $this->{$variable} = self::prepareData($value); } + + $this->updateEventTimeZoneString(); } } @@ -200,4 +214,29 @@ protected static function snakeCase($input, $glue = '_', $separator = '-') return strtolower($input); } + + /** + * Extend `{DTSTART|DTEND|RECURRENCE-ID}_array` to include + * `TZID=Timezone:YYYYMMDD[T]HHMMSS` of each event + * + * @return void + */ + protected function updateEventTimeZoneString() + { + $eventTimeZoneStringIndex = 3; + $calendarTimeZone = $this->ical->calendarTimeZone(true); + + $dtStartTimeZone = (isset($this->dtstart_array[0]['TZID'])) ? $this->dtstart_array[0]['TZID'] : $calendarTimeZone; + $this->dtstart_array[$eventTimeZoneStringIndex] = ((is_null($dtStartTimeZone)) ? '' : sprintf(self::TIMEZONE_TEMPLATE, $dtStartTimeZone)) . $this->dtstart_array[1]; + + if (isset($this->dtend_array)) { + $dtEndTimeZone = (isset($this->dtend_array[0]['TZID'])) ? $this->dtend_array[0]['TZID'] : $calendarTimeZone; + $this->dtend_array[$eventTimeZoneStringIndex] = ((is_null($dtEndTimeZone)) ? '' : sprintf(self::TIMEZONE_TEMPLATE, $dtEndTimeZone)) . $this->dtend_array[1]; + } + + if (isset($this->recurrence_id_array)) { + $recurrenceIdTimeZone = (isset($this->recurrence_id_array[0]['TZID'])) ? $this->recurrence_id_array[0]['TZID'] : $calendarTimeZone; + $this->recurrence_id_array[$eventTimeZoneStringIndex] = ((is_null($recurrenceIdTimeZone)) ? '' : sprintf(self::TIMEZONE_TEMPLATE, $recurrenceIdTimeZone)) . $this->recurrence_id_array[1]; + } + } } \ No newline at end of file diff --git a/src/ICal/ICal.php b/src/ICal/ICal.php index 2b70bc8..d45f8e1 100644 --- a/src/ICal/ICal.php +++ b/src/ICal/ICal.php @@ -576,9 +576,10 @@ protected function keyValueFromString($text) * YYYYMMDD[T]HHMMSS or * TZID=Timezone:YYYYMMDD[T]HHMMSS * @param boolean $forceTimeZone + * @param boolean $forceUtc * @return integer */ - public function iCalDateToUnixTimestamp($icalDate, $forceTimeZone = false) + public function iCalDateToUnixTimestamp($icalDate, $forceTimeZone = false, $forceUtc = false) { /** * iCal times may be in 3 formats, (http://www.kanzaki.com/docs/ical/dateTime.html) @@ -629,6 +630,10 @@ public function iCalDateToUnixTimestamp($icalDate, $forceTimeZone = false) $convDate->setDate((int) $date[2], (int) $date[3], (int) $date[4]); $convDate->setTime((int) $date[5], (int) $date[6], (int) $date[7]); + if ($forceUtc) { + $convDate->setTimezone(new \DateTimeZone('UTC')); + } + return $convDate->getTimestamp(); } @@ -712,6 +717,7 @@ protected function processEvents() $date = 'TZID=' . $anEvent[$type . '_array'][0]['TZID'] . ':' . $date; } $anEvent[$type . '_array'][2] = $this->iCalDateToUnixTimestamp($date); + $anEvent[$type . '_array'][3] = $date; } } @@ -720,7 +726,8 @@ protected function processEvents() if (!isset($this->alteredRecurrenceInstances[$uid])) { $this->alteredRecurrenceInstances[$uid] = array(); } - $this->alteredRecurrenceInstances[$uid][] = $anEvent['RECURRENCE-ID_array'][2]; + $recurrenceDateUtc = $this->iCalDateToUnixTimestamp($anEvent['RECURRENCE-ID_array'][3], true, true); + $this->alteredRecurrenceInstances[$uid][] = $recurrenceDateUtc; } $events[$key] = $anEvent; @@ -902,8 +909,11 @@ protected function processRecurrences() }); if (isset($anEvent['UID'])) { - if (isset($this->alteredRecurrenceInstances[$anEvent['UID']]) && in_array($dayRecurringTimestamp, $this->alteredRecurrenceInstances[$anEvent['UID']])) { - $isExcluded = true; + if (isset($this->alteredRecurrenceInstances[$anEvent['UID']])) { + $searchDateUtc = $this->iCalDateToUnixTimestamp($searchDate, true, true); + if (in_array($searchDateUtc, $this->alteredRecurrenceInstances[$anEvent['UID']])) { + $isExcluded = true; + } } } @@ -992,8 +1002,11 @@ protected function processRecurrences() }); if (isset($anEvent['UID'])) { - if (isset($this->alteredRecurrenceInstances[$anEvent['UID']]) && in_array($dayRecurringTimestamp, $this->alteredRecurrenceInstances[$anEvent['UID']])) { - $isExcluded = true; + if (isset($this->alteredRecurrenceInstances[$anEvent['UID']])) { + $searchDateUtc = $this->iCalDateToUnixTimestamp($searchDate, true, true); + if (in_array($searchDateUtc, $this->alteredRecurrenceInstances[$anEvent['UID']])) { + $isExcluded = true; + } } } @@ -1097,8 +1110,11 @@ protected function processRecurrences() }); if (isset($anEvent['UID'])) { - if (isset($this->alteredRecurrenceInstances[$anEvent['UID']]) && in_array($monthRecurringTimestamp, $this->alteredRecurrenceInstances[$anEvent['UID']])) { - $isExcluded = true; + if (isset($this->alteredRecurrenceInstances[$anEvent['UID']])) { + $searchDateUtc = $this->iCalDateToUnixTimestamp($searchDate, true, true); + if (in_array($searchDateUtc, $this->alteredRecurrenceInstances[$anEvent['UID']])) { + $isExcluded = true; + } } } @@ -1153,11 +1169,6 @@ protected function processRecurrences() $compareCurrentMonth = date('F', $monthRecurringTimestamp); $compareEventMonth = date('F', $eventStartTimestamp); - if ($compareCurrentMonth != $compareEventMonth) { - $monthRecurringTimestamp = strtotime($offset, $monthRecurringTimestamp); - continue; - } - if ($eventStartTimestamp > $startTimestamp && $eventStartTimestamp < $until) { $anEvent['DTSTART'] = date(self::DATE_TIME_FORMAT, $eventStartTimestamp) . ($isAllDayEvent || ($initialStartTimeZoneName === 'Z') ? 'Z' : ''); $anEvent['DTSTART_array'][1] = $anEvent['DTSTART']; @@ -1183,8 +1194,11 @@ protected function processRecurrences() }); if (isset($anEvent['UID'])) { - if (isset($this->alteredRecurrenceInstances[$anEvent['UID']]) && in_array($monthRecurringTimestamp, $this->alteredRecurrenceInstances[$anEvent['UID']])) { - $isExcluded = true; + if (isset($this->alteredRecurrenceInstances[$anEvent['UID']])) { + $searchDateUtc = $this->iCalDateToUnixTimestamp($searchDate, true, true); + if (in_array($searchDateUtc, $this->alteredRecurrenceInstances[$anEvent['UID']])) { + $isExcluded = true; + } } } @@ -1285,8 +1299,11 @@ protected function processRecurrences() }); if (isset($anEvent['UID'])) { - if (isset($this->alteredRecurrenceInstances[$anEvent['UID']]) && in_array($yearRecurringTimestamp, $this->alteredRecurrenceInstances[$anEvent['UID']])) { - $isExcluded = true; + if (isset($this->alteredRecurrenceInstances[$anEvent['UID']])) { + $searchDateUtc = $this->iCalDateToUnixTimestamp($searchDate, true, true); + if (in_array($searchDateUtc, $this->alteredRecurrenceInstances[$anEvent['UID']])) { + $isExcluded = true; + } } } @@ -1365,8 +1382,11 @@ protected function processRecurrences() }); if (isset($anEvent['UID'])) { - if (isset($this->alteredRecurrenceInstances[$anEvent['UID']]) && in_array($yearRecurringTimestamp, $this->alteredRecurrenceInstances[$anEvent['UID']])) { - $isExcluded = true; + if (isset($this->alteredRecurrenceInstances[$anEvent['UID']])) { + $searchDateUtc = $this->iCalDateToUnixTimestamp($searchDate, true, true); + if (in_array($searchDateUtc, $this->alteredRecurrenceInstances[$anEvent['UID']])) { + $isExcluded = true; + } } } @@ -1458,7 +1478,7 @@ public function events() if (!empty($array)) { foreach ($array as $event) { - $events[] = new Event($event); + $events[] = new Event($this, $event); } } @@ -1488,21 +1508,26 @@ public function calendarDescription() /** * Returns the calendar time zone * + * @return boolean $ignoreUtc * @return string */ - public function calendarTimeZone() + public function calendarTimeZone($ignoreUtc = false) { if (isset($this->cal['VCALENDAR']['X-WR-TIMEZONE'])) { $timeZone = $this->cal['VCALENDAR']['X-WR-TIMEZONE']; } elseif (isset($this->cal['VTIMEZONE']['TZID'])) { $timeZone = $this->cal['VTIMEZONE']['TZID']; } else { - return $this->defaultTimeZone; + $timeZone = $this->defaultTimeZone; } - // Use default time zone if defined is invalid + // Use default time zone if the calendar's is invalid if (!$this->isValidTimeZoneId($timeZone)) { - return $this->defaultTimeZone; + $timeZone = $this->defaultTimeZone; + } + + if ($ignoreUtc && $timeZone === 'UTC') { + return null; } return $timeZone;