diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -132,11 +132,16 @@ 'PhutilCIDRBlock' => 'ip/PhutilCIDRBlock.php', 'PhutilCIDRList' => 'ip/PhutilCIDRList.php', 'PhutilCLikeCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilCLikeCodeSnippetContextFreeGrammar.php', + 'PhutilCalendarAbsoluteDateTime' => 'parser/calendar/data/PhutilCalendarAbsoluteDateTime.php', 'PhutilCalendarContainerNode' => 'parser/calendar/data/PhutilCalendarContainerNode.php', + 'PhutilCalendarDateTime' => 'parser/calendar/data/PhutilCalendarDateTime.php', 'PhutilCalendarDocumentNode' => 'parser/calendar/data/PhutilCalendarDocumentNode.php', + 'PhutilCalendarDuration' => 'parser/calendar/data/PhutilCalendarDuration.php', 'PhutilCalendarEventNode' => 'parser/calendar/data/PhutilCalendarEventNode.php', 'PhutilCalendarNode' => 'parser/calendar/data/PhutilCalendarNode.php', + 'PhutilCalendarProxyDateTime' => 'parser/calendar/data/PhutilCalendarProxyDateTime.php', 'PhutilCalendarRawNode' => 'parser/calendar/data/PhutilCalendarRawNode.php', + 'PhutilCalendarRelativeDateTime' => 'parser/calendar/data/PhutilCalendarRelativeDateTime.php', 'PhutilCalendarRootNode' => 'parser/calendar/data/PhutilCalendarRootNode.php', 'PhutilCallbackFilterIterator' => 'utils/PhutilCallbackFilterIterator.php', 'PhutilCallbackSignalHandler' => 'future/exec/PhutilCallbackSignalHandler.php', @@ -708,11 +713,16 @@ 'PhutilCIDRBlock' => 'Phobject', 'PhutilCIDRList' => 'Phobject', 'PhutilCLikeCodeSnippetContextFreeGrammar' => 'PhutilCodeSnippetContextFreeGrammar', + 'PhutilCalendarAbsoluteDateTime' => 'PhutilCalendarDateTime', 'PhutilCalendarContainerNode' => 'PhutilCalendarNode', + 'PhutilCalendarDateTime' => 'Phobject', 'PhutilCalendarDocumentNode' => 'PhutilCalendarContainerNode', + 'PhutilCalendarDuration' => 'Phobject', 'PhutilCalendarEventNode' => 'PhutilCalendarNode', 'PhutilCalendarNode' => 'Phobject', + 'PhutilCalendarProxyDateTime' => 'PhutilCalendarDateTime', 'PhutilCalendarRawNode' => 'PhutilCalendarContainerNode', + 'PhutilCalendarRelativeDateTime' => 'PhutilCalendarProxyDateTime', 'PhutilCalendarRootNode' => 'PhutilCalendarContainerNode', 'PhutilCallbackFilterIterator' => 'FilterIterator', 'PhutilCallbackSignalHandler' => 'PhutilSignalHandler', diff --git a/src/parser/calendar/data/PhutilCalendarAbsoluteDateTime.php b/src/parser/calendar/data/PhutilCalendarAbsoluteDateTime.php new file mode 100644 --- /dev/null +++ b/src/parser/calendar/data/PhutilCalendarAbsoluteDateTime.php @@ -0,0 +1,115 @@ +year = $year; + return $this; + } + + public function getYear() { + return $this->year; + } + + public function setMonth($month) { + $this->month = $month; + return $this; + } + + public function getMonth() { + return $this->month; + } + + public function setDay($day) { + $this->day = $day; + return $this; + } + + public function getDay() { + return $this->day; + } + + public function setHour($hour) { + $this->hour = $hour; + return $this; + } + + public function getHour() { + return $this->hour; + } + + public function setMinute($minute) { + $this->minute = $minute; + return $this; + } + + public function getMinute() { + return $this->minute; + } + + public function setSecond($second) { + $this->second = $second; + return $this; + } + + public function getSecond() { + return $this->second; + } + + public function setTimezone($timezone) { + $this->timezone = $timezone; + return $this; + } + + public function getTimezone() { + return $this->timezone; + } + + private function getEffectiveTimezone() { + $zone = $this->getTimezone(); + if ($zone !== null) { + return $zone; + } + + $zone = $this->getViewerTimezone(); + if ($zone !== null) { + return $zone; + } + + throw new Exception( + pht( + 'Datetime has no timezone or viewer timezone.')); + } + + protected function newPHPDateTimeZone() { + $zone = $this->getEffectiveTimezone(); + return new DateTimeZone($zone); + } + + protected function newPHPDateTime() { + $zone = $this->newPHPDateTimeZone(); + + $y = $this->getYear(); + $m = $this->getMonth(); + $d = $this->getDay(); + + $h = $this->getHour(); + $i = $this->getMinute(); + $s = $this->getSecond(); + + $format = sprintf('%04d-%02d-%02d %02d:%02d:%02d', $y, $m, $d, $h, $i, $s); + + return new DateTime($format, $zone); + } + +} diff --git a/src/parser/calendar/data/PhutilCalendarDateTime.php b/src/parser/calendar/data/PhutilCalendarDateTime.php new file mode 100644 --- /dev/null +++ b/src/parser/calendar/data/PhutilCalendarDateTime.php @@ -0,0 +1,25 @@ +viewerTimezone = $viewer_timezone; + return $this; + } + + public function getViewerTimezone() { + return $this->viewerTimezone; + } + + public function getEpoch() { + $datetime = $this->newPHPDateTime(); + return (int)$datetime->format('U'); + } + + abstract protected function newPHPDateTimeZone(); + abstract protected function newPHPDateTime(); + +} diff --git a/src/parser/calendar/data/PhutilCalendarDuration.php b/src/parser/calendar/data/PhutilCalendarDuration.php new file mode 100644 --- /dev/null +++ b/src/parser/calendar/data/PhutilCalendarDuration.php @@ -0,0 +1,66 @@ +isNegative = $is_negative; + return $this; + } + + public function getIsNegative() { + return $this->isNegative; + } + + public function setDays($days) { + $this->days = $days; + return $this; + } + + public function getDays() { + return $this->days; + } + + public function setWeeks($weeks) { + $this->weeks = $weeks; + return $this; + } + + public function getWeeks() { + return $this->weeks; + } + + public function setHours($hours) { + $this->hours = $hours; + return $this; + } + + public function getHours() { + return $this->hours; + } + + public function setMinutes($minutes) { + $this->minutes = $minutes; + return $this; + } + + public function getMinutes() { + return $this->minutes; + } + + public function setSeconds($seconds) { + $this->seconds = $seconds; + return $this; + } + + public function getSeconds() { + return $this->seconds; + } + +} diff --git a/src/parser/calendar/data/PhutilCalendarEventNode.php b/src/parser/calendar/data/PhutilCalendarEventNode.php --- a/src/parser/calendar/data/PhutilCalendarEventNode.php +++ b/src/parser/calendar/data/PhutilCalendarEventNode.php @@ -5,4 +5,69 @@ const NODETYPE = 'event'; + private $name; + private $description; + private $startDateTime; + private $endDateTime; + private $duration; + + public function setName($name) { + $this->name = $name; + return $this; + } + + public function getName() { + return $this->name; + } + + public function setDescription($description) { + $this->description = $description; + return $this; + } + + public function getDescription() { + return $this->description; + } + + public function setStartDateTime(PhutilCalendarDateTime $start_date_time) { + $this->startDateTime = $start_date_time; + return $this; + } + + public function getStartDateTime() { + return $this->startDateTime; + } + + public function setEndDateTime(PhutilCalendarDateTime $end_date_time) { + $this->endDateTime = $end_date_time; + return $this; + } + + public function getEndDateTime() { + $end = $this->endDateTime; + if ($end) { + return $end; + } + + $start = $this->getStartDateTime(); + $duration = $this->getDuration(); + if ($start && $duration) { + return id(new PhutilCalendarRelativeDateTime()) + ->setOrigin($start) + ->setDuration($duration); + } + + return null; + } + + public function setDuration(PhutilCalendarDuration $duration) { + $this->duration = $duration; + return $this; + } + + public function getDuration() { + return $this->duration; + } + + } diff --git a/src/parser/calendar/data/PhutilCalendarProxyDateTime.php b/src/parser/calendar/data/PhutilCalendarProxyDateTime.php new file mode 100644 --- /dev/null +++ b/src/parser/calendar/data/PhutilCalendarProxyDateTime.php @@ -0,0 +1,34 @@ +proxy = $proxy; + return $this; + } + + final protected function getProxy() { + return $this->proxy; + } + + public function setViewerTimezone($timezone) { + $this->getProxy()->setViewerTimezone($timezone); + return $this; + } + + public function getViewerTimezone() { + return $this->getProxy()->getViewerTimezone(); + } + + protected function newPHPDateTimezone() { + return $this->getProxy()->newPHPDateTimezone(); + } + + protected function newPHPDateTime() { + return $this->getProxy()->newPHPDateTime(); + } + +} diff --git a/src/parser/calendar/data/PhutilCalendarRelativeDateTime.php b/src/parser/calendar/data/PhutilCalendarRelativeDateTime.php new file mode 100644 --- /dev/null +++ b/src/parser/calendar/data/PhutilCalendarRelativeDateTime.php @@ -0,0 +1,53 @@ +setProxy($origin); + } + + public function getOrigin() { + return $this->getProxy(); + } + + public function setDuration(PhutilCalendarDuration $duration) { + $this->duration = $duration; + return $this; + } + + public function getDuration() { + return $this->duration; + } + + protected function newPHPDateTime() { + $datetime = parent::newPHPDateTime(); + $duration = $this->getDuration(); + + if ($duration->getIsNegative()) { + $sign = '-'; + } else { + $sign = '+'; + } + + $map = array( + 'weeks' => $duration->getWeeks(), + 'days' => $duration->getDays(), + 'hours' => $duration->getHours(), + 'minutes' => $duration->getMinutes(), + 'seconds' => $duration->getSeconds(), + ); + + foreach ($map as $unit => $value) { + if (!$value) { + continue; + } + $datetime->modify("{$sign}{$value} {$unit}"); + } + + return $datetime; + } + +} diff --git a/src/parser/calendar/ics/PhutilICSParser.php b/src/parser/calendar/ics/PhutilICSParser.php --- a/src/parser/calendar/ics/PhutilICSParser.php +++ b/src/parser/calendar/ics/PhutilICSParser.php @@ -8,6 +8,8 @@ private $lines; private $cursor; + private $warnings; + const PARSE_MISSING_END = 'missing-end'; const PARSE_INITIAL_UNFOLD = 'initial-unfold'; const PARSE_UNEXPECTED_CHILD = 'unexpected-child'; @@ -22,11 +24,22 @@ const PARSE_MALFORMED_PROPERTY = 'malformed-property'; const PARSE_MISSING_VALUE = 'missing-value'; const PARSE_UNESCAPED_BACKSLASH = 'unescaped-backslash'; + const PARSE_MULTIPLE_PARAMETERS = 'multiple-parameters'; + const PARSE_EMPTY_DATETIME = 'empty-datetime'; + const PARSE_MANY_DATETIME = 'many-datetime'; + const PARSE_BAD_DATETIME = 'bad-datetime'; + const PARSE_BAD_TZID = 'bad-tzid'; + const PARSE_EMPTY_DURATION = 'empty-duration'; + const PARSE_MANY_DURATION = 'many-duration'; + const PARSE_BAD_DURATION = 'bad-duration'; + + const WARN_TZID_UTC = 'warn-tzid-utc'; public function parseICSData($data) { $this->stack = array(); $this->node = null; $this->cursor = null; + $this->warnings = array(); $lines = $this->unfoldICSLines($data); $this->lines = $lines; @@ -302,6 +315,7 @@ $node = $this->getNode(); + $raw = $node->getAttribute('ics.properties', array()); $raw[] = array( 'name' => $name, @@ -309,6 +323,12 @@ 'value' => $value, ); $node->setAttribute('ics.properties', $raw); + + switch ($node->getAttribute('ics.type')) { + case 'VEVENT': + $this->didParseEventProperty($node, $name, $parameters, $value); + break; + } } private function unescapeParameterValue($data) { @@ -465,10 +485,18 @@ $result = explode(',', $data); break; case 'DATE-TIME': - $result = explode(',', $data); + if (!strlen($data)) { + $result = array(); + } else { + $result = explode(',', $data); + } break; case 'DURATION': - $result = explode(',', $data); + if (!strlen($data)) { + $result = array(); + } else { + $result = explode(',', $data); + } break; case 'FLOAT': $result = explode(',', $data); @@ -572,4 +600,227 @@ ->setParserFailureCode($code); } + private function raiseWarning($code, $message) { + $this->warnings[] = array( + 'code' => $code, + 'line' => $this->cursor, + 'text' => $this->lines[$this->cursor], + 'message' => $message, + ); + + return $this; + } + + private function didParseEventProperty( + PhutilCalendarEventNode $node, + $name, + array $parameters, + array $value) { + + switch ($name) { + case 'SUMMARY': + $text = $this->newTextFromProperty($parameters, $value); + $node->setName($text); + break; + case 'DESCRIPTION': + $text = $this->newTextFromProperty($parameters, $value); + $node->setDescription($text); + break; + case 'DTSTART': + $datetime = $this->newDateTimeFromProperty($parameters, $value); + $node->setStartDateTime($datetime); + break; + case 'DTEND': + $datetime = $this->newDateTimeFromProperty($parameters, $value); + $node->setEndDateTime($datetime); + break; + case 'DURATION': + $duration = $this->newDurationFromProperty($parameters, $value); + $node->setDuration($duration); + break; + } + + } + + private function newTextFromProperty(array $parameters, array $value) { + $value = $value['value']; + return implode("\n\n", $value); + } + + private function newDateTimeFromProperty(array $parameters, array $value) { + $value = $value['value']; + + if (!$value) { + $this->raiseParseFailure( + self::PARSE_EMPTY_DATETIME, + pht( + 'Expected DATE-TIME to have exactly one value, found none.')); + + } + + if (count($value) > 1) { + $this->raiseParseFailure( + self::PARSE_MANY_DATETIME, + pht( + 'Expected DATE-TIME to have exactly one value, found more than '. + 'one.')); + } + + $value = head($value); + + $pattern = + '/^'. + '(?P\d{4})(?P\d{2})(?P\d{2})'. + '(?:'. + 'T(?P\d{2})(?P\d{2})(?P\d{2})(?Z)?'. + ')?'. + '\z/'; + + $matches = null; + $ok = preg_match($pattern, $value, $matches); + if (!$ok) { + $this->raiseParseFailure( + self::PARSE_BAD_DATETIME, + pht( + 'Expected DATE-TIME in the format "19990105T112233Z", found '. + '"%s".', + $value)); + } + + $tzid = $this->getScalarParameterValue($parameters, 'TZID'); + + if (isset($matches['z'])) { + if ($tzid) { + $this->raiseWarning( + self::WARN_TZID_UTC, + pht( + 'DATE-TIME "%s" uses "Z" to specify UTC, but also has a TZID '. + 'parameter with value "%s". This violates RFC5545. The TZID '. + 'will be ignored, and the value will be interpreted as UTC.', + $value, + $tzid)); + } + $tzid = 'UTC'; + } else if ($tzid !== null) { + $map = DateTimeZone::listIdentifiers(); + $map = array_fuse($map); + if (empty($map[$tzid])) { + $this->raiseParseFailure( + self::PARSE_BAD_TZID, + pht( + 'Timezone "%s" is not a recognized timezone.', + $tzid)); + } + } + + $datetime = id(new PhutilCalendarAbsoluteDateTime()) + ->setYear((int)$matches['y']) + ->setMonth((int)$matches['m']) + ->setDay((int)$matches['d']) + ->setTimezone($tzid); + + if (isset($matches['h'])) { + $datetime + ->setHour((int)$matches['h']) + ->setMinute((int)$matches['i']) + ->setSecond((int)$matches['s']); + } + + return $datetime; + } + + private function newDurationFromProperty(array $parameters, array $value) { + $value = $value['value']; + + if (!$value) { + $this->raiseParseFailure( + self::PARSE_EMPTY_DURATION, + pht( + 'Expected DURATION to have exactly one value, found none.')); + + } + + if (count($value) > 1) { + $this->raiseParseFailure( + self::PARSE_MANY_DURATION, + pht( + 'Expected DURATION to have exactly one value, found more than '. + 'one.')); + } + + $value = head($value); + + $pattern = + '/^'. + '(?P[+-])?'. + 'P'. + '(?:'. + '(?P\d+)W'. + '|'. + '(?:(?:(?P\d+)D)?'. + '(?:T(?:(?P\d+)H)?(?:(?P\d+)M)?(?:(?P\d+)S)?)?'. + ')'. + ')'. + '\z/'; + + $matches = null; + $ok = preg_match($pattern, $value, $matches); + if (!$ok) { + $this->raiseParseFailure( + self::PARSE_BAD_DURATION, + pht( + 'Expected DURATION in the format "P12DT3H4M5S", found '. + '"%s".', + $value)); + } + + $is_negative = (idx($matches, 'sign') == '-'); + + $duration = id(new PhutilCalendarDuration()) + ->setIsNegative($is_negative) + ->setWeeks((int)idx($matches, 'W', 0)) + ->setDays((int)idx($matches, 'D', 0)) + ->setHours((int)idx($matches, 'H', 0)) + ->setMinutes((int)idx($matches, 'M', 0)) + ->setSeconds((int)idx($matches, 'S', 0)); + + return $duration; + } + + private function getScalarParameterValue( + array $parameters, + $name, + $default = null) { + + $match = null; + foreach ($parameters as $parameter) { + if ($parameter['name'] == $name) { + $match = $parameter; + } + } + + if ($match === null) { + return $default; + } + + $value = $match['values']; + if (!$value) { + // Parameter is specified, but with no value, like "KEY=". Just return + // the default, as though the parameter was not specified. + return $default; + } + + if (count($value) > 1) { + $this->raiseParseFailure( + self::PARSE_MULTIPLE_PARAMETERS, + pht( + 'Expected parameter "%s" to have at most one value, but found '. + 'more than one.', + $name)); + } + + return idx(head($value), 'value'); + } + + } diff --git a/src/parser/calendar/ics/__tests__/PhutilICSParserTestCase.php b/src/parser/calendar/ics/__tests__/PhutilICSParserTestCase.php --- a/src/parser/calendar/ics/__tests__/PhutilICSParserTestCase.php +++ b/src/parser/calendar/ics/__tests__/PhutilICSParserTestCase.php @@ -3,16 +3,8 @@ final class PhutilICSParserTestCase extends PhutilTestCase { public function testICSParser() { - $root = $this->parseICSDocument('simple.ics'); + $event = $this->parseICSSingleEvent('simple.ics'); - $documents = $root->getDocuments(); - $this->assertEqual(1, count($documents)); - $document = head($documents); - - $events = $document->getEvents(); - $this->assertEqual(1, count($events)); - - $event = head($events); $this->assertEqual( array( array( @@ -38,6 +30,27 @@ ), ), array( + 'name' => 'DTSTART', + 'parameters' => array( + array( + 'name' => 'TZID', + 'values' => array( + array( + 'value' => 'America/Los_Angeles', + 'quoted' => false, + ), + ), + ), + ), + 'value' => array( + 'type' => 'DATE-TIME', + 'value' => array( + '20160915T090000', + ), + 'raw' => '20160915T090000', + ), + ), + array( 'name' => 'DTEND', 'parameters' => array( array( @@ -64,13 +77,119 @@ 'value' => array( 'type' => 'TEXT', 'value' => array( - 'Example Event', + 'Simple Event', + ), + 'raw' => 'Simple Event', + ), + ), + array( + 'name' => 'DESCRIPTION', + 'parameters' => array(), + 'value' => array( + 'type' => 'TEXT', + 'value' => array( + 'This is a simple event.', ), - 'raw' => 'Example Event', + 'raw' => 'This is a simple event.', ), ), ), $event->getAttribute('ics.properties')); + + $this->assertEqual( + 'Simple Event', + $event->getName()); + + $this->assertEqual( + 'This is a simple event.', + $event->getDescription()); + + $this->assertEqual( + 1473955200, + $event->getStartDateTime()->getEpoch()); + + $this->assertEqual( + 1473955200 + phutil_units('1 hour in seconds'), + $event->getEndDateTime()->getEpoch()); + } + + public function testICSFloatingTime() { + // This tests "floating" event times, which have no absolute time and are + // supposed to be interpreted using the viewer's timezone. It also uses + // a duration, and the duration needs to float along with the viewer + // timezone. + + $event = $this->parseICSSingleEvent('floating.ics'); + + $start = $event->getStartDateTime(); + + $caught = null; + try { + $start->getEpoch(); + } catch (Exception $ex) { + $caught = $ex; + } + + $this->assertTrue( + ($caught instanceof Exception), + pht('Expected exception for floating time with no viewer timezone.')); + + $newyears_utc = strtotime('2015-01-01 00:00:00 UTC'); + $this->assertEqual(1420070400, $newyears_utc); + + $start->setViewerTimezone('UTC'); + $this->assertEqual( + $newyears_utc, + $start->getEpoch()); + + $start->setViewerTimezone('America/Los_Angeles'); + $this->assertEqual( + $newyears_utc + phutil_units('8 hours in seconds'), + $start->getEpoch()); + + $start->setViewerTimezone('America/New_York'); + $this->assertEqual( + $newyears_utc + phutil_units('5 hours in seconds'), + $start->getEpoch()); + + $end = $event->getEndDateTime(); + $end->setViewerTimezone('UTC'); + $this->assertEqual( + $newyears_utc + phutil_units('24 hours in seconds'), + $end->getEpoch()); + + $end->setViewerTimezone('America/Los_Angeles'); + $this->assertEqual( + $newyears_utc + phutil_units('32 hours in seconds'), + $end->getEpoch()); + + $end->setViewerTimezone('America/New_York'); + $this->assertEqual( + $newyears_utc + phutil_units('29 hours in seconds'), + $end->getEpoch()); + } + + public function testICSDuration() { + $event = $this->parseICSSingleEvent('duration.ics'); + + // Raw value is "20160719T095722Z". + $start_epoch = strtotime('2016-07-19 09:57:22 UTC'); + $this->assertEqual(1468922242, $start_epoch); + + // Raw value is "P1DT17H4M23S". + $duration = + phutil_units('1 day in seconds') + + phutil_units('17 hours in seconds') + + phutil_units('4 minutes in seconds') + + phutil_units('23 seconds in seconds'); + + $this->assertEqual( + $start_epoch, + $event->getStartDateTime()->getEpoch()); + + $this->assertEqual( + $start_epoch + $duration, + $event->getEndDateTime()->getEpoch()); } public function testICSParserErrors() { @@ -94,6 +213,22 @@ PhutilICSParser::PARSE_UNESCAPED_BACKSLASH, 'err-unexpected-child.ics' => PhutilICSParser::PARSE_UNEXPECTED_CHILD, 'err-unexpected-text.ics' => PhutilICSParser::PARSE_UNEXPECTED_TEXT, + 'err-multiple-parameters.ics' => + PhutilICSParser::PARSE_MULTIPLE_PARAMETERS, + 'err-empty-datetime.ics' => + PhutilICSParser::PARSE_EMPTY_DATETIME, + 'err-many-datetime.ics' => + PhutilICSParser::PARSE_MANY_DATETIME, + 'err-bad-datetime.ics' => + PhutilICSParser::PARSE_BAD_DATETIME, + 'err-bad-tzid.ics' => + PhutilICSParser::PARSE_BAD_TZID, + 'err-empty-duration.ics' => + PhutilICSParser::PARSE_EMPTY_DURATION, + 'err-many-duration.ics' => + PhutilICSParser::PARSE_MANY_DURATION, + 'err-bad-duration.ics' => + PhutilICSParser::PARSE_BAD_DURATION, 'simple.ics' => null, 'good-boolean.ics' => null, @@ -135,6 +270,19 @@ } } + private function parseICSSingleEvent($name) { + $root = $this->parseICSDocument($name); + + $documents = $root->getDocuments(); + $this->assertEqual(1, count($documents)); + $document = head($documents); + + $events = $document->getEvents(); + $this->assertEqual(1, count($events)); + + return head($events); + } + private function parseICSDocument($name) { $path = dirname(__FILE__).'/data/'.$name; $data = Filesystem::readFile($path); diff --git a/src/parser/calendar/ics/__tests__/data/duration.ics b/src/parser/calendar/ics/__tests__/data/duration.ics new file mode 100644 --- /dev/null +++ b/src/parser/calendar/ics/__tests__/data/duration.ics @@ -0,0 +1,8 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTART:20160719T095722Z +DURATION:P1DT17H4M23S +SUMMARY:Duration Event +DESCRIPTION:This is an event with a complex duration. +END:VEVENT +END:VCALENDAR diff --git a/src/parser/calendar/ics/__tests__/data/err-bad-datetime.ics b/src/parser/calendar/ics/__tests__/data/err-bad-datetime.ics new file mode 100644 --- /dev/null +++ b/src/parser/calendar/ics/__tests__/data/err-bad-datetime.ics @@ -0,0 +1,5 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTART:quack +END:VEVENT +END:VCALENDAR diff --git a/src/parser/calendar/ics/__tests__/data/err-bad-duration.ics b/src/parser/calendar/ics/__tests__/data/err-bad-duration.ics new file mode 100644 --- /dev/null +++ b/src/parser/calendar/ics/__tests__/data/err-bad-duration.ics @@ -0,0 +1,5 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DURATION:quack +END:VEVENT +END:VCALENDAR diff --git a/src/parser/calendar/ics/__tests__/data/err-bad-tzid.ics b/src/parser/calendar/ics/__tests__/data/err-bad-tzid.ics new file mode 100644 --- /dev/null +++ b/src/parser/calendar/ics/__tests__/data/err-bad-tzid.ics @@ -0,0 +1,5 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTART;TZID=quack:20130101 +END:VEVENT +END:VCALENDAR diff --git a/src/parser/calendar/ics/__tests__/data/err-empty-datetime.ics b/src/parser/calendar/ics/__tests__/data/err-empty-datetime.ics new file mode 100644 --- /dev/null +++ b/src/parser/calendar/ics/__tests__/data/err-empty-datetime.ics @@ -0,0 +1,5 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTART: +END:VEVENT +END:VCALENDAR diff --git a/src/parser/calendar/ics/__tests__/data/err-empty-duration.ics b/src/parser/calendar/ics/__tests__/data/err-empty-duration.ics new file mode 100644 --- /dev/null +++ b/src/parser/calendar/ics/__tests__/data/err-empty-duration.ics @@ -0,0 +1,5 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DURATION: +END:VEVENT +END:VCALENDAR diff --git a/src/parser/calendar/ics/__tests__/data/err-many-datetime.ics b/src/parser/calendar/ics/__tests__/data/err-many-datetime.ics new file mode 100644 --- /dev/null +++ b/src/parser/calendar/ics/__tests__/data/err-many-datetime.ics @@ -0,0 +1,5 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTART:20130101,20130101 +END:VEVENT +END:VCALENDAR diff --git a/src/parser/calendar/ics/__tests__/data/err-many-duration.ics b/src/parser/calendar/ics/__tests__/data/err-many-duration.ics new file mode 100644 --- /dev/null +++ b/src/parser/calendar/ics/__tests__/data/err-many-duration.ics @@ -0,0 +1,5 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DURATION:P1W,P2W +END:VEVENT +END:VCALENDAR diff --git a/src/parser/calendar/ics/__tests__/data/err-multiple-parameters.ics b/src/parser/calendar/ics/__tests__/data/err-multiple-parameters.ics new file mode 100644 --- /dev/null +++ b/src/parser/calendar/ics/__tests__/data/err-multiple-parameters.ics @@ -0,0 +1,5 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTART;TZID=A,B:20160915T090000 +END:VEVENT +END:VCALENDAR diff --git a/src/parser/calendar/ics/__tests__/data/floating.ics b/src/parser/calendar/ics/__tests__/data/floating.ics new file mode 100644 --- /dev/null +++ b/src/parser/calendar/ics/__tests__/data/floating.ics @@ -0,0 +1,8 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTART:20150101T000000 +DURATION:P1D +SUMMARY:New Year's 2015 +DESCRIPTION:This is an event with a floating start time. +END:VEVENT +END:VCALENDAR diff --git a/src/parser/calendar/ics/__tests__/data/simple.ics b/src/parser/calendar/ics/__tests__/data/simple.ics --- a/src/parser/calendar/ics/__tests__/data/simple.ics +++ b/src/parser/calendar/ics/__tests__/data/simple.ics @@ -4,7 +4,9 @@ BEGIN:VEVENT CREATED:20160908T172702Z UID:1CEB57AF-0C9C-402D-B3BD-D75BD4843F68 +DTSTART;TZID=America/Los_Angeles:20160915T090000 DTEND;TZID=America/Los_Angeles:20160915T100000 -SUMMARY:Example Event +SUMMARY:Simple Event +DESCRIPTION:This is a simple event. END:VEVENT END:VCALENDAR