Page MenuHomePhabricator

D16590.diff
No OneTemporary

D16590.diff

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

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)

Event Timeline