@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class PhutilCalendarRecurrenceSet
4 extends Phobject {
5
6 private $sources = array();
7 private $viewerTimezone = 'UTC';
8
9 public function addSource(PhutilCalendarRecurrenceSource $source) {
10 $this->sources[] = $source;
11 return $this;
12 }
13
14 public function setViewerTimezone($viewer_timezone) {
15 $this->viewerTimezone = $viewer_timezone;
16 return $this;
17 }
18
19 public function getViewerTimezone() {
20 return $this->viewerTimezone;
21 }
22
23 public function getEventsBetween(
24 ?PhutilCalendarDateTime $start = null,
25 ?PhutilCalendarDateTime $end = null,
26 $limit = null) {
27
28 if ($end === null && $limit === null) {
29 throw new Exception(
30 pht(
31 'Recurring event range queries must have an end date, a limit, or '.
32 'both.'));
33 }
34
35 $timezone = $this->getViewerTimezone();
36
37 $sources = array();
38 foreach ($this->sources as $source) {
39 $source = clone $source;
40 $source->setViewerTimezone($timezone);
41 $source->resetSource();
42
43 $sources[] = array(
44 'source' => $source,
45 'state' => null,
46 'epoch' => null,
47 );
48 }
49
50 if ($start) {
51 $start = clone $start;
52 $start->setViewerTimezone($timezone);
53 $min_epoch = $start->getEpoch();
54 } else {
55 $min_epoch = 0;
56 }
57
58 if ($end) {
59 $end = clone $end;
60 $end->setViewerTimezone($timezone);
61 $end_epoch = $end->getEpoch();
62 } else {
63 $end_epoch = null;
64 }
65
66 $results = array();
67 $index = 0;
68 $cursor = 0;
69 while (true) {
70 // Get the next event for each source which we don't have a future
71 // event for.
72 foreach ($sources as $key => $source) {
73 $state = $source['state'];
74 $epoch = $source['epoch'];
75
76 if ($state !== null && $epoch >= $cursor) {
77 // We have an event for this source, and it's a future event, so
78 // we don't need to do anything.
79 continue;
80 }
81
82 $next = $source['source']->getNextEvent($cursor);
83 if ($next === null) {
84 // This source doesn't have any more events, so we're all done.
85 unset($sources[$key]);
86 continue;
87 }
88
89 $next_epoch = $next->getEpoch();
90
91 if ($end_epoch !== null && $next_epoch > $end_epoch) {
92 // We have an end time and the next event from this source is
93 // past that end, so we know there are no more relevant events
94 // coming from this source.
95 unset($sources[$key]);
96 continue;
97 }
98
99 $sources[$key]['state'] = $next;
100 $sources[$key]['epoch'] = $next_epoch;
101 }
102
103 if (!$sources) {
104 // We've run out of sources which can produce valid events in the
105 // window, so we're all done.
106 break;
107 }
108
109 // Find the minimum event time across all sources.
110 $next_epoch = null;
111 foreach ($sources as $source) {
112 if ($next_epoch === null) {
113 $next_epoch = $source['epoch'];
114 } else {
115 $next_epoch = min($next_epoch, $source['epoch']);
116 }
117 }
118
119 $is_exception = false;
120 $next_source = null;
121 foreach ($sources as $source) {
122 if ($source['epoch'] == $next_epoch) {
123 if ($source['source']->getIsExceptionSource()) {
124 $is_exception = true;
125 } else {
126 $next_source = $source;
127 }
128 }
129 }
130
131 // If this is an exception, it means the event does NOT occur. We
132 // skip it and move on. If it's not an exception, it does occur, so
133 // we record it.
134 if (!$is_exception) {
135
136 // Only actually include this event in the results if it starts after
137 // any specified start time. We increment the index regardless, so we
138 // return results with proper offsets.
139 if ($next_source['epoch'] >= $min_epoch) {
140 $results[$index] = $next_source['state'];
141 }
142 $index++;
143
144 if ($limit !== null && (count($results) >= $limit)) {
145 break;
146 }
147 }
148
149 $cursor = $next_epoch + 1;
150
151 // If we have an end of the window and we've reached it, we're done.
152 if ($end_epoch) {
153 if ($cursor > $end_epoch) {
154 break;
155 }
156 }
157 }
158
159 return $results;
160 }
161
162}