Page MenuHomePhabricator

D16648.id40079.diff
No OneTemporary

D16648.id40079.diff

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
@@ -15,6 +15,9 @@
private $modifiedDateTime;
private $organizer;
private $attendees = array();
+ private $recurrenceRule;
+ private $recurrenceExceptions = array();
+ private $recurrenceDates = array();
public function setUID($uid) {
$this->uid = $uid;
@@ -125,4 +128,52 @@
return $this;
}
+ public function setRecurrenceRule(
+ PhutilCalendarRecurrenceRule $recurrence_rule) {
+ $this->recurrenceRule = $recurrence_rule;
+ return $this;
+ }
+
+ public function getRecurrenceRule() {
+ return $this->recurrenceRule;
+ }
+
+ public function setRecurrenceUntilDateTime(PhutilCalendarDateTime $date) {
+ $this->recurrenceUntilDateTime = $date;
+ return $this;
+ }
+
+ public function getRecurrenceUntilDateTime() {
+ return $this->recurrenceUntilDateTime;
+ }
+
+ public function setRecurrenceCount($recurrence_count) {
+ $this->recurrenceCount = $recurrence_count;
+ return $this;
+ }
+
+ public function getRecurrenceCount() {
+ return $this->recurrenceCount;
+ }
+
+ public function setRecurrenceExceptions(array $recurrence_exceptions) {
+ assert_instances_of($recurrence_exceptions, 'PhutilCalendarDateTime');
+ $this->recurrenceExceptions = $recurrence_exceptions;
+ return $this;
+ }
+
+ public function getRecurrenceExceptions() {
+ return $this->recurrenceExceptions;
+ }
+
+ public function setRecurrenceDates(array $recurrence_dates) {
+ assert_instances_of($recurrence_dates, 'PhutilCalendarDateTime');
+ $this->recurrenceDates = $recurrence_dates;
+ return $this;
+ }
+
+ public function getRecurrenceDates() {
+ return $this->recurrenceDates;
+ }
+
}
diff --git a/src/parser/calendar/data/PhutilCalendarRecurrenceRule.php b/src/parser/calendar/data/PhutilCalendarRecurrenceRule.php
--- a/src/parser/calendar/data/PhutilCalendarRecurrenceRule.php
+++ b/src/parser/calendar/data/PhutilCalendarRecurrenceRule.php
@@ -17,6 +17,8 @@
private $byMonth = array();
private $bySetPosition = array();
private $weekStart = self::WEEKDAY_MONDAY;
+ private $count;
+ private $until;
private $cursorSecond;
private $cursorMinute;
@@ -84,6 +86,175 @@
const WEEKINDEX_FRIDAY = 5;
const WEEKINDEX_SATURDAY = 6;
+ public function toDictionary() {
+ $parts = array();
+
+ $parts['FREQ'] = $this->getFrequency();
+
+ $interval = $this->getInterval();
+ if ($interval != 1) {
+ $parts['INTERVAL'] = $interval;
+ }
+
+ $by_second = $this->getBySecond();
+ if ($by_second) {
+ $parts['BYSECOND'] = $by_second;
+ }
+
+ $by_minute = $this->getByMinute();
+ if ($by_minute) {
+ $parts['BYMINUTE'] = $by_minute;
+ }
+
+ $by_hour = $this->getByHour();
+ if ($by_hour) {
+ $parts['BYHOUR'] = $by_hour;
+ }
+
+ $by_day = $this->getByDay();
+ if ($by_day) {
+ $parts['BYDAY'] = $by_day;
+ }
+
+ $by_month = $this->getByMonth();
+ if ($by_month) {
+ $parts['BYMONTH'] = $by_month;
+ }
+
+ $by_monthday = $this->getByMonthDay();
+ if ($by_monthday) {
+ $parts['BYMONTHDAY'] = $by_monthday;
+ }
+
+ $by_yearday = $this->getByYearDay();
+ if ($by_yearday) {
+ $parts['BYYEARDAY'] = $by_yearday;
+ }
+
+ $by_weekno = $this->getByWeekNumber();
+ if ($by_weekno) {
+ $parts['BYWEEKNO'] = $by_weekno;
+ }
+
+ $by_setpos = $this->getBySetPosition();
+ if ($by_setpos) {
+ $parts['BYSETPOS'] = $by_setpos;
+ }
+
+ $wkst = $this->getWeekStart();
+ if ($wkst != self::WEEKDAY_MONDAY) {
+ $parts['WKST'] = $wkst;
+ }
+
+ $count = $this->getCount();
+ if ($count) {
+ $parts['COUNT'] = $count;
+ }
+
+ $until = $this->getUntil();
+ if ($until) {
+ $parts['UNTIL'] = $until->getISO8601();
+ }
+
+ return $parts;
+ }
+
+ public static function newFromDictionary(array $dict) {
+ static $expect;
+ if ($expect === null) {
+ $expect = array_fuse(
+ array(
+ 'FREQ',
+ 'INTERVAL',
+ 'BYSECOND',
+ 'BYMINUTE',
+ 'BYHOUR',
+ 'BYDAY',
+ 'BYMONTHDAY',
+ 'BYYEARDAY',
+ 'BYWEEKNO',
+ 'BYSETPOS',
+ 'WKST',
+ 'UNTIL',
+ 'COUNT',
+ ));
+ }
+
+ foreach ($dict as $key => $value) {
+ if (empty($expect[$key])) {
+ throw new Exception(
+ pht(
+ 'RRULE dictionary includes unknown key "%s". Expected keys '.
+ 'are: %s.',
+ $key,
+ implode(', ', array_keys($expect))));
+ }
+ }
+
+ $rrule = id(new self())
+ ->setFrequency(idx($dict, 'FREQ'))
+ ->setInterval(idx($dict, 'INTERVAL', 1))
+ ->setBySecond(idx($dict, 'BYSECOND', array()))
+ ->setByMinute(idx($dict, 'BYMINUTE', array()))
+ ->setByHour(idx($dict, 'BYHOUR', array()))
+ ->setByDay(idx($dict, 'BYDAY', array()))
+ ->setByMonthDay(idx($dict, 'BYMONTHDAY', array()))
+ ->setByYearDay(idx($dict, 'BYYEARDAY', array()))
+ ->setByWeekNumber(idx($dict, 'BYWEEKNO', array()))
+ ->setBySetPosition(idx($dict, 'BYSETPOS', array()))
+ ->setWeekStart(idx($dict, 'WKST', self::WEEKDAY_MONDAY));
+
+ $count = idx($dict, 'COUNT');
+ if ($count) {
+ $rrule->setCount($count);
+ }
+
+ $until = idx($dict, 'UNTIL');
+ if ($until) {
+ $until = PhutilCalendarAbsoluteDateTime::newFromISO8601($until);
+ $rrule->setUntil($until);
+ }
+
+ return $rrule;
+ }
+
+ public function toRRULE() {
+ $dict = $this->toDictionary();
+
+ $parts = array();
+ foreach ($dict as $key => $value) {
+ if (is_array($value)) {
+ $value = implode(',', $value);
+ }
+ $parts[] = "{$key}={$value}";
+ }
+
+ return implode(';', $parts);
+ }
+
+ public static function newFromRRULE($rrule) {
+ $parts = explode(';', $rrule);
+
+ $dict = array();
+ foreach ($parts as $part) {
+ list($key, $value) = explode('=', $part, 2);
+ switch ($key) {
+ case 'FREQ':
+ case 'INTERVAL':
+ case 'WKST':
+ case 'COUNT':
+ case 'UNTIL';
+ break;
+ default:
+ $value = explode(',', $value);
+ break;
+ }
+ $dict[$key] = $value;
+ }
+
+ return self::newFromDictionary($dict);
+ }
+
private static function getAllWeekdayConstants() {
return array_keys(self::getWeekdayIndexMap());
}
@@ -126,6 +297,31 @@
return $this->startDateTime;
}
+ public function setCount($count) {
+ if ($count < 1) {
+ throw new Exception(
+ pht(
+ 'RRULE COUNT value "%s" is invalid: count must be at least 1.',
+ $count));
+ }
+
+ $this->count = $count;
+ return $this;
+ }
+
+ public function getCount() {
+ return $this->count;
+ }
+
+ public function setUntil(PhutilCalendarDateTime $until) {
+ $this->until = $until;
+ return $this;
+ }
+
+ public function getUntil() {
+ return $this->until;
+ }
+
public function setFrequency($frequency) {
static $map = array(
self::FREQUENCY_SECONDLY => self::SCALE_SECONDLY,
diff --git a/src/parser/calendar/ics/PhutilICSWriter.php b/src/parser/calendar/ics/PhutilICSWriter.php
--- a/src/parser/calendar/ics/PhutilICSWriter.php
+++ b/src/parser/calendar/ics/PhutilICSWriter.php
@@ -209,6 +209,27 @@
}
}
+ $rrule = $event->getRecurrenceRule();
+ if ($rrule) {
+ $properties[] = $this->newRRULEProperty(
+ 'RRULE',
+ $rrule);
+ }
+
+ $exdates = $event->getRecurrenceExceptions();
+ if ($exdates) {
+ $properties[] = $this->newDateTimesProperty(
+ 'EXDATE',
+ $exdates);
+ }
+
+ $rdates = $event->getRecurrenceDates();
+ if ($rdates) {
+ $properties[] = $this->newDateTimesProperty(
+ 'RDATE',
+ $rdates);
+ }
+
return $properties;
}
@@ -238,9 +259,17 @@
$name,
PhutilCalendarDateTime $value,
array $parameters = array()) {
- $datetime = $value->getISO8601();
- if ($value->getIsAllDay()) {
+ return $this->newDateTimesProperty($name, array($value), $parameters);
+ }
+
+ private function newDateTimesProperty(
+ $name,
+ array $values,
+ array $parameters = array()) {
+ assert_instances_of($values, 'PhutilCalendarDateTime');
+
+ if (head($values)->getIsAllDay()) {
$parameters[] = array(
'name' => 'VALUE',
'values' => array(
@@ -249,7 +278,13 @@
);
}
- return $this->newProperty($name, $datetime, $parameters);
+ $datetimes = array();
+ foreach ($values as $value) {
+ $datetimes[] = $value->getISO8601();
+ }
+ $datetimes = implode(';', $datetimes);
+
+ return $this->newProperty($name, $datetimes, $parameters);
}
private function newUserProperty(
@@ -292,6 +327,15 @@
return $this->newProperty($name, $value->getURI(), $parameters);
}
+ private function newRRULEProperty(
+ $name,
+ PhutilCalendarRecurrenceRule $rule,
+ array $parameters = array()) {
+
+ $value = $rule->toRRULE();
+ return $this->newProperty($name, $value, $parameters);
+ }
+
private function newProperty(
$name,
$value,
diff --git a/src/parser/calendar/ics/__tests__/PhutilICSWriterTestCase.php b/src/parser/calendar/ics/__tests__/PhutilICSWriterTestCase.php
--- a/src/parser/calendar/ics/__tests__/PhutilICSWriterTestCase.php
+++ b/src/parser/calendar/ics/__tests__/PhutilICSWriterTestCase.php
@@ -37,6 +37,39 @@
$this->assertICS('writer-tea-time.ics', $ics_data);
}
+ public function testICSWriterChristmas() {
+ $start = PhutilCalendarAbsoluteDateTime::newFromISO8601('20001225T000000Z');
+ $end = PhutilCalendarAbsoluteDateTime::newFromISO8601('20001226T000000Z');
+
+ $rrule = id(new PhutilCalendarRecurrenceRule())
+ ->setFrequency(PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY)
+ ->setByMonth(array(12))
+ ->setByMonthDay(array(25));
+
+ $event = id(new PhutilCalendarEventNode())
+ ->setUID('recurring-christmas')
+ ->setName('Christmas')
+ ->setDescription('Festival holiday first occurring in the year 2000.')
+ ->setStartDateTime($start)
+ ->setEndDateTime($end)
+ ->setCreatedDateTime($start)
+ ->setModifiedDateTime($start)
+ ->setRecurrenceRule($rrule)
+ ->setRecurrenceExceptions(
+ array(
+ // In 2007, Christmas was cancelled.
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20071225T000000Z'),
+ ))
+ ->setRecurrenceDates(
+ array(
+ // We had an extra early Christmas in 2009.
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20091125T000000Z'),
+ ));
+
+ $ics_data = $this->writeICSSingleEvent($event);
+ $this->assertICS('writer-recurring-christmas.ics', $ics_data);
+ }
+
public function testICSWriterAllDay() {
$event = id(new PhutilCalendarEventNode())
->setUID('christmas-day')
diff --git a/src/parser/calendar/ics/__tests__/data/writer-recurring-christmas.ics b/src/parser/calendar/ics/__tests__/data/writer-recurring-christmas.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/writer-recurring-christmas.ics
@@ -0,0 +1,16 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Phacility//Phabricator//EN
+BEGIN:VEVENT
+UID:recurring-christmas
+CREATED:20001225T000000Z
+DTSTAMP:20001225T000000Z
+DTSTART:20001225T000000Z
+DTEND:20001226T000000Z
+SUMMARY:Christmas
+DESCRIPTION:Festival holiday first occurring in the year 2000.
+RRULE:FREQ=YEARLY;BYMONTH=12;BYMONTHDAY=25
+EXDATE:20071225T000000Z
+RDATE:20091125T000000Z
+END:VEVENT
+END:VCALENDAR

File Metadata

Mime Type
text/plain
Expires
Mon, Dec 23, 12:34 AM (12 h, 21 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6919960
Default Alt Text
D16648.id40079.diff (11 KB)

Event Timeline