7
|
1 <?php
|
|
2
|
|
3 namespace Sabre\VObject\Recur;
|
|
4
|
|
5 use DateTime;
|
|
6 use InvalidArgumentException;
|
|
7 use Iterator;
|
|
8 use Sabre\VObject\DateTimeParser;
|
|
9 use Sabre\VObject\Property;
|
|
10
|
|
11
|
|
12 /**
|
|
13 * RRuleParser
|
|
14 *
|
|
15 * This class receives an RRULE string, and allows you to iterate to get a list
|
|
16 * of dates in that recurrence.
|
|
17 *
|
|
18 * For instance, passing: FREQ=DAILY;LIMIT=5 will cause the iterator to contain
|
|
19 * 5 items, one for each day.
|
|
20 *
|
|
21 * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
|
|
22 * @author Evert Pot (http://evertpot.com/)
|
|
23 * @license http://sabre.io/license/ Modified BSD License
|
|
24 */
|
|
25 class RRuleIterator implements Iterator {
|
|
26
|
|
27 /**
|
|
28 * Creates the Iterator
|
|
29 *
|
|
30 * @param string|array $rrule
|
|
31 * @param DateTime $start
|
|
32 */
|
|
33 public function __construct($rrule, DateTime $start) {
|
|
34
|
|
35 $this->startDate = $start;
|
|
36 $this->parseRRule($rrule);
|
|
37 $this->currentDate = clone $this->startDate;
|
|
38
|
|
39 }
|
|
40
|
|
41 /* Implementation of the Iterator interface {{{ */
|
|
42
|
|
43 public function current() {
|
|
44
|
|
45 if (!$this->valid()) return null;
|
|
46 return clone $this->currentDate;
|
|
47
|
|
48 }
|
|
49
|
|
50 /**
|
|
51 * Returns the current item number
|
|
52 *
|
|
53 * @return int
|
|
54 */
|
|
55 public function key() {
|
|
56
|
|
57 return $this->counter;
|
|
58
|
|
59 }
|
|
60
|
|
61 /**
|
|
62 * Returns whether the current item is a valid item for the recurrence
|
|
63 * iterator. This will return false if we've gone beyond the UNTIL or COUNT
|
|
64 * statements.
|
|
65 *
|
|
66 * @return bool
|
|
67 */
|
|
68 public function valid() {
|
|
69
|
|
70 if (!is_null($this->count)) {
|
|
71 return $this->counter < $this->count;
|
|
72 }
|
|
73 return is_null($this->until) || $this->currentDate <= $this->until;
|
|
74
|
|
75 }
|
|
76
|
|
77 /**
|
|
78 * Resets the iterator
|
|
79 *
|
|
80 * @return void
|
|
81 */
|
|
82 public function rewind() {
|
|
83
|
|
84 $this->currentDate = clone $this->startDate;
|
|
85 $this->counter = 0;
|
|
86
|
|
87 }
|
|
88
|
|
89 /**
|
|
90 * Goes on to the next iteration
|
|
91 *
|
|
92 * @return void
|
|
93 */
|
|
94 public function next() {
|
|
95
|
|
96 $previousStamp = $this->currentDate->getTimeStamp();
|
|
97
|
|
98 // Otherwise, we find the next event in the normal RRULE
|
|
99 // sequence.
|
|
100 switch($this->frequency) {
|
|
101
|
|
102 case 'hourly' :
|
|
103 $this->nextHourly();
|
|
104 break;
|
|
105
|
|
106 case 'daily' :
|
|
107 $this->nextDaily();
|
|
108 break;
|
|
109
|
|
110 case 'weekly' :
|
|
111 $this->nextWeekly();
|
|
112 break;
|
|
113
|
|
114 case 'monthly' :
|
|
115 $this->nextMonthly();
|
|
116 break;
|
|
117
|
|
118 case 'yearly' :
|
|
119 $this->nextYearly();
|
|
120 break;
|
|
121
|
|
122 }
|
|
123 $this->counter++;
|
|
124
|
|
125 }
|
|
126
|
|
127 /* End of Iterator implementation }}} */
|
|
128
|
|
129 /**
|
|
130 * Returns true if this recurring event never ends.
|
|
131 *
|
|
132 * @return bool
|
|
133 */
|
|
134 public function isInfinite() {
|
|
135
|
|
136 return !$this->count && !$this->until;
|
|
137
|
|
138 }
|
|
139
|
|
140 /**
|
|
141 * This method allows you to quickly go to the next occurrence after the
|
|
142 * specified date.
|
|
143 *
|
|
144 * @param DateTime $dt
|
|
145 * @return void
|
|
146 */
|
|
147 public function fastForward(\DateTime $dt) {
|
|
148
|
|
149 while($this->valid() && $this->currentDate < $dt ) {
|
|
150 $this->next();
|
|
151 }
|
|
152
|
|
153 }
|
|
154
|
|
155 /**
|
|
156 * The reference start date/time for the rrule.
|
|
157 *
|
|
158 * All calculations are based on this initial date.
|
|
159 *
|
|
160 * @var DateTime
|
|
161 */
|
|
162 protected $startDate;
|
|
163
|
|
164 /**
|
|
165 * The date of the current iteration. You can get this by calling
|
|
166 * ->current().
|
|
167 *
|
|
168 * @var DateTime
|
|
169 */
|
|
170 protected $currentDate;
|
|
171
|
|
172 /**
|
|
173 * Frequency is one of: secondly, minutely, hourly, daily, weekly, monthly,
|
|
174 * yearly.
|
|
175 *
|
|
176 * @var string
|
|
177 */
|
|
178 protected $frequency;
|
|
179
|
|
180 /**
|
|
181 * The number of recurrences, or 'null' if infinitely recurring.
|
|
182 *
|
|
183 * @var int
|
|
184 */
|
|
185 protected $count;
|
|
186
|
|
187 /**
|
|
188 * The interval.
|
|
189 *
|
|
190 * If for example frequency is set to daily, interval = 2 would mean every
|
|
191 * 2 days.
|
|
192 *
|
|
193 * @var int
|
|
194 */
|
|
195 protected $interval = 1;
|
|
196
|
|
197 /**
|
|
198 * The last instance of this recurrence, inclusively
|
|
199 *
|
|
200 * @var \DateTime|null
|
|
201 */
|
|
202 protected $until;
|
|
203
|
|
204 /**
|
|
205 * Which seconds to recur.
|
|
206 *
|
|
207 * This is an array of integers (between 0 and 60)
|
|
208 *
|
|
209 * @var array
|
|
210 */
|
|
211 protected $bySecond;
|
|
212
|
|
213 /**
|
|
214 * Which minutes to recur
|
|
215 *
|
|
216 * This is an array of integers (between 0 and 59)
|
|
217 *
|
|
218 * @var array
|
|
219 */
|
|
220 protected $byMinute;
|
|
221
|
|
222 /**
|
|
223 * Which hours to recur
|
|
224 *
|
|
225 * This is an array of integers (between 0 and 23)
|
|
226 *
|
|
227 * @var array
|
|
228 */
|
|
229 protected $byHour;
|
|
230
|
|
231 /**
|
|
232 * The current item in the list.
|
|
233 *
|
|
234 * You can get this number with the key() method.
|
|
235 *
|
|
236 * @var int
|
|
237 */
|
|
238 protected $counter = 0;
|
|
239
|
|
240 /**
|
|
241 * Which weekdays to recur.
|
|
242 *
|
|
243 * This is an array of weekdays
|
|
244 *
|
|
245 * This may also be preceeded by a positive or negative integer. If present,
|
|
246 * this indicates the nth occurrence of a specific day within the monthly or
|
|
247 * yearly rrule. For instance, -2TU indicates the second-last tuesday of
|
|
248 * the month, or year.
|
|
249 *
|
|
250 * @var array
|
|
251 */
|
|
252 protected $byDay;
|
|
253
|
|
254 /**
|
|
255 * Which days of the month to recur
|
|
256 *
|
|
257 * This is an array of days of the months (1-31). The value can also be
|
|
258 * negative. -5 for instance means the 5th last day of the month.
|
|
259 *
|
|
260 * @var array
|
|
261 */
|
|
262 protected $byMonthDay;
|
|
263
|
|
264 /**
|
|
265 * Which days of the year to recur.
|
|
266 *
|
|
267 * This is an array with days of the year (1 to 366). The values can also
|
|
268 * be negative. For instance, -1 will always represent the last day of the
|
|
269 * year. (December 31st).
|
|
270 *
|
|
271 * @var array
|
|
272 */
|
|
273 protected $byYearDay;
|
|
274
|
|
275 /**
|
|
276 * Which week numbers to recur.
|
|
277 *
|
|
278 * This is an array of integers from 1 to 53. The values can also be
|
|
279 * negative. -1 will always refer to the last week of the year.
|
|
280 *
|
|
281 * @var array
|
|
282 */
|
|
283 protected $byWeekNo;
|
|
284
|
|
285 /**
|
|
286 * Which months to recur.
|
|
287 *
|
|
288 * This is an array of integers from 1 to 12.
|
|
289 *
|
|
290 * @var array
|
|
291 */
|
|
292 protected $byMonth;
|
|
293
|
|
294 /**
|
|
295 * Which items in an existing st to recur.
|
|
296 *
|
|
297 * These numbers work together with an existing by* rule. It specifies
|
|
298 * exactly which items of the existing by-rule to filter.
|
|
299 *
|
|
300 * Valid values are 1 to 366 and -1 to -366. As an example, this can be
|
|
301 * used to recur the last workday of the month.
|
|
302 *
|
|
303 * This would be done by setting frequency to 'monthly', byDay to
|
|
304 * 'MO,TU,WE,TH,FR' and bySetPos to -1.
|
|
305 *
|
|
306 * @var array
|
|
307 */
|
|
308 protected $bySetPos;
|
|
309
|
|
310 /**
|
|
311 * When the week starts.
|
|
312 *
|
|
313 * @var string
|
|
314 */
|
|
315 protected $weekStart = 'MO';
|
|
316
|
|
317 /* Functions that advance the iterator {{{ */
|
|
318
|
|
319 /**
|
|
320 * Does the processing for advancing the iterator for hourly frequency.
|
|
321 *
|
|
322 * @return void
|
|
323 */
|
|
324 protected function nextHourly() {
|
|
325
|
|
326 $this->currentDate->modify('+' . $this->interval . ' hours');
|
|
327
|
|
328 }
|
|
329
|
|
330 /**
|
|
331 * Does the processing for advancing the iterator for daily frequency.
|
|
332 *
|
|
333 * @return void
|
|
334 */
|
|
335 protected function nextDaily() {
|
|
336
|
|
337 if (!$this->byHour && !$this->byDay) {
|
|
338 $this->currentDate->modify('+' . $this->interval . ' days');
|
|
339 return;
|
|
340 }
|
|
341
|
|
342 if (isset($this->byHour)) {
|
|
343 $recurrenceHours = $this->getHours();
|
|
344 }
|
|
345
|
|
346 if (isset($this->byDay)) {
|
|
347 $recurrenceDays = $this->getDays();
|
|
348 }
|
|
349
|
|
350 if (isset($this->byMonth)) {
|
|
351 $recurrenceMonths = $this->getMonths();
|
|
352 }
|
|
353
|
|
354 do {
|
|
355 if ($this->byHour) {
|
|
356 if ($this->currentDate->format('G') == '23') {
|
|
357 // to obey the interval rule
|
|
358 $this->currentDate->modify('+' . $this->interval-1 . ' days');
|
|
359 }
|
|
360
|
|
361 $this->currentDate->modify('+1 hours');
|
|
362
|
|
363 } else {
|
|
364 $this->currentDate->modify('+' . $this->interval . ' days');
|
|
365
|
|
366 }
|
|
367
|
|
368 // Current month of the year
|
|
369 $currentMonth = $this->currentDate->format('n');
|
|
370
|
|
371 // Current day of the week
|
|
372 $currentDay = $this->currentDate->format('w');
|
|
373
|
|
374 // Current hour of the day
|
|
375 $currentHour = $this->currentDate->format('G');
|
|
376
|
|
377 } while (
|
|
378 ($this->byDay && !in_array($currentDay, $recurrenceDays)) ||
|
|
379 ($this->byHour && !in_array($currentHour, $recurrenceHours)) ||
|
|
380 ($this->byMonth && !in_array($currentMonth, $recurrenceMonths))
|
|
381 );
|
|
382
|
|
383 }
|
|
384
|
|
385 /**
|
|
386 * Does the processing for advancing the iterator for weekly frequency.
|
|
387 *
|
|
388 * @return void
|
|
389 */
|
|
390 protected function nextWeekly() {
|
|
391
|
|
392 if (!$this->byHour && !$this->byDay) {
|
|
393 $this->currentDate->modify('+' . $this->interval . ' weeks');
|
|
394 return;
|
|
395 }
|
|
396
|
|
397 if ($this->byHour) {
|
|
398 $recurrenceHours = $this->getHours();
|
|
399 }
|
|
400
|
|
401 if ($this->byDay) {
|
|
402 $recurrenceDays = $this->getDays();
|
|
403 }
|
|
404
|
|
405 // First day of the week:
|
|
406 $firstDay = $this->dayMap[$this->weekStart];
|
|
407
|
|
408 do {
|
|
409
|
|
410 if ($this->byHour) {
|
|
411 $this->currentDate->modify('+1 hours');
|
|
412 } else {
|
|
413 $this->currentDate->modify('+1 days');
|
|
414 }
|
|
415
|
|
416 // Current day of the week
|
|
417 $currentDay = (int) $this->currentDate->format('w');
|
|
418
|
|
419 // Current hour of the day
|
|
420 $currentHour = (int) $this->currentDate->format('G');
|
|
421
|
|
422 // We need to roll over to the next week
|
|
423 if ($currentDay === $firstDay && (!$this->byHour || $currentHour == '0')) {
|
|
424 $this->currentDate->modify('+' . $this->interval-1 . ' weeks');
|
|
425
|
|
426 // We need to go to the first day of this week, but only if we
|
|
427 // are not already on this first day of this week.
|
|
428 if($this->currentDate->format('w') != $firstDay) {
|
|
429 $this->currentDate->modify('last ' . $this->dayNames[$this->dayMap[$this->weekStart]]);
|
|
430 }
|
|
431 }
|
|
432
|
|
433 // We have a match
|
|
434 } while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours)));
|
|
435 }
|
|
436
|
|
437 /**
|
|
438 * Does the processing for advancing the iterator for monthly frequency.
|
|
439 *
|
|
440 * @return void
|
|
441 */
|
|
442 protected function nextMonthly() {
|
|
443
|
|
444 $currentDayOfMonth = $this->currentDate->format('j');
|
|
445 if (!$this->byMonthDay && !$this->byDay) {
|
|
446
|
|
447 // If the current day is higher than the 28th, rollover can
|
|
448 // occur to the next month. We Must skip these invalid
|
|
449 // entries.
|
|
450 if ($currentDayOfMonth < 29) {
|
|
451 $this->currentDate->modify('+' . $this->interval . ' months');
|
|
452 } else {
|
|
453 $increase = 0;
|
|
454 do {
|
|
455 $increase++;
|
|
456 $tempDate = clone $this->currentDate;
|
|
457 $tempDate->modify('+ ' . ($this->interval*$increase) . ' months');
|
|
458 } while ($tempDate->format('j') != $currentDayOfMonth);
|
|
459 $this->currentDate = $tempDate;
|
|
460 }
|
|
461 return;
|
|
462 }
|
|
463
|
|
464 while(true) {
|
|
465
|
|
466 $occurrences = $this->getMonthlyOccurrences();
|
|
467
|
|
468 foreach($occurrences as $occurrence) {
|
|
469
|
|
470 // The first occurrence thats higher than the current
|
|
471 // day of the month wins.
|
|
472 if ($occurrence > $currentDayOfMonth) {
|
|
473 break 2;
|
|
474 }
|
|
475
|
|
476 }
|
|
477
|
|
478 // If we made it all the way here, it means there were no
|
|
479 // valid occurrences, and we need to advance to the next
|
|
480 // month.
|
|
481 //
|
|
482 // This line does not currently work in hhvm. Temporary workaround
|
|
483 // follows:
|
|
484 // $this->currentDate->modify('first day of this month');
|
|
485 $this->currentDate = new \DateTime($this->currentDate->format('Y-m-1 H:i:s'), $this->currentDate->getTimezone());
|
|
486 // end of workaround
|
|
487 $this->currentDate->modify('+ ' . $this->interval . ' months');
|
|
488
|
|
489 // This goes to 0 because we need to start counting at the
|
|
490 // beginning.
|
|
491 $currentDayOfMonth = 0;
|
|
492
|
|
493 }
|
|
494
|
|
495 $this->currentDate->setDate($this->currentDate->format('Y'), $this->currentDate->format('n'), $occurrence);
|
|
496
|
|
497 }
|
|
498
|
|
499 /**
|
|
500 * Does the processing for advancing the iterator for yearly frequency.
|
|
501 *
|
|
502 * @return void
|
|
503 */
|
|
504 protected function nextYearly() {
|
|
505
|
|
506 $currentMonth = $this->currentDate->format('n');
|
|
507 $currentYear = $this->currentDate->format('Y');
|
|
508 $currentDayOfMonth = $this->currentDate->format('j');
|
|
509
|
|
510 // No sub-rules, so we just advance by year
|
|
511 if (!$this->byMonth) {
|
|
512
|
|
513 // Unless it was a leap day!
|
|
514 if ($currentMonth==2 && $currentDayOfMonth==29) {
|
|
515
|
|
516 $counter = 0;
|
|
517 do {
|
|
518 $counter++;
|
|
519 // Here we increase the year count by the interval, until
|
|
520 // we hit a date that's also in a leap year.
|
|
521 //
|
|
522 // We could just find the next interval that's dividable by
|
|
523 // 4, but that would ignore the rule that there's no leap
|
|
524 // year every year that's dividable by a 100, but not by
|
|
525 // 400. (1800, 1900, 2100). So we just rely on the datetime
|
|
526 // functions instead.
|
|
527 $nextDate = clone $this->currentDate;
|
|
528 $nextDate->modify('+ ' . ($this->interval*$counter) . ' years');
|
|
529 } while ($nextDate->format('n')!=2);
|
|
530 $this->currentDate = $nextDate;
|
|
531
|
|
532 return;
|
|
533
|
|
534 }
|
|
535
|
|
536 // The easiest form
|
|
537 $this->currentDate->modify('+' . $this->interval . ' years');
|
|
538 return;
|
|
539
|
|
540 }
|
|
541
|
|
542 $currentMonth = $this->currentDate->format('n');
|
|
543 $currentYear = $this->currentDate->format('Y');
|
|
544 $currentDayOfMonth = $this->currentDate->format('j');
|
|
545
|
|
546 $advancedToNewMonth = false;
|
|
547
|
|
548 // If we got a byDay or getMonthDay filter, we must first expand
|
|
549 // further.
|
|
550 if ($this->byDay || $this->byMonthDay) {
|
|
551
|
|
552 while(true) {
|
|
553
|
|
554 $occurrences = $this->getMonthlyOccurrences();
|
|
555
|
|
556 foreach($occurrences as $occurrence) {
|
|
557
|
|
558 // The first occurrence that's higher than the current
|
|
559 // day of the month wins.
|
|
560 // If we advanced to the next month or year, the first
|
|
561 // occurrence is always correct.
|
|
562 if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) {
|
|
563 break 2;
|
|
564 }
|
|
565
|
|
566 }
|
|
567
|
|
568 // If we made it here, it means we need to advance to
|
|
569 // the next month or year.
|
|
570 $currentDayOfMonth = 1;
|
|
571 $advancedToNewMonth = true;
|
|
572 do {
|
|
573
|
|
574 $currentMonth++;
|
|
575 if ($currentMonth>12) {
|
|
576 $currentYear+=$this->interval;
|
|
577 $currentMonth = 1;
|
|
578 }
|
|
579 } while (!in_array($currentMonth, $this->byMonth));
|
|
580
|
|
581 $this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth);
|
|
582
|
|
583 }
|
|
584
|
|
585 // If we made it here, it means we got a valid occurrence
|
|
586 $this->currentDate->setDate($currentYear, $currentMonth, $occurrence);
|
|
587 return;
|
|
588
|
|
589 } else {
|
|
590
|
|
591 // These are the 'byMonth' rules, if there are no byDay or
|
|
592 // byMonthDay sub-rules.
|
|
593 do {
|
|
594
|
|
595 $currentMonth++;
|
|
596 if ($currentMonth>12) {
|
|
597 $currentYear+=$this->interval;
|
|
598 $currentMonth = 1;
|
|
599 }
|
|
600 } while (!in_array($currentMonth, $this->byMonth));
|
|
601 $this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth);
|
|
602
|
|
603 return;
|
|
604
|
|
605 }
|
|
606
|
|
607 }
|
|
608
|
|
609 /* }}} */
|
|
610
|
|
611 /**
|
|
612 * This method receives a string from an RRULE property, and populates this
|
|
613 * class with all the values.
|
|
614 *
|
|
615 * @param string|array $rrule
|
|
616 * @return void
|
|
617 */
|
|
618 protected function parseRRule($rrule) {
|
|
619
|
|
620 if (is_string($rrule)) {
|
|
621 $rrule = Property\ICalendar\Recur::stringToArray($rrule);
|
|
622 }
|
|
623
|
|
624 foreach($rrule as $key=>$value) {
|
|
625
|
|
626 $key = strtoupper($key);
|
|
627 switch($key) {
|
|
628
|
|
629 case 'FREQ' :
|
|
630 $value = strtolower($value);
|
|
631 if (!in_array(
|
|
632 $value,
|
|
633 array('secondly','minutely','hourly','daily','weekly','monthly','yearly')
|
|
634 )) {
|
|
635 throw new InvalidArgumentException('Unknown value for FREQ=' . strtoupper($value));
|
|
636 }
|
|
637 $this->frequency = $value;
|
|
638 break;
|
|
639
|
|
640 case 'UNTIL' :
|
|
641 $this->until = DateTimeParser::parse($value, $this->startDate->getTimezone());
|
|
642
|
|
643 // In some cases events are generated with an UNTIL=
|
|
644 // parameter before the actual start of the event.
|
|
645 //
|
|
646 // Not sure why this is happening. We assume that the
|
|
647 // intention was that the event only recurs once.
|
|
648 //
|
|
649 // So we are modifying the parameter so our code doesn't
|
|
650 // break.
|
|
651 if($this->until < $this->startDate) {
|
|
652 $this->until = $this->startDate;
|
|
653 }
|
|
654 break;
|
|
655
|
|
656 case 'INTERVAL' :
|
|
657 // No break
|
|
658
|
|
659 case 'COUNT' :
|
|
660 $val = (int)$value;
|
|
661 if ($val < 1) {
|
|
662 throw new \InvalidArgumentException(strtoupper($key) . ' in RRULE must be a positive integer!');
|
|
663 }
|
|
664 $key = strtolower($key);
|
|
665 $this->$key = $val;
|
|
666 break;
|
|
667
|
|
668 case 'BYSECOND' :
|
|
669 $this->bySecond = (array)$value;
|
|
670 break;
|
|
671
|
|
672 case 'BYMINUTE' :
|
|
673 $this->byMinute = (array)$value;
|
|
674 break;
|
|
675
|
|
676 case 'BYHOUR' :
|
|
677 $this->byHour = (array)$value;
|
|
678 break;
|
|
679
|
|
680 case 'BYDAY' :
|
|
681 $value = (array)$value;
|
|
682 foreach($value as $part) {
|
|
683 if (!preg_match('#^ (-|\+)? ([1-5])? (MO|TU|WE|TH|FR|SA|SU) $# xi', $part)) {
|
|
684 throw new \InvalidArgumentException('Invalid part in BYDAY clause: ' . $part);
|
|
685 }
|
|
686 }
|
|
687 $this->byDay = $value;
|
|
688 break;
|
|
689
|
|
690 case 'BYMONTHDAY' :
|
|
691 $this->byMonthDay = (array)$value;
|
|
692 break;
|
|
693
|
|
694 case 'BYYEARDAY' :
|
|
695 $this->byYearDay = (array)$value;
|
|
696 break;
|
|
697
|
|
698 case 'BYWEEKNO' :
|
|
699 $this->byWeekNo = (array)$value;
|
|
700 break;
|
|
701
|
|
702 case 'BYMONTH' :
|
|
703 $this->byMonth = (array)$value;
|
|
704 break;
|
|
705
|
|
706 case 'BYSETPOS' :
|
|
707 $this->bySetPos = (array)$value;
|
|
708 break;
|
|
709
|
|
710 case 'WKST' :
|
|
711 $this->weekStart = strtoupper($value);
|
|
712 break;
|
|
713
|
|
714 default:
|
|
715 throw new \InvalidArgumentException('Not supported: ' . strtoupper($key));
|
|
716
|
|
717 }
|
|
718
|
|
719 }
|
|
720
|
|
721 }
|
|
722
|
|
723 /**
|
|
724 * Mappings between the day number and english day name.
|
|
725 *
|
|
726 * @var array
|
|
727 */
|
|
728 protected $dayNames = array(
|
|
729 0 => 'Sunday',
|
|
730 1 => 'Monday',
|
|
731 2 => 'Tuesday',
|
|
732 3 => 'Wednesday',
|
|
733 4 => 'Thursday',
|
|
734 5 => 'Friday',
|
|
735 6 => 'Saturday',
|
|
736 );
|
|
737
|
|
738 /**
|
|
739 * Returns all the occurrences for a monthly frequency with a 'byDay' or
|
|
740 * 'byMonthDay' expansion for the current month.
|
|
741 *
|
|
742 * The returned list is an array of integers with the day of month (1-31).
|
|
743 *
|
|
744 * @return array
|
|
745 */
|
|
746 protected function getMonthlyOccurrences() {
|
|
747
|
|
748 $startDate = clone $this->currentDate;
|
|
749
|
|
750 $byDayResults = array();
|
|
751
|
|
752 // Our strategy is to simply go through the byDays, advance the date to
|
|
753 // that point and add it to the results.
|
|
754 if ($this->byDay) foreach($this->byDay as $day) {
|
|
755
|
|
756 $dayName = $this->dayNames[$this->dayMap[substr($day,-2)]];
|
|
757
|
|
758
|
|
759 // Dayname will be something like 'wednesday'. Now we need to find
|
|
760 // all wednesdays in this month.
|
|
761 $dayHits = array();
|
|
762
|
|
763 // workaround for missing 'first day of the month' support in hhvm
|
|
764 $checkDate = new \DateTime($startDate->format('Y-m-1'));
|
|
765 // workaround modify always advancing the date even if the current day is a $dayName in hhvm
|
|
766 if ($checkDate->format('l') !== $dayName) {
|
|
767 $checkDate->modify($dayName);
|
|
768 }
|
|
769
|
|
770 do {
|
|
771 $dayHits[] = $checkDate->format('j');
|
|
772 $checkDate->modify('next ' . $dayName);
|
|
773 } while ($checkDate->format('n') === $startDate->format('n'));
|
|
774
|
|
775 // So now we have 'all wednesdays' for month. It is however
|
|
776 // possible that the user only really wanted the 1st, 2nd or last
|
|
777 // wednesday.
|
|
778 if (strlen($day)>2) {
|
|
779 $offset = (int)substr($day,0,-2);
|
|
780
|
|
781 if ($offset>0) {
|
|
782 // It is possible that the day does not exist, such as a
|
|
783 // 5th or 6th wednesday of the month.
|
|
784 if (isset($dayHits[$offset-1])) {
|
|
785 $byDayResults[] = $dayHits[$offset-1];
|
|
786 }
|
|
787 } else {
|
|
788
|
|
789 // if it was negative we count from the end of the array
|
|
790 $byDayResults[] = $dayHits[count($dayHits) + $offset];
|
|
791 }
|
|
792 } else {
|
|
793 // There was no counter (first, second, last wednesdays), so we
|
|
794 // just need to add the all to the list).
|
|
795 $byDayResults = array_merge($byDayResults, $dayHits);
|
|
796
|
|
797 }
|
|
798
|
|
799 }
|
|
800
|
|
801 $byMonthDayResults = array();
|
|
802 if ($this->byMonthDay) foreach($this->byMonthDay as $monthDay) {
|
|
803
|
|
804 // Removing values that are out of range for this month
|
|
805 if ($monthDay > $startDate->format('t') ||
|
|
806 $monthDay < 0-$startDate->format('t')) {
|
|
807 continue;
|
|
808 }
|
|
809 if ($monthDay>0) {
|
|
810 $byMonthDayResults[] = $monthDay;
|
|
811 } else {
|
|
812 // Negative values
|
|
813 $byMonthDayResults[] = $startDate->format('t') + 1 + $monthDay;
|
|
814 }
|
|
815 }
|
|
816
|
|
817 // If there was just byDay or just byMonthDay, they just specify our
|
|
818 // (almost) final list. If both were provided, then byDay limits the
|
|
819 // list.
|
|
820 if ($this->byMonthDay && $this->byDay) {
|
|
821 $result = array_intersect($byMonthDayResults, $byDayResults);
|
|
822 } elseif ($this->byMonthDay) {
|
|
823 $result = $byMonthDayResults;
|
|
824 } else {
|
|
825 $result = $byDayResults;
|
|
826 }
|
|
827 $result = array_unique($result);
|
|
828 sort($result, SORT_NUMERIC);
|
|
829
|
|
830 // The last thing that needs checking is the BYSETPOS. If it's set, it
|
|
831 // means only certain items in the set survive the filter.
|
|
832 if (!$this->bySetPos) {
|
|
833 return $result;
|
|
834 }
|
|
835
|
|
836 $filteredResult = array();
|
|
837 foreach($this->bySetPos as $setPos) {
|
|
838
|
|
839 if ($setPos<0) {
|
|
840 $setPos = count($result)-($setPos+1);
|
|
841 }
|
|
842 if (isset($result[$setPos-1])) {
|
|
843 $filteredResult[] = $result[$setPos-1];
|
|
844 }
|
|
845 }
|
|
846
|
|
847 sort($filteredResult, SORT_NUMERIC);
|
|
848 return $filteredResult;
|
|
849
|
|
850 }
|
|
851
|
|
852 /**
|
|
853 * Simple mapping from iCalendar day names to day numbers
|
|
854 *
|
|
855 * @var array
|
|
856 */
|
|
857 protected $dayMap = array(
|
|
858 'SU' => 0,
|
|
859 'MO' => 1,
|
|
860 'TU' => 2,
|
|
861 'WE' => 3,
|
|
862 'TH' => 4,
|
|
863 'FR' => 5,
|
|
864 'SA' => 6,
|
|
865 );
|
|
866
|
|
867 protected function getHours()
|
|
868 {
|
|
869 $recurrenceHours = array();
|
|
870 foreach($this->byHour as $byHour) {
|
|
871 $recurrenceHours[] = $byHour;
|
|
872 }
|
|
873
|
|
874 return $recurrenceHours;
|
|
875 }
|
|
876
|
|
877 protected function getDays() {
|
|
878
|
|
879 $recurrenceDays = array();
|
|
880 foreach($this->byDay as $byDay) {
|
|
881
|
|
882 // The day may be preceeded with a positive (+n) or
|
|
883 // negative (-n) integer. However, this does not make
|
|
884 // sense in 'weekly' so we ignore it here.
|
|
885 $recurrenceDays[] = $this->dayMap[substr($byDay,-2)];
|
|
886
|
|
887 }
|
|
888
|
|
889 return $recurrenceDays;
|
|
890 }
|
|
891
|
|
892 protected function getMonths() {
|
|
893
|
|
894 $recurrenceMonths = array();
|
|
895 foreach($this->byMonth as $byMonth) {
|
|
896 $recurrenceMonths[] = $byMonth;
|
|
897 }
|
|
898
|
|
899 return $recurrenceMonths;
|
|
900 }
|
|
901 }
|