-
Notifications
You must be signed in to change notification settings - Fork 247
/
Copy pathAvailabilityGenerator.php
181 lines (161 loc) Β· 5.56 KB
/
AvailabilityGenerator.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
<?php
declare(strict_types=1);
/*
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCA\Calendar\Service\Appointments;
use DateInterval;
use DatePeriod;
use DateTimeImmutable;
use DateTimeZone;
use OCA\Calendar\Db\AppointmentConfig;
use OCP\AppFramework\Utility\ITimeFactory;
use Psr\Log\LoggerInterface;
use function ceil;
use function max;
use function min;
class AvailabilityGenerator {
/** @var ITimeFactory */
private $timeFactory;
public function __construct(ITimeFactory $timeFactory, private LoggerInterface $logger) {
$this->timeFactory = $timeFactory;
}
/**
* Generate intervals at which the user is generally available
*
* @param AppointmentConfig $config
* @param int $start
* @param int $end
*
* @return Interval[]
*/
public function generate(AppointmentConfig $config,
int $start,
int $end): array {
$now = $this->timeFactory->getTime();
$bufferBeforeStart = ($config->getTimeBeforeNextSlot() ?? 0);
$earliestStart = max(
$start,
$now + $bufferBeforeStart,
($config->getStart() ?? $now) + $bufferBeforeStart
);
// Always round to "beautiful" slot starts according to slot length
// E.g. 5m slots should only be available at 10:20 and 10:25, not at 10:17
// when the user opens the page at 10:17.
// But only do this when the time isn't already a "pretty" time
if ($earliestStart % $config->getIncrement() !== 0) {
$roundTo = (int)round(($config->getIncrement()) / 300) * 300;
$earliestStart = (int)ceil($earliestStart / $roundTo) * $roundTo;
}
$latestEnd = min(
$end,
$config->getEnd() ?? $end
);
// If we reach this state then there are no available dates anymore
if ($latestEnd <= $earliestStart) {
$this->logger->debug('Appointment config ' . $config->getToken() . ' has {latestEnd} as latest end but {earliestStart} as earliest start. No slots available.', [
'latestEnd' => $latestEnd,
'earliestStart' => $earliestStart,
'app' => 'calendar-appointments'
]);
return [];
}
if (empty($config->getAvailability())) {
// No availability -> full time range is available
$this->logger->debug('Full time range available', ['app' => 'calendar-appointments']);
return [
new Interval($earliestStart, $latestEnd),
];
}
$availabilityRule = json_decode($config->getAvailability(), true);
$timeZone = $availabilityRule['timezoneId'];
$slots = $availabilityRule['slots'];
$applicableSlots = $this->filterDates($start, $slots, $timeZone);
$this->logger->debug('Found ' . count($applicableSlots) . ' applicable slot(s) after date filtering', ['app' => 'calendar-appointments']);
$intervals = [];
foreach ($applicableSlots as $slot) {
if ($slot->getEnd() <= $earliestStart || $slot->getStart() >= $latestEnd) {
continue;
}
$startSlot = max(
$earliestStart,
$slot->getStart()
);
$endSlot = min(
$latestEnd,
$slot->getEnd()
);
$intervals[] = new Interval($startSlot, $endSlot);
}
return $intervals;
}
/**
* @param int $start
* @param array $availabilityArray
* @param string $timeZone
*
* @return Interval[]
*/
private function filterDates(int $start, array $availabilityArray, string $timeZone): array {
$tz = new DateTimeZone($timeZone);
// First, transform all timestamps to DateTime Objects
$availabilityRules = [];
foreach ($availabilityArray as $key => $availabilitySlots) {
if (empty($availabilitySlots)) {
$availabilityRules[$key] = [];
continue;
}
foreach ($availabilitySlots as $slot) {
$availabilityRules[$key][] = [
'start' => (new DateTimeImmutable())->setTimezone($tz)->setTimestamp($slot['start']),
'end' => (new DateTimeImmutable())->setTimezone($tz)->setTimestamp($slot['end'])
];
}
}
// get the period the check can apply to
$period = new DatePeriod(
(new DateTimeImmutable())->setTimezone($tz)->setTimestamp($start - 87600)->setTime(0, 0),
new DateInterval('P1D'),
(new DateTimeImmutable())->setTimezone($tz)->setTimestamp($start + 87600)->setTime(23, 59)
);
/** @var Interval[] $applicable */
$applicable = [];
foreach ($period as $item) {
/** @var DateTimeImmutable $item */
// get the weekday from our item and select the applicable rule
$weekday = strtoupper(mb_strcut($item->format('D'), 0, 2));
/** @var DateTimeImmutable[][] $dailyRules */
$dailyRules = $availabilityRules[$weekday];
// days with no rule should be treated as unavailable
if (empty($dailyRules)) {
continue;
}
foreach ($dailyRules as $dailyRule) {
$dStart = $dailyRule['start'];
$dEnd = $dailyRule['end'];
$applicable[] = new Interval(
$item->setTime((int)$dStart->format('H'), (int)$dStart->format('i'))->getTimestamp(),
$item->setTime((int)$dEnd->format('H'), (int)$dEnd->format('i'))->getTimestamp()
);
}
}
return $applicable;
}
}