Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F15349387
D16590.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
17 KB
Referenced Files
None
Subscribers
None
D16590.diff
View Options
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
@@ -141,6 +141,11 @@
'PhutilCalendarNode' => 'parser/calendar/data/PhutilCalendarNode.php',
'PhutilCalendarProxyDateTime' => 'parser/calendar/data/PhutilCalendarProxyDateTime.php',
'PhutilCalendarRawNode' => 'parser/calendar/data/PhutilCalendarRawNode.php',
+ 'PhutilCalendarRecurrenceList' => 'parser/calendar/data/PhutilCalendarRecurrenceList.php',
+ 'PhutilCalendarRecurrenceRule' => 'parser/calendar/data/PhutilCalendarRecurrenceRule.php',
+ 'PhutilCalendarRecurrenceSet' => 'parser/calendar/data/PhutilCalendarRecurrenceSet.php',
+ 'PhutilCalendarRecurrenceSource' => 'parser/calendar/data/PhutilCalendarRecurrenceSource.php',
+ 'PhutilCalendarRecurrenceTestCase' => 'parser/calendar/data/__tests__/PhutilCalendarRecurrenceTestCase.php',
'PhutilCalendarRelativeDateTime' => 'parser/calendar/data/PhutilCalendarRelativeDateTime.php',
'PhutilCalendarRootNode' => 'parser/calendar/data/PhutilCalendarRootNode.php',
'PhutilCalendarUserNode' => 'parser/calendar/data/PhutilCalendarUserNode.php',
@@ -725,6 +730,11 @@
'PhutilCalendarNode' => 'Phobject',
'PhutilCalendarProxyDateTime' => 'PhutilCalendarDateTime',
'PhutilCalendarRawNode' => 'PhutilCalendarContainerNode',
+ 'PhutilCalendarRecurrenceList' => 'PhutilCalendarRecurrenceSource',
+ 'PhutilCalendarRecurrenceRule' => 'PhutilCalendarRecurrenceSource',
+ 'PhutilCalendarRecurrenceSet' => 'Phobject',
+ 'PhutilCalendarRecurrenceSource' => 'Phobject',
+ 'PhutilCalendarRecurrenceTestCase' => 'PhutilTestCase',
'PhutilCalendarRelativeDateTime' => 'PhutilCalendarProxyDateTime',
'PhutilCalendarRootNode' => 'PhutilCalendarContainerNode',
'PhutilCalendarUserNode' => 'PhutilCalendarNode',
diff --git a/src/parser/calendar/data/PhutilCalendarRecurrenceList.php b/src/parser/calendar/data/PhutilCalendarRecurrenceList.php
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/data/PhutilCalendarRecurrenceList.php
@@ -0,0 +1,43 @@
+<?php
+
+final class PhutilCalendarRecurrenceList
+ extends PhutilCalendarRecurrenceSource {
+
+ private $dates = array();
+ private $order;
+
+ public function setDates(array $dates) {
+ assert_instances_of($dates, 'PhutilCalendarDateTime');
+ $this->dates = $dates;
+ return $this;
+ }
+
+ public function getDates() {
+ return $this->dates;
+ }
+
+ public function resetSource() {
+ foreach ($this->getDates() as $date) {
+ $date->setViewerTimezone($this->getViewerTimezone());
+ }
+
+ $order = msort($this->getDates(), 'getEpoch');
+ $order = array_reverse($order);
+ $this->order = $order;
+
+ return $this;
+ }
+
+ public function getNextEvent($cursor) {
+ while ($this->order) {
+ $next = array_pop($this->order);
+ if ($next->getEpoch() >= $cursor) {
+ return $next;
+ }
+ }
+
+ return null;
+ }
+
+
+}
diff --git a/src/parser/calendar/data/PhutilCalendarRecurrenceRule.php b/src/parser/calendar/data/PhutilCalendarRecurrenceRule.php
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/data/PhutilCalendarRecurrenceRule.php
@@ -0,0 +1,142 @@
+<?php
+
+final class PhutilCalendarRecurrenceRule
+ extends PhutilCalendarRecurrenceSource {
+
+ private $frequency;
+ private $until;
+ private $count;
+ private $interval;
+ private $bySecond;
+ private $byMinute;
+ private $byHour;
+ private $byDay;
+ private $byMonthDay;
+ private $byYearDay;
+ private $byWeekNumber;
+ private $byMonth;
+ private $bySetPosition;
+ private $weekStart = 'MO';
+
+ public function setFrequency($frequency) {
+ $this->frequency = $frequency;
+ return $this;
+ }
+
+ public function getFrequency() {
+ return $this->frequency;
+ }
+
+ public function setUntil(PhutilCalendarDateTime $until) {
+ $this->until = $until;
+ return $this;
+ }
+
+ public function getUntil() {
+ return $this->until;
+ }
+
+ public function setCount($count) {
+ $this->count = $count;
+ return $this;
+ }
+
+ public function getCount() {
+ return $this->count;
+ }
+
+ public function setInterval($interval) {
+ $this->interval = $interval;
+ return $this;
+ }
+
+ public function getInterval() {
+ return $this->interval;
+ }
+
+ public function setBySecond($by_second) {
+ $this->bySecond = $by_second;
+ return $this;
+ }
+
+ public function getBySecond() {
+ return $this->bySecond;
+ }
+
+ public function setByMinute($by_minute) {
+ $this->byMinute = $by_minute;
+ return $this;
+ }
+
+ public function getByMinute() {
+ return $this->byMinute;
+ }
+
+ public function setByHour($by_hour) {
+ $this->byHour = $by_hour;
+ return $this;
+ }
+
+ public function getByHour() {
+ return $this->byHour;
+ }
+
+ public function setByDay($by_day) {
+ $this->byDay = $by_day;
+ return $this;
+ }
+
+ public function getByDay() {
+ return $this->byDay;
+ }
+
+ public function setByMonthDay($by_month_day) {
+ $this->byMonthDay = $by_month_day;
+ return $this;
+ }
+
+ public function getByMonthDay() {
+ return $this->byMonthDay;
+ }
+
+ public function setByYearDay($by_year_day) {
+ $this->byYearDay = $by_year_day;
+ return $this;
+ }
+
+ public function getByYearDay() {
+ return $this->byYearDay;
+ }
+
+ public function setByMonth($by_month) {
+ $this->byMonth = $by_month;
+ return $this;
+ }
+
+ public function getByMonth() {
+ return $this->byMonth;
+ }
+
+ public function setBySetPosition($by_set_position) {
+ $this->bySetPosition = $by_set_position;
+ return $this;
+ }
+
+ public function getBySetPosition() {
+ return $this->bySetPosition;
+ }
+
+ public function setWeekStart($week_start) {
+ $this->weekStart = $week_start;
+ return $this;
+ }
+
+ public function getWeekStart() {
+ return $this->weekStart;
+ }
+
+ public function getNextEvent($cursor) {
+ throw new PhutilMethodNotImplementedException();
+ }
+
+}
diff --git a/src/parser/calendar/data/PhutilCalendarRecurrenceSet.php b/src/parser/calendar/data/PhutilCalendarRecurrenceSet.php
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/data/PhutilCalendarRecurrenceSet.php
@@ -0,0 +1,152 @@
+<?php
+
+final class PhutilCalendarRecurrenceSet
+ extends Phobject {
+
+ private $sources = array();
+ private $viewerTimezone = 'UTC';
+
+ public function addSource(PhutilCalendarRecurrenceSource $source) {
+ $this->sources[] = $source;
+ return $this;
+ }
+
+ public function setViewerTimezone($viewer_timezone) {
+ $this->viewerTimezone = $viewer_timezone;
+ return $this;
+ }
+
+ public function getViewerTimezone() {
+ return $this->viewerTimezone;
+ }
+
+ public function getEventsBetween(
+ PhutilCalendarDateTime $start = null,
+ PhutilCalendarDateTime $end = null,
+ $limit = null) {
+
+ if ($end === null && $limit === null) {
+ throw new Exception(
+ pht(
+ 'Recurring event range queries must have an end date, a limit, or '.
+ 'both.'));
+ }
+
+ $timezone = $this->getViewerTimezone();
+
+ $sources = array();
+ foreach ($this->sources as $source) {
+ $source = clone $source;
+ $source->setViewerTimezone($timezone);
+ $source->resetSource();
+
+ $sources[] = array(
+ 'source' => $source,
+ 'state' => null,
+ 'epoch' => null,
+ );
+ }
+
+ if ($start) {
+ $start = clone $start;
+ $start->setViewerTimezone($timezone);
+ $cursor = $start->getEpoch();
+ } else {
+ $cursor = 0;
+ }
+
+ if ($end) {
+ $end = clone $end;
+ $end->setViewerTimezone($timezone);
+ $end_epoch = $end->getEpoch();
+ } else {
+ $end_epoch = null;
+ }
+
+ $results = array();
+ while (true) {
+ // Get the next event for each source which we don't have a future
+ // event for.
+ foreach ($sources as $key => $source) {
+ $state = $source['state'];
+ $epoch = $source['epoch'];
+
+ if ($state !== null && $epoch >= $cursor) {
+ // We have an event for this source, and it's a future event, so
+ // we don't need to do anything.
+ continue;
+ }
+
+ $next = $source['source']->getNextEvent($cursor);
+ if ($next === null) {
+ // This source doesn't have any more events, so we're all done.
+ unset($sources[$key]);
+ continue;
+ }
+
+ $next_epoch = $next->getEpoch();
+
+ if ($end_epoch !== null && $next_epoch > $end_epoch) {
+ // We have an end time and the next event from this source is
+ // past that end, so we know there are no more relevant events
+ // coming from this source.
+ unset($sources[$key]);
+ continue;
+ }
+
+ $sources[$key]['state'] = $next;
+ $sources[$key]['epoch'] = $next_epoch;
+ }
+
+ if (!$sources) {
+ // We've run out of sources which can produce valid events in the
+ // window, so we're all done.
+ break;
+ }
+
+ // Find the minimum event time across all sources.
+ $next_epoch = null;
+ foreach ($sources as $source) {
+ if ($next_epoch === null) {
+ $next_epoch = $source['epoch'];
+ } else {
+ $next_epoch = min($next_epoch, $source['epoch']);
+ }
+ }
+
+ $is_exception = false;
+ $next_source = null;
+ foreach ($sources as $source) {
+ if ($source['epoch'] == $next_epoch) {
+ if ($source['source']->getIsExceptionSource()) {
+ $is_exception = true;
+ } else {
+ $next_source = $source;
+ }
+ }
+ }
+
+ // If this is an exception, it means the event does NOT occur. We
+ // skip it and move on. If it's not an exception, it does occur, so
+ // we record it.
+ if (!$is_exception) {
+ $results[] = $next_source['state'];
+ if ($limit !== null && (count($results) >= $limit)) {
+ break;
+ }
+ }
+
+ $cursor = $next_epoch + 1;
+
+ // If we have an end of the window and we've reached it, we're done.
+ if ($end_epoch) {
+ if ($cursor > $end_epoch) {
+ break;
+ }
+ }
+ }
+
+ return $results;
+ }
+
+}
diff --git a/src/parser/calendar/data/PhutilCalendarRecurrenceSource.php b/src/parser/calendar/data/PhutilCalendarRecurrenceSource.php
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/data/PhutilCalendarRecurrenceSource.php
@@ -0,0 +1,34 @@
+<?php
+
+abstract class PhutilCalendarRecurrenceSource
+ extends Phobject {
+
+ private $isExceptionSource;
+ private $viewerTimezone;
+
+ public function setIsExceptionSource($is_exception_source) {
+ $this->isExceptionSource = $is_exception_source;
+ return $this;
+ }
+
+ public function getIsExceptionSource() {
+ return $this->isExceptionSource;
+ }
+
+ public function setViewerTimezone($viewer_timezone) {
+ $this->viewerTimezone = $viewer_timezone;
+ return $this;
+ }
+
+ public function getViewerTimezone() {
+ return $this->viewerTimezone;
+ }
+
+ public function resetSource() {
+ return;
+ }
+
+ abstract public function getNextEvent($cursor);
+
+
+}
diff --git a/src/parser/calendar/data/__tests__/PhutilCalendarRecurrenceTestCase.php b/src/parser/calendar/data/__tests__/PhutilCalendarRecurrenceTestCase.php
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/data/__tests__/PhutilCalendarRecurrenceTestCase.php
@@ -0,0 +1,169 @@
+<?php
+
+final class PhutilCalendarRecurrenceTestCase extends PhutilTestCase {
+
+ public function testCalendarRecurrenceLists() {
+ $set = id(new PhutilCalendarRecurrenceSet());
+ $result = $set->getEventsBetween(null, null, 0xFFFF);
+ $this->assertEqual(
+ array(),
+ $result,
+ pht('Set with no sources.'));
+
+
+ $set = id(new PhutilCalendarRecurrenceSet())
+ ->addSource(new PhutilCalendarRecurrenceList());
+ $result = $set->getEventsBetween(null, null, 0xFFFF);
+ $this->assertEqual(
+ array(),
+ $result,
+ pht('Set with empty list source.'));
+
+
+ $list = array(
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'),
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T120000Z'),
+ );
+
+ $source = id(new PhutilCalendarRecurrenceList())
+ ->setDates($list);
+
+ $set = id(new PhutilCalendarRecurrenceSet())
+ ->addSource($source);
+
+ $expect = array(
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T120000Z'),
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'),
+ );
+
+ $result = $set->getEventsBetween(null, null, 0xFFFF);
+ $this->assertEqual(
+ mpull($expect, 'getISO8601'),
+ mpull($result, 'getISO8601'),
+ pht('Simple date list.'));
+
+ $list_a = array(
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'),
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
+ );
+
+ $list_b = array(
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T120000Z'),
+ );
+
+ $source_a = id(new PhutilCalendarRecurrenceList())
+ ->setDates($list_a);
+
+ $source_b = id(new PhutilCalendarRecurrenceList())
+ ->setDates($list_b);
+
+ $set = id(new PhutilCalendarRecurrenceSet())
+ ->addSource($source_a)
+ ->addSource($source_b);
+
+ $expect = array(
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T120000Z'),
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'),
+ );
+
+ $result = $set->getEventsBetween(null, null, 0xFFFF);
+ $this->assertEqual(
+ mpull($expect, 'getISO8601'),
+ mpull($result, 'getISO8601'),
+ pht('Multiple date lists.'));
+
+ $list_a = array(
+ // This is Jan 1, 3, 5, 7, 8 and 10, but listed out-of-order.
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160105T120000Z'),
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160108T120000Z'),
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'),
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160110T120000Z'),
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160107T120000Z'),
+ );
+
+ $list_b = array(
+ // This is Jan 2, 4, 5, 8, but listed out of order.
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T120000Z'),
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160104T120000Z'),
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160105T120000Z'),
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160108T120000Z'),
+ );
+
+ $list_c = array(
+ // We're going to use this as an exception list.
+
+ // This is Jan 7 (listed in one other source), 8 (listed in two)
+ // and 9 (listed in none).
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160107T120000Z'),
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160108T120000Z'),
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160109T120000Z'),
+ );
+
+ $expect = array(
+ // From source A.
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
+ // From source B.
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T120000Z'),
+ // From source A.
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'),
+ // From source B.
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160104T120000Z'),
+ // From source A and B. Should appear only once.
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160105T120000Z'),
+ // The 6th appears in no source.
+ // The 7th, 8th and 9th are excluded.
+ // The 10th is from source A.
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160110T120000Z'),
+ );
+
+ $list_a = id(new PhutilCalendarRecurrenceList())
+ ->setDates($list_a);
+
+ $list_b = id(new PhutilCalendarRecurrenceList())
+ ->setDates($list_b);
+
+ $list_c = id(new PhutilCalendarRecurrenceList())
+ ->setDates($list_c)
+ ->setIsExceptionSource(true);
+
+ $date_set = id(new PhutilCalendarRecurrenceSet())
+ ->addSource($list_b)
+ ->addSource($list_c)
+ ->addSource($list_a);
+
+ $date_set->setViewerTimezone('UTC');
+
+ $result = $date_set->getEventsBetween(null, null, 0xFFFF);
+ $this->assertEqual(
+ mpull($expect, 'getISO8601'),
+ mpull($result, 'getISO8601'),
+ pht('Set of all results in multiple lists with exclusions.'));
+
+
+ $expect = array(
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
+ );
+ $result = $date_set->getEventsBetween(null, null, 1);
+ $this->assertEqual(
+ mpull($expect, 'getISO8601'),
+ mpull($result, 'getISO8601'),
+ pht('Multiple lists, one result.'));
+
+ $expect = array(
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'),
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160104T120000Z'),
+ );
+ $result = $date_set->getEventsBetween(
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'),
+ PhutilCalendarAbsoluteDateTime::newFromISO8601('20160104T120000Z'));
+ $this->assertEqual(
+ mpull($expect, 'getISO8601'),
+ mpull($result, 'getISO8601'),
+ pht('Multiple lists, time window.'));
+ }
+
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Tue, Mar 11, 2:56 PM (1 w, 4 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7531753
Default Alt Text
D16590.diff (17 KB)
Attached To
Mode
D16590: Add initial support for complex recurring events
Attached
Detach File
Event Timeline
Log In to Comment