\+|-)?P((?\d+)W)?((?\d+)D)?(T((?\d+)H)?((?\d+)M)?((?\d+)S)?)?$/', $duration, $matches); if (!$result) { throw new InvalidDataException('The supplied iCalendar duration value is incorrect: ' . $duration); } if (!$asString) { $invert = false; if ($matches['plusminus'] === '-') { $invert = true; } $parts = [ 'week', 'day', 'hour', 'minute', 'second', ]; foreach ($parts as $part) { $matches[$part] = isset($matches[$part]) && $matches[$part] ? (int)$matches[$part] : 0; } // We need to re-construct the $duration string, because weeks and // days are not supported by DateInterval in the same string. $duration = 'P'; $days = $matches['day']; if ($matches['week']) { $days += $matches['week'] * 7; } if ($days) { $duration .= $days . 'D'; } if ($matches['minute'] || $matches['second'] || $matches['hour']) { $duration .= 'T'; if ($matches['hour']) { $duration .= $matches['hour'] . 'H'; } if ($matches['minute']) { $duration .= $matches['minute'] . 'M'; } if ($matches['second']) { $duration .= $matches['second'] . 'S'; } } if ($duration === 'P') { $duration = 'PT0S'; } $iv = new DateInterval($duration); if ($invert) { $iv->invert = true; } return $iv; } $parts = [ 'week', 'day', 'hour', 'minute', 'second', ]; $newDur = ''; foreach ($parts as $part) { if (isset($matches[$part]) && $matches[$part]) { $newDur .= ' ' . $matches[$part] . ' ' . $part . 's'; } } $newDur = ($matches['plusminus'] === '-' ? '-' : '+') . trim($newDur); if ($newDur === '+') { $newDur = '+0 seconds'; }; return $newDur; } /** * Parses either a Date or DateTime, or Duration value. * * @param string $date * @param DateTimeZone|string $referenceTz * * @return DateTimeImmutable|DateInterval */ static function parse($date, $referenceTz = null) { if ($date[0] === 'P' || ($date[0] === '-' && $date[1] === 'P')) { return self::parseDuration($date); } elseif (strlen($date) === 8) { return self::parseDate($date, $referenceTz); } else { return self::parseDateTime($date, $referenceTz); } } /** * This method parses a vCard date and or time value. * * This can be used for the DATE, DATE-TIME, TIMESTAMP and * DATE-AND-OR-TIME value. * * This method returns an array, not a DateTime value. * * The elements in the array are in the following order: * year, month, date, hour, minute, second, timezone * * Almost any part of the string may be omitted. It's for example legal to * just specify seconds, leave out the year, etc. * * Timezone is either returned as 'Z' or as '+0800' * * For any non-specified values null is returned. * * List of date formats that are supported: * YYYY * YYYY-MM * YYYYMMDD * --MMDD * ---DD * * YYYY-MM-DD * --MM-DD * ---DD * * List of supported time formats: * * HH * HHMM * HHMMSS * -MMSS * --SS * * HH * HH:MM * HH:MM:SS * -MM:SS * --SS * * A full basic-format date-time string looks like : * 20130603T133901 * * A full extended-format date-time string looks like : * 2013-06-03T13:39:01 * * Times may be postfixed by a timezone offset. This can be either 'Z' for * UTC, or a string like -0500 or +1100. * * @param string $date * * @return array */ static function parseVCardDateTime($date) { $regex = '/^ (?: # date part (?: (?: (? [0-9]{4}) (?: -)?| --) (? [0-9]{2})? |---) (? [0-9]{2})? )? (?:T # time part (? [0-9]{2} | -) (? [0-9]{2} | -)? (? [0-9]{2})? (?: \.[0-9]{3})? # milliseconds (?P # timezone offset Z | (?: \+|-)(?: [0-9]{4}) )? )? $/x'; if (!preg_match($regex, $date, $matches)) { // Attempting to parse the extended format. $regex = '/^ (?: # date part (?: (? [0-9]{4}) - | -- ) (? [0-9]{2}) - (? [0-9]{2}) )? (?:T # time part (?: (? [0-9]{2}) : | -) (?: (? [0-9]{2}) : | -)? (? [0-9]{2})? (?: \.[0-9]{3})? # milliseconds (?P # timezone offset Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2}) )? )? $/x'; if (!preg_match($regex, $date, $matches)) { throw new InvalidDataException('Invalid vCard date-time string: ' . $date); } } $parts = [ 'year', 'month', 'date', 'hour', 'minute', 'second', 'timezone' ]; $result = []; foreach ($parts as $part) { if (empty($matches[$part])) { $result[$part] = null; } elseif ($matches[$part] === '-' || $matches[$part] === '--') { $result[$part] = null; } else { $result[$part] = $matches[$part]; } } return $result; } /** * This method parses a vCard TIME value. * * This method returns an array, not a DateTime value. * * The elements in the array are in the following order: * hour, minute, second, timezone * * Almost any part of the string may be omitted. It's for example legal to * just specify seconds, leave out the hour etc. * * Timezone is either returned as 'Z' or as '+08:00' * * For any non-specified values null is returned. * * List of supported time formats: * * HH * HHMM * HHMMSS * -MMSS * --SS * * HH * HH:MM * HH:MM:SS * -MM:SS * --SS * * A full basic-format time string looks like : * 133901 * * A full extended-format time string looks like : * 13:39:01 * * Times may be postfixed by a timezone offset. This can be either 'Z' for * UTC, or a string like -0500 or +11:00. * * @param string $date * * @return array */ static function parseVCardTime($date) { $regex = '/^ (? [0-9]{2} | -) (? [0-9]{2} | -)? (? [0-9]{2})? (?: \.[0-9]{3})? # milliseconds (?P # timezone offset Z | (?: \+|-)(?: [0-9]{4}) )? $/x'; if (!preg_match($regex, $date, $matches)) { // Attempting to parse the extended format. $regex = '/^ (?: (? [0-9]{2}) : | -) (?: (? [0-9]{2}) : | -)? (? [0-9]{2})? (?: \.[0-9]{3})? # milliseconds (?P # timezone offset Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2}) )? $/x'; if (!preg_match($regex, $date, $matches)) { throw new InvalidDataException('Invalid vCard time string: ' . $date); } } $parts = [ 'hour', 'minute', 'second', 'timezone' ]; $result = []; foreach ($parts as $part) { if (empty($matches[$part])) { $result[$part] = null; } elseif ($matches[$part] === '-') { $result[$part] = null; } else { $result[$part] = $matches[$part]; } } return $result; } /** * This method parses a vCard date and or time value. * * This can be used for the DATE, DATE-TIME and * DATE-AND-OR-TIME value. * * This method returns an array, not a DateTime value. * The elements in the array are in the following order: * year, month, date, hour, minute, second, timezone * Almost any part of the string may be omitted. It's for example legal to * just specify seconds, leave out the year, etc. * * Timezone is either returned as 'Z' or as '+0800' * * For any non-specified values null is returned. * * List of date formats that are supported: * 20150128 * 2015-01 * --01 * --0128 * ---28 * * List of supported time formats: * 13 * 1353 * 135301 * -53 * -5301 * --01 (unreachable, see the tests) * --01Z * --01+1234 * * List of supported date-time formats: * 20150128T13 * --0128T13 * ---28T13 * ---28T1353 * ---28T135301 * ---28T13Z * ---28T13+1234 * * See the regular expressions for all the possible patterns. * * Times may be postfixed by a timezone offset. This can be either 'Z' for * UTC, or a string like -0500 or +1100. * * @param string $date * * @return array */ static function parseVCardDateAndOrTime($date) { // \d{8}|\d{4}-\d\d|--\d\d(\d\d)?|---\d\d $valueDate = '/^(?J)(?:' . '(?\d{4})(?\d\d)(?\d\d)' . '|(?\d{4})-(?\d\d)' . '|--(?\d\d)(?\d\d)?' . '|---(?\d\d)' . ')$/'; // (\d\d(\d\d(\d\d)?)?|-\d\d(\d\d)?|--\d\d)(Z|[+\-]\d\d(\d\d)?)? $valueTime = '/^(?J)(?:' . '((?\d\d)((?\d\d)(?\d\d)?)?' . '|-(?\d\d)(?\d\d)?' . '|--(?\d\d))' . '(?(Z|[+\-]\d\d(\d\d)?))?' . ')$/'; // (\d{8}|--\d{4}|---\d\d)T\d\d(\d\d(\d\d)?)?(Z|[+\-]\d\d(\d\d?)? $valueDateTime = '/^(?:' . '((?\d{4})(?\d\d)(?\d\d)' . '|--(?\d\d)(?\d\d)' . '|---(?\d\d))' . 'T' . '(?\d\d)((?\d\d)(?\d\d)?)?' . '(?(Z|[+\-]\d\d(\d\d?)))?' . ')$/'; // date-and-or-time is date | date-time | time // in this strict order. if (0 === preg_match($valueDate, $date, $matches) && 0 === preg_match($valueDateTime, $date, $matches) && 0 === preg_match($valueTime, $date, $matches)) { throw new InvalidDataException('Invalid vCard date-time string: ' . $date); } $parts = [ 'year' => null, 'month' => null, 'date' => null, 'hour' => null, 'minute' => null, 'second' => null, 'timezone' => null ]; // The $valueDateTime expression has a bug with (?J) so we simulate it. $parts['date0'] = &$parts['date']; $parts['date1'] = &$parts['date']; $parts['date2'] = &$parts['date']; $parts['month0'] = &$parts['month']; $parts['month1'] = &$parts['month']; $parts['year0'] = &$parts['year']; foreach ($parts as $part => &$value) { if (!empty($matches[$part])) { $value = $matches[$part]; } } unset($parts['date0']); unset($parts['date1']); unset($parts['date2']); unset($parts['month0']); unset($parts['month1']); unset($parts['year0']); return $parts; } }