diff --git a/src/parser/calendar/data/PhutilCalendarRecurrenceSet.php b/src/parser/calendar/data/PhutilCalendarRecurrenceSet.php index a44fa55..6ebcd4a 100644 --- a/src/parser/calendar/data/PhutilCalendarRecurrenceSet.php +++ b/src/parser/calendar/data/PhutilCalendarRecurrenceSet.php @@ -1,152 +1,162 @@ 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(); + $min_epoch = $start->getEpoch(); } else { - $cursor = 0; + $min_epoch = 0; } if ($end) { $end = clone $end; $end->setViewerTimezone($timezone); $end_epoch = $end->getEpoch(); } else { $end_epoch = null; } $results = array(); + $index = 0; + $cursor = 0; 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']; + + // Only actually include this event in the results if it starts after + // any specified start time. We increment the index regardless, so we + // return results with proper offsets. + if ($next_source['epoch'] >= $min_epoch) { + $results[$index] = $next_source['state']; + } + $index++; + 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/__tests__/PhutilCalendarRecurrenceTestCase.php b/src/parser/calendar/data/__tests__/PhutilCalendarRecurrenceTestCase.php index 18e76fd..989975e 100644 --- a/src/parser/calendar/data/__tests__/PhutilCalendarRecurrenceTestCase.php +++ b/src/parser/calendar/data/__tests__/PhutilCalendarRecurrenceTestCase.php @@ -1,169 +1,196 @@ 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'), + 2 => PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'), + 3 => 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.')); } + public function testCalendarRecurrenceOffsets() { + $list = array( + PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'), + PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'), + PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T120000Z'), + ); + + $source = id(new PhutilCalendarRecurrenceList()) + ->setDates($list); + + $set = id(new PhutilCalendarRecurrenceSet()) + ->addSource($source); + + $t1 = PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T120001Z'); + $t2 = PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'); + + $expect = array( + 2 => $t2, + ); + + $result = $set->getEventsBetween($t1, null, 0xFFFF); + $this->assertEqual( + mpull($expect, 'getISO8601'), + mpull($result, 'getISO8601'), + pht('Correct event indexes with start date.')); + } + }