1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* @file |
5
|
|
|
* Class EventItemizer |
6
|
|
|
*/ |
7
|
|
|
|
8
|
|
|
namespace Roomify\Bat\Event; |
9
|
|
|
|
10
|
|
|
use Roomify\Bat\Event\EventInterface; |
11
|
|
|
use Roomify\Bat\Unit\Unit; |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* The EventItemizer class does the hard work of splitting an event into discrete time |
15
|
|
|
* units in the following structure |
16
|
|
|
* [BAT_DAY][year][month][day][value] |
17
|
|
|
* [BAT_HOUR][year][month][day][hour][value] |
18
|
|
|
* [BAT_MINUTE][year][month][hour][minute][value] |
19
|
|
|
* |
20
|
|
|
* This data structure allows to quickly retrieve the state of a unit for a provided time |
21
|
|
|
* period. The value is either the default value of the unit or the value that the event |
22
|
|
|
* introduced. If the value in either BAT_DAY or BAT_HOUR is -1 it means that that specific |
23
|
|
|
* day or that specific hour are non-determinant. This means that in order to determine the |
24
|
|
|
* value of the event for that point in time we need to look at a lower level of granularity. |
25
|
|
|
* |
26
|
|
|
* Example - consider breaking up the following event |
27
|
|
|
* |
28
|
|
|
* start-date: 2016-01-01 1210 |
29
|
|
|
* end-date: 2016-01-03 1210 |
30
|
|
|
* value: 10 |
31
|
|
|
* |
32
|
|
|
* [BAT_DAY][2016][01][d1][-1] - The first day starts at 1210 so the DAY array is not enough |
33
|
|
|
* [BAT_DAY][2016][01][d2][10] - The second day is a full day at the same value of 10 |
34
|
|
|
* [BAT_DAY][2016][01][d3][-1] - The last day is no a full day so the day array in non-determinant |
35
|
|
|
* [BAT_HOUR][2016][01][d1][h12][-1] - The first hour of the event starts at 10 minutes so the hour is non-determinant |
36
|
|
|
* [BAT_HOUR][2016][01][d1][h13][10] |
37
|
|
|
* [BAT_HOUR][2016][01][d1][h14][10] |
38
|
|
|
* [BAT_HOUR][2016][01][d1][h15][10] |
39
|
|
|
* [BAT_HOUR][2016][01][d1][h16][10] |
40
|
|
|
* [BAT_HOUR][2016][01][d1][h17][10] |
41
|
|
|
* [BAT_HOUR][2016][01][d1][h18][10] |
42
|
|
|
* [BAT_HOUR][2016][01][d1][h19][10] |
43
|
|
|
* [BAT_HOUR][2016][01][d1][h20][10] |
44
|
|
|
* [BAT_HOUR][2016][01][d1][h21][10] |
45
|
|
|
* [BAT_HOUR][2016][01][d1][h22][10] |
46
|
|
|
* [BAT_HOUR][2016][01][d1][h23][10] |
47
|
|
|
* - we don't need to state anything about hours on the 2nd of Jan since the day array is determinant |
48
|
|
|
* [BAT_HOUR][2016][01][d3][h01][10] |
49
|
|
|
* [BAT_HOUR][2016][01][d3][h02][10] |
50
|
|
|
* [BAT_HOUR][2016][01][d3][h03][10] |
51
|
|
|
* [BAT_HOUR][2016][01][d3][h04][10] |
52
|
|
|
* [BAT_HOUR][2016][01][d3][h05][10] |
53
|
|
|
* [BAT_HOUR][2016][01][d3][h06][10] |
54
|
|
|
* [BAT_HOUR][2016][01][d3][h07][10] |
55
|
|
|
* [BAT_HOUR][2016][01][d3][h08][10] |
56
|
|
|
* [BAT_HOUR][2016][01][d3][h09][10] |
57
|
|
|
* [BAT_HOUR][2016][01][d3][h10][10] |
58
|
|
|
* [BAT_HOUR][2016][01][d3][h11][10] |
59
|
|
|
* [BAT_HOUR][2016][01][d3][h12][-1] - The last hour of the event ends at the 10th minute so will need to minute array |
60
|
|
|
* |
61
|
|
|
* [BAT_MINUTE][2016][01][d1][h12][m00][10] - Minutes, which is the maximum granularity, are always determinant |
62
|
|
|
* [BAT_MINUTE][2016][01][d1][h12][m01][10] |
63
|
|
|
* [BAT_MINUTE][2016][01][d1][h12][m02][10] |
64
|
|
|
* [BAT_MINUTE][2016][01][d1][h12][m03][10] |
65
|
|
|
* [BAT_MINUTE][2016][01][d1][h12][m04][10] |
66
|
|
|
* [BAT_MINUTE][2016][01][d1][h12][m05][10] |
67
|
|
|
* [BAT_MINUTE][2016][01][d1][h12][m06][10] |
68
|
|
|
* [BAT_MINUTE][2016][01][d1][h12][m07][10] |
69
|
|
|
* [BAT_MINUTE][2016][01][d1][h12][m08][10] |
70
|
|
|
* [BAT_MINUTE][2016][01][d1][h12][m09][10] |
71
|
|
|
* [BAT_MINUTE][2016][01][d1][h12][m10][10] |
72
|
|
|
* |
73
|
|
|
* [BAT_MINUTE][2016][01][d3][h12][m00][10] |
74
|
|
|
* [BAT_MINUTE][2016][01][d3][h12][m01][10] |
75
|
|
|
* [BAT_MINUTE][2016][01][d3][h12][m02][10] |
76
|
|
|
* [BAT_MINUTE][2016][01][d3][h12][m03][10] |
77
|
|
|
* [BAT_MINUTE][2016][01][d3][h12][m04][10] |
78
|
|
|
* [BAT_MINUTE][2016][01][d3][h12][m05][10] |
79
|
|
|
* [BAT_MINUTE][2016][01][d3][h12][m06][10] |
80
|
|
|
* [BAT_MINUTE][2016][01][d3][h12][m07][10] |
81
|
|
|
* [BAT_MINUTE][2016][01][d3][h12][m08][10] |
82
|
|
|
* [BAT_MINUTE][2016][01][d3][h12][m09][10] |
83
|
|
|
* [BAT_MINUTE][2016][01][d3][h12][m10][10] |
84
|
|
|
* |
85
|
|
|
* Class EventItemizer |
86
|
|
|
* @package Roomify\Bat\Event |
87
|
|
|
*/ |
88
|
|
|
class EventItemizer { |
89
|
|
|
|
90
|
|
|
const BAT_DAY = 'bat_day'; |
91
|
|
|
const BAT_HOUR = 'bat_hour'; |
92
|
|
|
const BAT_MINUTE = 'bat_minute'; |
93
|
|
|
const BAT_HOURLY = 'bat_hourly'; |
94
|
|
|
const BAT_DAILY = 'bat_daily'; |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* @var \Roomify\Bat\Event\EventInterface |
98
|
|
|
*/ |
99
|
|
|
protected $event; |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* @var string |
103
|
|
|
*/ |
104
|
|
|
protected $granularity; |
105
|
|
|
|
106
|
|
|
public function __construct(EventInterface $event, $granularity = EventItemizer::BAT_HOURLY) { |
107
|
|
|
$this->event = $event; |
108
|
|
|
$this->granularity = $granularity; |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* Transforms the event in a breakdown of days, hours and minutes with associated states. |
113
|
|
|
* |
114
|
|
|
* @return array |
115
|
|
|
*/ |
116
|
|
|
public function itemizeEvent() { |
117
|
|
|
// In order to itemize the event we cycle through each day of the event and determine |
118
|
|
|
// what should go in the DAY array to start with. While we could use P1M this created |
119
|
|
|
// problems with months like February (because the period is 30 days) so stepping through |
120
|
|
|
// each day is safer. |
121
|
|
|
$interval = new \DateInterval('P1D'); |
122
|
|
|
|
123
|
|
|
// Set the end date to the last day of the month so that we are sure to get that last month unless |
124
|
|
|
// we are already dealing with the last day of the month |
125
|
|
|
if ($this->event->getEndDate()->format('d') != $this->event->getEndDate()->format('t')) { |
126
|
|
|
$adjusted_end_day = new \DateTime($this->event->getEndDate()->format('Y-n-t')); |
127
|
|
|
} |
128
|
|
|
// Deal with the special case of last day of month and daily granularity where the DatePeriod will not indicate one day unless the time is slightly different |
129
|
|
|
// We add a minute to compensate |
130
|
|
|
elseif (($this->event->getStartDate()->format('Y-m-d H:i') == $this->event->getEndDate()->format('Y-m-d H:i')) && $this->granularity == EventItemizer::BAT_DAILY) { |
131
|
|
|
$adjusted_end_day = new \DateTime($this->event->getEndDate()->add(new \DateInterval('PT1M'))->format('Y-m-d H:i')); |
132
|
|
|
} |
133
|
|
|
else { |
134
|
|
|
$adjusted_end_day = new \DateTime($this->event->getEndDate()->format('Y-m-d H:i')); |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
$daterange = new \DatePeriod($this->event->getStartDate(), $interval, $adjusted_end_day); |
138
|
|
|
|
139
|
|
|
$itemized = array(); |
140
|
|
|
|
141
|
|
|
$old_month = $this->event->getStartDate()->format('Y-n'); |
142
|
|
|
|
143
|
|
|
$start = TRUE; |
144
|
|
|
|
145
|
|
|
// Cycle through each month |
146
|
|
|
foreach ($daterange as $date) { |
147
|
|
|
|
148
|
|
|
// Check if we have |
149
|
|
|
if (($date->format('Y-n') != $old_month) || ($start)) { |
150
|
|
|
|
151
|
|
|
$year = $date->format("Y"); |
152
|
|
|
$dayinterval = new \DateInterval('P1D'); |
153
|
|
|
|
154
|
|
|
// Handle the first month |
155
|
|
|
if ($this->event->isFirstMonth($date)) { |
156
|
|
|
// If we are in the same month the end date is the end date of the event |
157
|
|
|
if ($this->event->isSameMonth()) { |
158
|
|
|
$dayrange = new \DatePeriod($this->event->getStartDate(), $dayinterval, new \DateTime($this->event->getEndDate()->format("Y-n-j 23:59:59"))); |
159
|
|
|
} else { // alternatively it is the last day of the start month |
160
|
|
|
$dayrange = new \DatePeriod($this->event->getStartDate(), $dayinterval, $this->event->endMonthDate($this->event->getStartDate())); |
161
|
|
|
} |
162
|
|
View Code Duplication |
foreach ($dayrange as $day) { |
|
|
|
|
163
|
|
|
$itemized[EventItemizer::BAT_DAY][$year][$day->format('n')]['d' . $day->format('j')] = $this->event->getValue(); |
164
|
|
|
} |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
// Handle the last month (will be skipped if event is same month) |
168
|
|
|
elseif ($this->event->isLastMonth($date)) { |
169
|
|
|
$dayrange = new \DatePeriod(new \DateTime($date->format("Y-n-1")), $dayinterval, $this->event->getEndDate()); |
170
|
|
View Code Duplication |
foreach ($dayrange as $day) { |
|
|
|
|
171
|
|
|
$itemized[EventItemizer::BAT_DAY][$year][$day->format('n')]['d' . $day->format('j')] = $this->event->getValue(); |
172
|
|
|
} |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
// We are in an in-between month - just cycle through and set dates (time on end date set to ensure it is included) |
176
|
|
|
else { |
177
|
|
|
$dayrange = new \DatePeriod(new \DateTime($date->format("Y-n-1")), $dayinterval, new \DateTime($date->format("Y-n-t 23:59:59"))); |
178
|
|
View Code Duplication |
foreach ($dayrange as $day) { |
|
|
|
|
179
|
|
|
$itemized[EventItemizer::BAT_DAY][$year][$day->format('n')]['d' . $day->format('j')] = $this->event->getValue(); |
180
|
|
|
} |
181
|
|
|
} |
182
|
|
|
} |
183
|
|
|
$start = FALSE; |
184
|
|
|
$old_month = $date->format('Y-n'); |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
if ($this->granularity == EventItemizer::BAT_HOURLY) { |
188
|
|
|
// Add granural info in |
189
|
|
|
$itemized = $this->createDayGranural($itemized); |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
return $itemized; |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
/** |
196
|
|
|
* Based on the start and end dates of the event it creates the appropriate granular events |
197
|
|
|
* and adds them to an array suitable for manipulating easily or storing in the database. |
198
|
|
|
* |
199
|
|
|
* @param array $itemized |
200
|
|
|
* @return array |
201
|
|
|
*/ |
202
|
|
|
private function createDayGranural($itemized = array()) { |
203
|
|
|
$interval = new \DateInterval('PT1M'); |
204
|
|
|
|
205
|
|
|
$sy = $this->event->getStartDate()->format('Y'); |
206
|
|
|
$sm = $this->event->getStartDate()->format('n'); |
207
|
|
|
$sd = $this->event->getStartDate()->format('j'); |
208
|
|
|
|
209
|
|
|
$ey = $this->event->getEndDate()->format('Y'); |
210
|
|
|
$em = $this->event->getEndDate()->format('n'); |
211
|
|
|
$ed = $this->event->getEndDate()->format('j'); |
212
|
|
|
|
213
|
|
|
// Clone the dates otherwise changes will change the event dates themselves |
214
|
|
|
$start_date = clone($this->event->getStartDate()); |
215
|
|
|
$end_date = clone($this->event->getEndDate()); |
216
|
|
|
|
217
|
|
|
if ($this->event->isSameDay()) { |
218
|
|
|
if (!($this->event->getStartDate()->format('H:i') == '00:00' && $this->event->getEndDate()->format('H:i') == '23:59')) { |
219
|
|
|
$itemized_same_day = $this->createHourlyGranular($start_date, $end_date->add(new \DateInterval('PT1M')), $interval); |
220
|
|
|
$itemized[EventItemizer::BAT_DAY][$sy][$sm]['d' . $sd] = -1; |
221
|
|
|
$itemized[EventItemizer::BAT_HOUR][$sy][$sm]['d' . $sd] = $itemized_same_day[EventItemizer::BAT_HOUR][$sy][$sm]['d' . $sd]; |
222
|
|
|
$itemized[EventItemizer::BAT_MINUTE][$sy][$sm]['d' . $sd] = $itemized_same_day[EventItemizer::BAT_MINUTE][$sy][$sm]['d' . $sd]; |
223
|
|
|
} |
224
|
|
|
} |
225
|
|
|
else { |
226
|
|
|
// Deal with the start day unless it starts on midnight precisely at which point the whole day is booked |
227
|
|
View Code Duplication |
if (!($this->event->getStartDate()->format('H:i') == '00:00')) { |
|
|
|
|
228
|
|
|
$itemized_start = $this->createHourlyGranular($start_date, new \DateTime($start_date->format("Y-n-j 23:59:59")), $interval); |
229
|
|
|
$itemized[EventItemizer::BAT_DAY][$sy][$sm]['d' . $sd] = -1; |
230
|
|
|
$itemized[EventItemizer::BAT_HOUR][$sy][$sm]['d' . $sd] = $itemized_start[EventItemizer::BAT_HOUR][$sy][$sm]['d' . $sd]; |
231
|
|
|
$itemized[EventItemizer::BAT_MINUTE][$sy][$sm]['d' . $sd] = $itemized_start[EventItemizer::BAT_MINUTE][$sy][$sm]['d' . $sd]; |
232
|
|
|
} |
233
|
|
|
else { |
234
|
|
|
// Just set an empty hour and minute |
235
|
|
|
$itemized[EventItemizer::BAT_HOUR][$sy][$sm]['d' . $sd] = array(); |
236
|
|
|
$itemized[EventItemizer::BAT_MINUTE][$sy][$sm]['d' . $sd] = array(); |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
// Deal with the end date unless it ends just before midnight at which point we don't need to go further |
240
|
|
View Code Duplication |
if ($this->event->getEndDate()->format('H:i') == '23:59') { |
|
|
|
|
241
|
|
|
$itemized[EventItemizer::BAT_HOUR][$ey][$em]['d' . $ed] = array(); |
242
|
|
|
$itemized[EventItemizer::BAT_MINUTE][$ey][$em]['d' . $ed] = array(); |
243
|
|
|
} |
244
|
|
|
else { |
245
|
|
|
$itemized_end = $this->createHourlyGranular(new \DateTime($end_date->format("Y-n-j 00:00:00")), $end_date->add(new \DateInterval('PT1M')), $interval); |
246
|
|
|
$itemized[EventItemizer::BAT_DAY][$ey][$em]['d' . $ed] = -1; |
247
|
|
|
$itemized[EventItemizer::BAT_HOUR][$ey][$em]['d' . $ed] = $itemized_end[EventItemizer::BAT_HOUR][$ey][$em]['d' . $ed]; |
248
|
|
|
$itemized[EventItemizer::BAT_MINUTE][$ey][$em]['d' . $ed] = $itemized_end[EventItemizer::BAT_MINUTE][$ey][$em]['d' . $ed]; |
249
|
|
|
} |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
return $itemized; |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
/** |
256
|
|
|
* Given a DatePeriod it transforms it in hours and minutes. Used to break the first and |
257
|
|
|
* last days of an event into more granular events. |
258
|
|
|
* |
259
|
|
|
* @param \DateTime $start_date |
260
|
|
|
* @param \DateTime $end_date |
261
|
|
|
* @param \DateInterval $interval |
262
|
|
|
* @return array |
263
|
|
|
*/ |
264
|
|
|
public function createHourlyGranular(\DateTime $start_date, \DateTime $end_date, \DateInterval $interval) { |
265
|
|
|
$period = new \DatePeriod($start_date, $interval, $end_date); |
266
|
|
|
|
267
|
|
|
$itemized = array(); |
268
|
|
|
|
269
|
|
|
$start_minute = (int)$start_date->format('i'); |
270
|
|
|
|
271
|
|
|
$event_value = $this->event->getValue(); |
272
|
|
|
|
273
|
|
|
$year = $start_date->format('Y'); |
274
|
|
|
$month = $start_date->format('n'); |
275
|
|
|
$day = $start_date->format('j'); |
276
|
|
|
$hour = $start_date->format('G'); |
277
|
|
|
$min = $start_date->format('i'); |
278
|
|
|
|
279
|
|
|
foreach ($period as $minute) { |
280
|
|
|
// Re-calculate if we're at a day boundary. |
281
|
|
|
if ($hour == 24) { |
282
|
|
|
$year = $minute->format('Y'); |
283
|
|
|
$month = $minute->format('n'); |
284
|
|
|
$day = $minute->format('j'); |
285
|
|
|
$hour = $minute->format('G'); |
286
|
|
|
$min = $minute->format('i'); |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
// Doing minutes so set the values in the minute array |
290
|
|
|
$itemized[EventItemizer::BAT_MINUTE][$year][$month]['d' . $day]['h' . $hour]['m' . $min] = $event_value; |
291
|
|
|
// Let the hours know that it cannot determine availability |
292
|
|
|
$itemized[EventItemizer::BAT_HOUR][$year][$month]['d' . $day]['h' . $hour] = -1; |
293
|
|
|
$min++; |
294
|
|
|
|
295
|
|
|
if ($min == 60 && $start_minute !== 0) { |
296
|
|
|
// Not a real hour - leave as is and move on |
297
|
|
|
$min = 0; |
298
|
|
|
$hour++; |
299
|
|
|
$start_minute = 0; |
300
|
|
|
} |
301
|
|
|
elseif ($min == 60 && $start_minute == 0) { |
302
|
|
|
// Did a real whole hour so initialize the hour |
303
|
|
|
$itemized[EventItemizer::BAT_HOUR][$year][$month]['d' . $day]['h' . $hour] = $event_value; |
304
|
|
|
|
305
|
|
|
$min = 0; |
306
|
|
|
$hour++; |
307
|
|
|
$start_minute = 0; |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
$min = str_pad($min, 2, 0, STR_PAD_LEFT); |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
// Daylight Saving Time |
314
|
|
|
$timezone = new \DateTimeZone(date_default_timezone_get()); |
315
|
|
|
$transitions = $timezone->getTransitions($start_date->getTimestamp(), $end_date->getTimestamp()); |
316
|
|
|
|
317
|
|
|
unset($transitions[0]); |
318
|
|
|
foreach ($transitions as $transition) { |
319
|
|
|
if ($transition['isdst']) { |
320
|
|
|
$date = new \DateTime(); |
321
|
|
|
$date->setTimestamp($transition['ts']); |
322
|
|
|
|
323
|
|
|
$hour = $date->format('G'); |
324
|
|
|
for ($i = 0; $i < 60; $i++) { |
325
|
|
|
$minute = ($i < 10) ? '0' . $i : $i; |
326
|
|
|
|
327
|
|
|
$itemized[EventItemizer::BAT_MINUTE][$date->format('Y')][$date->format('n')]['d' . $date->format('j')]['h' . $hour]['m' . $minute] = $this->event->getValue(); |
328
|
|
|
} |
329
|
|
|
} |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
return $itemized; |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
} |
336
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.