Mercurial > hg > rc1
comparison plugins/libcalendaring/lib/Horde_Date_Recurrence.php @ 4:888e774ee983
libcalendar plugin as distributed
author | Charlie Root |
---|---|
date | Sat, 13 Jan 2018 08:57:56 -0500 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
3:f6fe4b6ae66a | 4:888e774ee983 |
---|---|
1 <?php | |
2 | |
3 /** | |
4 * This is a modified copy of Horde/Date/Recurrence.php (2015-01-05) | |
5 * Pull the latest version of this file from the PEAR channel of the Horde | |
6 * project at http://pear.horde.org by installing the Horde_Date package. | |
7 */ | |
8 | |
9 if (!class_exists('Horde_Date')) { | |
10 require_once(__DIR__ . '/Horde_Date.php'); | |
11 } | |
12 | |
13 // minimal required implementation of Horde_Date_Translation to avoid a huge dependency nightmare | |
14 class Horde_Date_Translation | |
15 { | |
16 function t($arg) { return $arg; } | |
17 function ngettext($sing, $plur, $num) { return ($num > 1 ? $plur : $sing); } | |
18 } | |
19 | |
20 | |
21 /** | |
22 * This file contains the Horde_Date_Recurrence class and according constants. | |
23 * | |
24 * Copyright 2007-2015 Horde LLC (http://www.horde.org/) | |
25 * | |
26 * See the enclosed file COPYING for license information (LGPL). If you | |
27 * did not receive this file, see http://www.horde.org/licenses/lgpl21. | |
28 * | |
29 * @category Horde | |
30 * @package Date | |
31 */ | |
32 | |
33 /** | |
34 * The Horde_Date_Recurrence class implements algorithms for calculating | |
35 * recurrences of events, including several recurrence types, intervals, | |
36 * exceptions, and conversion from and to vCalendar and iCalendar recurrence | |
37 * rules. | |
38 * | |
39 * All methods expecting dates as parameters accept all values that the | |
40 * Horde_Date constructor accepts, i.e. a timestamp, another Horde_Date | |
41 * object, an ISO time string or a hash. | |
42 * | |
43 * @author Jan Schneider <jan@horde.org> | |
44 * @category Horde | |
45 * @package Date | |
46 */ | |
47 class Horde_Date_Recurrence | |
48 { | |
49 /** No Recurrence **/ | |
50 const RECUR_NONE = 0; | |
51 | |
52 /** Recurs daily. */ | |
53 const RECUR_DAILY = 1; | |
54 | |
55 /** Recurs weekly. */ | |
56 const RECUR_WEEKLY = 2; | |
57 | |
58 /** Recurs monthly on the same date. */ | |
59 const RECUR_MONTHLY_DATE = 3; | |
60 | |
61 /** Recurs monthly on the same week day. */ | |
62 const RECUR_MONTHLY_WEEKDAY = 4; | |
63 | |
64 /** Recurs yearly on the same date. */ | |
65 const RECUR_YEARLY_DATE = 5; | |
66 | |
67 /** Recurs yearly on the same day of the year. */ | |
68 const RECUR_YEARLY_DAY = 6; | |
69 | |
70 /** Recurs yearly on the same week day. */ | |
71 const RECUR_YEARLY_WEEKDAY = 7; | |
72 | |
73 /** | |
74 * The start time of the event. | |
75 * | |
76 * @var Horde_Date | |
77 */ | |
78 public $start; | |
79 | |
80 /** | |
81 * The end date of the recurrence interval. | |
82 * | |
83 * @var Horde_Date | |
84 */ | |
85 public $recurEnd = null; | |
86 | |
87 /** | |
88 * The number of recurrences. | |
89 * | |
90 * @var integer | |
91 */ | |
92 public $recurCount = null; | |
93 | |
94 /** | |
95 * The type of recurrence this event follows. RECUR_* constant. | |
96 * | |
97 * @var integer | |
98 */ | |
99 public $recurType = self::RECUR_NONE; | |
100 | |
101 /** | |
102 * The length of time between recurrences. The time unit depends on the | |
103 * recurrence type. | |
104 * | |
105 * @var integer | |
106 */ | |
107 public $recurInterval = 1; | |
108 | |
109 /** | |
110 * Any additional recurrence data. | |
111 * | |
112 * @var integer | |
113 */ | |
114 public $recurData = null; | |
115 | |
116 /** | |
117 * BYDAY recurrence number | |
118 * | |
119 * @var integer | |
120 */ | |
121 public $recurNthDay = null; | |
122 | |
123 /** | |
124 * BYMONTH recurrence data | |
125 * | |
126 * @var array | |
127 */ | |
128 public $recurMonths = array(); | |
129 | |
130 /** | |
131 * RDATE recurrence values | |
132 * | |
133 * @var array | |
134 */ | |
135 public $rdates = array(); | |
136 | |
137 /** | |
138 * All the exceptions from recurrence for this event. | |
139 * | |
140 * @var array | |
141 */ | |
142 public $exceptions = array(); | |
143 | |
144 /** | |
145 * All the dates this recurrence has been marked as completed. | |
146 * | |
147 * @var array | |
148 */ | |
149 public $completions = array(); | |
150 | |
151 /** | |
152 * Constructor. | |
153 * | |
154 * @param Horde_Date $start Start of the recurring event. | |
155 */ | |
156 public function __construct($start) | |
157 { | |
158 $this->start = new Horde_Date($start); | |
159 } | |
160 | |
161 /** | |
162 * Resets the class properties. | |
163 */ | |
164 public function reset() | |
165 { | |
166 $this->recurEnd = null; | |
167 $this->recurCount = null; | |
168 $this->recurType = self::RECUR_NONE; | |
169 $this->recurInterval = 1; | |
170 $this->recurData = null; | |
171 $this->exceptions = array(); | |
172 $this->completions = array(); | |
173 } | |
174 | |
175 /** | |
176 * Checks if this event recurs on a given day of the week. | |
177 * | |
178 * @param integer $dayMask A mask consisting of Horde_Date::MASK_* | |
179 * constants specifying the day(s) to check. | |
180 * | |
181 * @return boolean True if this event recurs on the given day(s). | |
182 */ | |
183 public function recurOnDay($dayMask) | |
184 { | |
185 return ($this->recurData & $dayMask); | |
186 } | |
187 | |
188 /** | |
189 * Specifies the days this event recurs on. | |
190 * | |
191 * @param integer $dayMask A mask consisting of Horde_Date::MASK_* | |
192 * constants specifying the day(s) to recur on. | |
193 */ | |
194 public function setRecurOnDay($dayMask) | |
195 { | |
196 $this->recurData = $dayMask; | |
197 } | |
198 | |
199 /** | |
200 * | |
201 * @param integer $nthDay The nth weekday of month to repeat events on | |
202 */ | |
203 public function setRecurNthWeekday($nth) | |
204 { | |
205 $this->recurNthDay = (int)$nth; | |
206 } | |
207 | |
208 /** | |
209 * | |
210 * @return integer The nth weekday of month to repeat events. | |
211 */ | |
212 public function getRecurNthWeekday() | |
213 { | |
214 return isset($this->recurNthDay) ? $this->recurNthDay : ceil($this->start->mday / 7); | |
215 } | |
216 | |
217 /** | |
218 * Specifies the months for yearly (weekday) recurrence | |
219 * | |
220 * @param array $months List of months (integers) this event recurs on. | |
221 */ | |
222 function setRecurByMonth($months) | |
223 { | |
224 $this->recurMonths = (array)$months; | |
225 } | |
226 | |
227 /** | |
228 * Returns a list of months this yearly event recurs on | |
229 * | |
230 * @return array List of months (integers) this event recurs on. | |
231 */ | |
232 function getRecurByMonth() | |
233 { | |
234 return $this->recurMonths; | |
235 } | |
236 | |
237 /** | |
238 * Returns the days this event recurs on. | |
239 * | |
240 * @return integer A mask consisting of Horde_Date::MASK_* constants | |
241 * specifying the day(s) this event recurs on. | |
242 */ | |
243 public function getRecurOnDays() | |
244 { | |
245 return $this->recurData; | |
246 } | |
247 | |
248 /** | |
249 * Returns whether this event has a specific recurrence type. | |
250 * | |
251 * @param integer $recurrence RECUR_* constant of the | |
252 * recurrence type to check for. | |
253 * | |
254 * @return boolean True if the event has the specified recurrence type. | |
255 */ | |
256 public function hasRecurType($recurrence) | |
257 { | |
258 return ($recurrence == $this->recurType); | |
259 } | |
260 | |
261 /** | |
262 * Sets a recurrence type for this event. | |
263 * | |
264 * @param integer $recurrence A RECUR_* constant. | |
265 */ | |
266 public function setRecurType($recurrence) | |
267 { | |
268 $this->recurType = $recurrence; | |
269 } | |
270 | |
271 /** | |
272 * Returns recurrence type of this event. | |
273 * | |
274 * @return integer A RECUR_* constant. | |
275 */ | |
276 public function getRecurType() | |
277 { | |
278 return $this->recurType; | |
279 } | |
280 | |
281 /** | |
282 * Returns a description of this event's recurring type. | |
283 * | |
284 * @return string Human readable recurring type. | |
285 */ | |
286 public function getRecurName() | |
287 { | |
288 switch ($this->getRecurType()) { | |
289 case self::RECUR_NONE: | |
290 return Horde_Date_Translation::t("No recurrence"); | |
291 case self::RECUR_DAILY: | |
292 return Horde_Date_Translation::t("Daily"); | |
293 case self::RECUR_WEEKLY: | |
294 return Horde_Date_Translation::t("Weekly"); | |
295 case self::RECUR_MONTHLY_DATE: | |
296 case self::RECUR_MONTHLY_WEEKDAY: | |
297 return Horde_Date_Translation::t("Monthly"); | |
298 case self::RECUR_YEARLY_DATE: | |
299 case self::RECUR_YEARLY_DAY: | |
300 case self::RECUR_YEARLY_WEEKDAY: | |
301 return Horde_Date_Translation::t("Yearly"); | |
302 } | |
303 } | |
304 | |
305 /** | |
306 * Sets the length of time between recurrences of this event. | |
307 * | |
308 * @param integer $interval The time between recurrences. | |
309 */ | |
310 public function setRecurInterval($interval) | |
311 { | |
312 if ($interval > 0) { | |
313 $this->recurInterval = $interval; | |
314 } | |
315 } | |
316 | |
317 /** | |
318 * Retrieves the length of time between recurrences of this event. | |
319 * | |
320 * @return integer The number of seconds between recurrences. | |
321 */ | |
322 public function getRecurInterval() | |
323 { | |
324 return $this->recurInterval; | |
325 } | |
326 | |
327 /** | |
328 * Sets the number of recurrences of this event. | |
329 * | |
330 * @param integer $count The number of recurrences. | |
331 */ | |
332 public function setRecurCount($count) | |
333 { | |
334 if ($count > 0) { | |
335 $this->recurCount = (int)$count; | |
336 // Recurrence counts and end dates are mutually exclusive. | |
337 $this->recurEnd = null; | |
338 } else { | |
339 $this->recurCount = null; | |
340 } | |
341 } | |
342 | |
343 /** | |
344 * Retrieves the number of recurrences of this event. | |
345 * | |
346 * @return integer The number recurrences. | |
347 */ | |
348 public function getRecurCount() | |
349 { | |
350 return $this->recurCount; | |
351 } | |
352 | |
353 /** | |
354 * Returns whether this event has a recurrence with a fixed count. | |
355 * | |
356 * @return boolean True if this recurrence has a fixed count. | |
357 */ | |
358 public function hasRecurCount() | |
359 { | |
360 return isset($this->recurCount); | |
361 } | |
362 | |
363 /** | |
364 * Sets the start date of the recurrence interval. | |
365 * | |
366 * @param Horde_Date $start The recurrence start. | |
367 */ | |
368 public function setRecurStart($start) | |
369 { | |
370 $this->start = clone $start; | |
371 } | |
372 | |
373 /** | |
374 * Retrieves the start date of the recurrence interval. | |
375 * | |
376 * @return Horde_Date The recurrence start. | |
377 */ | |
378 public function getRecurStart() | |
379 { | |
380 return $this->start; | |
381 } | |
382 | |
383 /** | |
384 * Sets the end date of the recurrence interval. | |
385 * | |
386 * @param Horde_Date $end The recurrence end. | |
387 */ | |
388 public function setRecurEnd($end) | |
389 { | |
390 if (!empty($end)) { | |
391 // Recurrence counts and end dates are mutually exclusive. | |
392 $this->recurCount = null; | |
393 $this->recurEnd = clone $end; | |
394 } else { | |
395 $this->recurEnd = $end; | |
396 } | |
397 } | |
398 | |
399 /** | |
400 * Retrieves the end date of the recurrence interval. | |
401 * | |
402 * @return Horde_Date The recurrence end. | |
403 */ | |
404 public function getRecurEnd() | |
405 { | |
406 return $this->recurEnd; | |
407 } | |
408 | |
409 /** | |
410 * Returns whether this event has a recurrence end. | |
411 * | |
412 * @return boolean True if this recurrence ends. | |
413 */ | |
414 public function hasRecurEnd() | |
415 { | |
416 return isset($this->recurEnd) && isset($this->recurEnd->year) && | |
417 $this->recurEnd->year != 9999; | |
418 } | |
419 | |
420 /** | |
421 * Finds the next recurrence of this event that's after $afterDate. | |
422 * | |
423 * @param Horde_Date|string $after Return events after this date. | |
424 * | |
425 * @return Horde_Date|boolean The date of the next recurrence or false | |
426 * if the event does not recur after | |
427 * $afterDate. | |
428 */ | |
429 public function nextRecurrence($after) | |
430 { | |
431 if (!($after instanceof Horde_Date)) { | |
432 $after = new Horde_Date($after); | |
433 } else { | |
434 $after = clone($after); | |
435 } | |
436 | |
437 // Make sure $after and $this->start are in the same TZ | |
438 $after->setTimezone($this->start->timezone); | |
439 if ($this->start->compareDateTime($after) >= 0) { | |
440 return clone $this->start; | |
441 } | |
442 | |
443 if ($this->recurInterval == 0 && empty($this->rdates)) { | |
444 return false; | |
445 } | |
446 | |
447 switch ($this->getRecurType()) { | |
448 case self::RECUR_DAILY: | |
449 $diff = $this->start->diff($after); | |
450 $recur = ceil($diff / $this->recurInterval); | |
451 if ($this->recurCount && $recur >= $this->recurCount) { | |
452 return false; | |
453 } | |
454 | |
455 $recur *= $this->recurInterval; | |
456 $next = $this->start->add(array('day' => $recur)); | |
457 if ((!$this->hasRecurEnd() || | |
458 $next->compareDateTime($this->recurEnd) <= 0) && | |
459 $next->compareDateTime($after) >= 0) { | |
460 return $next; | |
461 } | |
462 | |
463 break; | |
464 | |
465 case self::RECUR_WEEKLY: | |
466 if (empty($this->recurData)) { | |
467 return false; | |
468 } | |
469 | |
470 $start_week = Horde_Date_Utils::firstDayOfWeek($this->start->format('W'), | |
471 $this->start->year); | |
472 $start_week->timezone = $this->start->timezone; | |
473 $start_week->hour = $this->start->hour; | |
474 $start_week->min = $this->start->min; | |
475 $start_week->sec = $this->start->sec; | |
476 | |
477 // Make sure we are not at the ISO-8601 first week of year while | |
478 // still in month 12...OR in the ISO-8601 last week of year while | |
479 // in month 1 and adjust the year accordingly. | |
480 $week = $after->format('W'); | |
481 if ($week == 1 && $after->month == 12) { | |
482 $theYear = $after->year + 1; | |
483 } elseif ($week >= 52 && $after->month == 1) { | |
484 $theYear = $after->year - 1; | |
485 } else { | |
486 $theYear = $after->year; | |
487 } | |
488 | |
489 $after_week = Horde_Date_Utils::firstDayOfWeek($week, $theYear); | |
490 $after_week->timezone = $this->start->timezone; | |
491 $after_week_end = clone $after_week; | |
492 $after_week_end->mday += 7; | |
493 | |
494 $diff = $start_week->diff($after_week); | |
495 $interval = $this->recurInterval * 7; | |
496 $repeats = floor($diff / $interval); | |
497 if ($diff % $interval < 7) { | |
498 $recur = $diff; | |
499 } else { | |
500 /** | |
501 * If the after_week is not in the first week interval the | |
502 * search needs to skip ahead a complete interval. The way it is | |
503 * calculated here means that an event that occurs every second | |
504 * week on Monday and Wednesday with the event actually starting | |
505 * on Tuesday or Wednesday will only have one incidence in the | |
506 * first week. | |
507 */ | |
508 $recur = $interval * ($repeats + 1); | |
509 } | |
510 | |
511 if ($this->hasRecurCount()) { | |
512 $recurrences = 0; | |
513 /** | |
514 * Correct the number of recurrences by the number of events | |
515 * that lay between the start of the start week and the | |
516 * recurrence start. | |
517 */ | |
518 $next = clone $start_week; | |
519 while ($next->compareDateTime($this->start) < 0) { | |
520 if ($this->recurOnDay((int)pow(2, $next->dayOfWeek()))) { | |
521 $recurrences--; | |
522 } | |
523 ++$next->mday; | |
524 } | |
525 if ($repeats > 0) { | |
526 $weekdays = $this->recurData; | |
527 $total_recurrences_per_week = 0; | |
528 while ($weekdays > 0) { | |
529 if ($weekdays % 2) { | |
530 $total_recurrences_per_week++; | |
531 } | |
532 $weekdays = ($weekdays - ($weekdays % 2)) / 2; | |
533 } | |
534 $recurrences += $total_recurrences_per_week * $repeats; | |
535 } | |
536 } | |
537 | |
538 $next = clone $start_week; | |
539 $next->mday += $recur; | |
540 while ($next->compareDateTime($after) < 0 && | |
541 $next->compareDateTime($after_week_end) < 0) { | |
542 if ($this->hasRecurCount() | |
543 && $next->compareDateTime($after) < 0 | |
544 && $this->recurOnDay((int)pow(2, $next->dayOfWeek()))) { | |
545 $recurrences++; | |
546 } | |
547 ++$next->mday; | |
548 } | |
549 if ($this->hasRecurCount() && | |
550 $recurrences >= $this->recurCount) { | |
551 return false; | |
552 } | |
553 if (!$this->hasRecurEnd() || | |
554 $next->compareDateTime($this->recurEnd) <= 0) { | |
555 if ($next->compareDateTime($after_week_end) >= 0) { | |
556 return $this->nextRecurrence($after_week_end); | |
557 } | |
558 while (!$this->recurOnDay((int)pow(2, $next->dayOfWeek())) && | |
559 $next->compareDateTime($after_week_end) < 0) { | |
560 ++$next->mday; | |
561 } | |
562 if (!$this->hasRecurEnd() || | |
563 $next->compareDateTime($this->recurEnd) <= 0) { | |
564 if ($next->compareDateTime($after_week_end) >= 0) { | |
565 return $this->nextRecurrence($after_week_end); | |
566 } else { | |
567 return $next; | |
568 } | |
569 } | |
570 } | |
571 break; | |
572 | |
573 case self::RECUR_MONTHLY_DATE: | |
574 $start = clone $this->start; | |
575 if ($after->compareDateTime($start) < 0) { | |
576 $after = clone $start; | |
577 } else { | |
578 $after = clone $after; | |
579 } | |
580 | |
581 // If we're starting past this month's recurrence of the event, | |
582 // look in the next month on the day the event recurs. | |
583 if ($after->mday > $start->mday) { | |
584 ++$after->month; | |
585 $after->mday = $start->mday; | |
586 } | |
587 | |
588 // Adjust $start to be the first match. | |
589 $offset = ($after->month - $start->month) + ($after->year - $start->year) * 12; | |
590 $offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval; | |
591 | |
592 if ($this->recurCount && | |
593 ($offset / $this->recurInterval) >= $this->recurCount) { | |
594 return false; | |
595 } | |
596 $start->month += $offset; | |
597 $count = $offset / $this->recurInterval; | |
598 | |
599 do { | |
600 if ($this->recurCount && | |
601 $count++ >= $this->recurCount) { | |
602 return false; | |
603 } | |
604 | |
605 // Bail if we've gone past the end of recurrence. | |
606 if ($this->hasRecurEnd() && | |
607 $this->recurEnd->compareDateTime($start) < 0) { | |
608 return false; | |
609 } | |
610 if ($start->isValid()) { | |
611 return $start; | |
612 } | |
613 | |
614 // If the interval is 12, and the date isn't valid, then we | |
615 // need to see if February 29th is an option. If not, then the | |
616 // event will _never_ recur, and we need to stop checking to | |
617 // avoid an infinite loop. | |
618 if ($this->recurInterval == 12 && ($start->month != 2 || $start->mday > 29)) { | |
619 return false; | |
620 } | |
621 | |
622 // Add the recurrence interval. | |
623 $start->month += $this->recurInterval; | |
624 } while (true); | |
625 | |
626 break; | |
627 | |
628 case self::RECUR_MONTHLY_WEEKDAY: | |
629 // Start with the start date of the event. | |
630 $estart = clone $this->start; | |
631 | |
632 // What day of the week, and week of the month, do we recur on? | |
633 if (isset($this->recurNthDay)) { | |
634 $nth = $this->recurNthDay; | |
635 $weekday = log($this->recurData, 2); | |
636 } else { | |
637 $nth = ceil($this->start->mday / 7); | |
638 $weekday = $estart->dayOfWeek(); | |
639 } | |
640 | |
641 // Adjust $estart to be the first candidate. | |
642 $offset = ($after->month - $estart->month) + ($after->year - $estart->year) * 12; | |
643 $offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval; | |
644 | |
645 // Adjust our working date until it's after $after. | |
646 $estart->month += $offset - $this->recurInterval; | |
647 | |
648 $count = $offset / $this->recurInterval; | |
649 do { | |
650 if ($this->recurCount && | |
651 $count++ >= $this->recurCount) { | |
652 return false; | |
653 } | |
654 | |
655 $estart->month += $this->recurInterval; | |
656 | |
657 $next = clone $estart; | |
658 $next->setNthWeekday($weekday, $nth); | |
659 | |
660 if ($next->month != $estart->month) { | |
661 // We're already in the next month. | |
662 continue; | |
663 } | |
664 if ($next->compareDateTime($after) < 0) { | |
665 // We haven't made it past $after yet, try again. | |
666 continue; | |
667 } | |
668 if ($this->hasRecurEnd() && | |
669 $next->compareDateTime($this->recurEnd) > 0) { | |
670 // We've gone past the end of recurrence; we can give up | |
671 // now. | |
672 return false; | |
673 } | |
674 | |
675 // We have a candidate to return. | |
676 break; | |
677 } while (true); | |
678 | |
679 return $next; | |
680 | |
681 case self::RECUR_YEARLY_DATE: | |
682 // Start with the start date of the event. | |
683 $estart = clone $this->start; | |
684 $after = clone $after; | |
685 | |
686 if ($after->month > $estart->month || | |
687 ($after->month == $estart->month && $after->mday > $estart->mday)) { | |
688 ++$after->year; | |
689 $after->month = $estart->month; | |
690 $after->mday = $estart->mday; | |
691 } | |
692 | |
693 // Seperate case here for February 29th | |
694 if ($estart->month == 2 && $estart->mday == 29) { | |
695 while (!Horde_Date_Utils::isLeapYear($after->year)) { | |
696 ++$after->year; | |
697 } | |
698 } | |
699 | |
700 // Adjust $estart to be the first candidate. | |
701 $offset = $after->year - $estart->year; | |
702 if ($offset > 0) { | |
703 $offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval; | |
704 $estart->year += $offset; | |
705 } | |
706 | |
707 // We've gone past the end of recurrence; give up. | |
708 if ($this->recurCount && | |
709 $offset >= $this->recurCount) { | |
710 return false; | |
711 } | |
712 if ($this->hasRecurEnd() && | |
713 $this->recurEnd->compareDateTime($estart) < 0) { | |
714 return false; | |
715 } | |
716 | |
717 return $estart; | |
718 | |
719 case self::RECUR_YEARLY_DAY: | |
720 // Check count first. | |
721 $dayofyear = $this->start->dayOfYear(); | |
722 $count = ($after->year - $this->start->year) / $this->recurInterval + 1; | |
723 if ($this->recurCount && | |
724 ($count > $this->recurCount || | |
725 ($count == $this->recurCount && | |
726 $after->dayOfYear() > $dayofyear))) { | |
727 return false; | |
728 } | |
729 | |
730 // Start with a rough interval. | |
731 $estart = clone $this->start; | |
732 $estart->year += floor($count - 1) * $this->recurInterval; | |
733 | |
734 // Now add the difference to the required day of year. | |
735 $estart->mday += $dayofyear - $estart->dayOfYear(); | |
736 | |
737 // Add an interval if the estimation was wrong. | |
738 if ($estart->compareDate($after) < 0) { | |
739 $estart->year += $this->recurInterval; | |
740 $estart->mday += $dayofyear - $estart->dayOfYear(); | |
741 } | |
742 | |
743 // We've gone past the end of recurrence; give up. | |
744 if ($this->hasRecurEnd() && | |
745 $this->recurEnd->compareDateTime($estart) < 0) { | |
746 return false; | |
747 } | |
748 | |
749 return $estart; | |
750 | |
751 case self::RECUR_YEARLY_WEEKDAY: | |
752 // Start with the start date of the event. | |
753 $estart = clone $this->start; | |
754 | |
755 // What day of the week, and week of the month, do we recur on? | |
756 if (isset($this->recurNthDay)) { | |
757 $nth = $this->recurNthDay; | |
758 $weekday = log($this->recurData, 2); | |
759 } else { | |
760 $nth = ceil($this->start->mday / 7); | |
761 $weekday = $estart->dayOfWeek(); | |
762 } | |
763 | |
764 // Adjust $estart to be the first candidate. | |
765 $offset = floor(($after->year - $estart->year + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval; | |
766 | |
767 // Adjust our working date until it's after $after. | |
768 $estart->year += $offset - $this->recurInterval; | |
769 | |
770 $count = $offset / $this->recurInterval; | |
771 do { | |
772 if ($this->recurCount && | |
773 $count++ >= $this->recurCount) { | |
774 return false; | |
775 } | |
776 | |
777 $estart->year += $this->recurInterval; | |
778 | |
779 $next = clone $estart; | |
780 $next->setNthWeekday($weekday, $nth); | |
781 | |
782 if ($next->compareDateTime($after) < 0) { | |
783 // We haven't made it past $after yet, try again. | |
784 continue; | |
785 } | |
786 if ($this->hasRecurEnd() && | |
787 $next->compareDateTime($this->recurEnd) > 0) { | |
788 // We've gone past the end of recurrence; we can give up | |
789 // now. | |
790 return false; | |
791 } | |
792 | |
793 // We have a candidate to return. | |
794 break; | |
795 } while (true); | |
796 | |
797 return $next; | |
798 } | |
799 | |
800 // fall-back to RDATE properties | |
801 if (!empty($this->rdates)) { | |
802 $next = clone $this->start; | |
803 foreach ($this->rdates as $rdate) { | |
804 $next->year = $rdate->year; | |
805 $next->month = $rdate->month; | |
806 $next->mday = $rdate->mday; | |
807 if ($next->compareDateTime($after) >= 0) { | |
808 return $next; | |
809 } | |
810 } | |
811 } | |
812 | |
813 // We didn't find anything, the recurType was bad, or something else | |
814 // went wrong - return false. | |
815 return false; | |
816 } | |
817 | |
818 /** | |
819 * Returns whether this event has any date that matches the recurrence | |
820 * rules and is not an exception. | |
821 * | |
822 * @return boolean True if an active recurrence exists. | |
823 */ | |
824 public function hasActiveRecurrence() | |
825 { | |
826 if (!$this->hasRecurEnd()) { | |
827 return true; | |
828 } | |
829 | |
830 $next = $this->nextRecurrence(new Horde_Date($this->start)); | |
831 while (is_object($next)) { | |
832 if (!$this->hasException($next->year, $next->month, $next->mday) && | |
833 !$this->hasCompletion($next->year, $next->month, $next->mday)) { | |
834 return true; | |
835 } | |
836 | |
837 $next = $this->nextRecurrence($next->add(array('day' => 1))); | |
838 } | |
839 | |
840 return false; | |
841 } | |
842 | |
843 /** | |
844 * Returns the next active recurrence. | |
845 * | |
846 * @param Horde_Date $afterDate Return events after this date. | |
847 * | |
848 * @return Horde_Date|boolean The date of the next active | |
849 * recurrence or false if the event | |
850 * has no active recurrence after | |
851 * $afterDate. | |
852 */ | |
853 public function nextActiveRecurrence($afterDate) | |
854 { | |
855 $next = $this->nextRecurrence($afterDate); | |
856 while (is_object($next)) { | |
857 if (!$this->hasException($next->year, $next->month, $next->mday) && | |
858 !$this->hasCompletion($next->year, $next->month, $next->mday)) { | |
859 return $next; | |
860 } | |
861 $next->mday++; | |
862 $next = $this->nextRecurrence($next); | |
863 } | |
864 | |
865 return false; | |
866 } | |
867 | |
868 /** | |
869 * Adds an absolute recurrence date. | |
870 * | |
871 * @param integer $year The year of the instance. | |
872 * @param integer $month The month of the instance. | |
873 * @param integer $mday The day of the month of the instance. | |
874 */ | |
875 public function addRDate($year, $month, $mday) | |
876 { | |
877 $this->rdates[] = new Horde_Date($year, $month, $mday); | |
878 } | |
879 | |
880 /** | |
881 * Adds an exception to a recurring event. | |
882 * | |
883 * @param integer $year The year of the execption. | |
884 * @param integer $month The month of the execption. | |
885 * @param integer $mday The day of the month of the exception. | |
886 */ | |
887 public function addException($year, $month, $mday) | |
888 { | |
889 $key = sprintf('%04d%02d%02d', $year, $month, $mday); | |
890 if (array_search($key, $this->exceptions) === false) { | |
891 $this->exceptions[] = sprintf('%04d%02d%02d', $year, $month, $mday); | |
892 } | |
893 } | |
894 | |
895 /** | |
896 * Deletes an exception from a recurring event. | |
897 * | |
898 * @param integer $year The year of the execption. | |
899 * @param integer $month The month of the execption. | |
900 * @param integer $mday The day of the month of the exception. | |
901 */ | |
902 public function deleteException($year, $month, $mday) | |
903 { | |
904 $key = array_search(sprintf('%04d%02d%02d', $year, $month, $mday), $this->exceptions); | |
905 if ($key !== false) { | |
906 unset($this->exceptions[$key]); | |
907 } | |
908 } | |
909 | |
910 /** | |
911 * Checks if an exception exists for a given reccurence of an event. | |
912 * | |
913 * @param integer $year The year of the reucrance. | |
914 * @param integer $month The month of the reucrance. | |
915 * @param integer $mday The day of the month of the reucrance. | |
916 * | |
917 * @return boolean True if an exception exists for the given date. | |
918 */ | |
919 public function hasException($year, $month, $mday) | |
920 { | |
921 return in_array(sprintf('%04d%02d%02d', $year, $month, $mday), | |
922 $this->getExceptions()); | |
923 } | |
924 | |
925 /** | |
926 * Retrieves all the exceptions for this event. | |
927 * | |
928 * @return array Array containing the dates of all the exceptions in | |
929 * YYYYMMDD form. | |
930 */ | |
931 public function getExceptions() | |
932 { | |
933 return $this->exceptions; | |
934 } | |
935 | |
936 /** | |
937 * Adds a completion to a recurring event. | |
938 * | |
939 * @param integer $year The year of the execption. | |
940 * @param integer $month The month of the execption. | |
941 * @param integer $mday The day of the month of the completion. | |
942 */ | |
943 public function addCompletion($year, $month, $mday) | |
944 { | |
945 $this->completions[] = sprintf('%04d%02d%02d', $year, $month, $mday); | |
946 } | |
947 | |
948 /** | |
949 * Deletes a completion from a recurring event. | |
950 * | |
951 * @param integer $year The year of the execption. | |
952 * @param integer $month The month of the execption. | |
953 * @param integer $mday The day of the month of the completion. | |
954 */ | |
955 public function deleteCompletion($year, $month, $mday) | |
956 { | |
957 $key = array_search(sprintf('%04d%02d%02d', $year, $month, $mday), $this->completions); | |
958 if ($key !== false) { | |
959 unset($this->completions[$key]); | |
960 } | |
961 } | |
962 | |
963 /** | |
964 * Checks if a completion exists for a given reccurence of an event. | |
965 * | |
966 * @param integer $year The year of the reucrance. | |
967 * @param integer $month The month of the recurrance. | |
968 * @param integer $mday The day of the month of the recurrance. | |
969 * | |
970 * @return boolean True if a completion exists for the given date. | |
971 */ | |
972 public function hasCompletion($year, $month, $mday) | |
973 { | |
974 return in_array(sprintf('%04d%02d%02d', $year, $month, $mday), | |
975 $this->getCompletions()); | |
976 } | |
977 | |
978 /** | |
979 * Retrieves all the completions for this event. | |
980 * | |
981 * @return array Array containing the dates of all the completions in | |
982 * YYYYMMDD form. | |
983 */ | |
984 public function getCompletions() | |
985 { | |
986 return $this->completions; | |
987 } | |
988 | |
989 /** | |
990 * Parses a vCalendar 1.0 recurrence rule. | |
991 * | |
992 * @link http://www.imc.org/pdi/vcal-10.txt | |
993 * @link http://www.shuchow.com/vCalAddendum.html | |
994 * | |
995 * @param string $rrule A vCalendar 1.0 conform RRULE value. | |
996 */ | |
997 public function fromRRule10($rrule) | |
998 { | |
999 $this->reset(); | |
1000 | |
1001 if (!$rrule) { | |
1002 return; | |
1003 } | |
1004 | |
1005 if (!preg_match('/([A-Z]+)(\d+)?(.*)/', $rrule, $matches)) { | |
1006 // No recurrence data - event does not recur. | |
1007 $this->setRecurType(self::RECUR_NONE); | |
1008 } | |
1009 | |
1010 // Always default the recurInterval to 1. | |
1011 $this->setRecurInterval(!empty($matches[2]) ? $matches[2] : 1); | |
1012 | |
1013 $remainder = trim($matches[3]); | |
1014 | |
1015 switch ($matches[1]) { | |
1016 case 'D': | |
1017 $this->setRecurType(self::RECUR_DAILY); | |
1018 break; | |
1019 | |
1020 case 'W': | |
1021 $this->setRecurType(self::RECUR_WEEKLY); | |
1022 if (!empty($remainder)) { | |
1023 $mask = 0; | |
1024 while (preg_match('/^ ?[A-Z]{2} ?/', $remainder, $matches)) { | |
1025 $day = trim($matches[0]); | |
1026 $remainder = substr($remainder, strlen($matches[0])); | |
1027 $mask |= $maskdays[$day]; | |
1028 } | |
1029 $this->setRecurOnDay($mask); | |
1030 } else { | |
1031 // Recur on the day of the week of the original recurrence. | |
1032 $maskdays = array( | |
1033 Horde_Date::DATE_SUNDAY => Horde_Date::MASK_SUNDAY, | |
1034 Horde_Date::DATE_MONDAY => Horde_Date::MASK_MONDAY, | |
1035 Horde_Date::DATE_TUESDAY => Horde_Date::MASK_TUESDAY, | |
1036 Horde_Date::DATE_WEDNESDAY => Horde_Date::MASK_WEDNESDAY, | |
1037 Horde_Date::DATE_THURSDAY => Horde_Date::MASK_THURSDAY, | |
1038 Horde_Date::DATE_FRIDAY => Horde_Date::MASK_FRIDAY, | |
1039 Horde_Date::DATE_SATURDAY => Horde_Date::MASK_SATURDAY, | |
1040 ); | |
1041 $this->setRecurOnDay($maskdays[$this->start->dayOfWeek()]); | |
1042 } | |
1043 break; | |
1044 | |
1045 case 'MP': | |
1046 $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY); | |
1047 break; | |
1048 | |
1049 case 'MD': | |
1050 $this->setRecurType(self::RECUR_MONTHLY_DATE); | |
1051 break; | |
1052 | |
1053 case 'YM': | |
1054 $this->setRecurType(self::RECUR_YEARLY_DATE); | |
1055 break; | |
1056 | |
1057 case 'YD': | |
1058 $this->setRecurType(self::RECUR_YEARLY_DAY); | |
1059 break; | |
1060 } | |
1061 | |
1062 // We don't support modifiers at the moment, strip them. | |
1063 while ($remainder && !preg_match('/^(#\d+|\d{8})($| |T\d{6})/', $remainder)) { | |
1064 $remainder = substr($remainder, 1); | |
1065 } | |
1066 if (!empty($remainder)) { | |
1067 if (strpos($remainder, '#') === 0) { | |
1068 $this->setRecurCount(substr($remainder, 1)); | |
1069 } else { | |
1070 list($year, $month, $mday, $hour, $min, $sec, $tz) = | |
1071 sscanf($remainder, '%04d%02d%02dT%02d%02d%02d%s'); | |
1072 $this->setRecurEnd(new Horde_Date(array('year' => $year, | |
1073 'month' => $month, | |
1074 'mday' => $mday, | |
1075 'hour' => $hour, | |
1076 'min' => $min, | |
1077 'sec' => $sec), | |
1078 $tz == 'Z' ? 'UTC' : $this->start->timezone)); | |
1079 } | |
1080 } | |
1081 } | |
1082 | |
1083 /** | |
1084 * Creates a vCalendar 1.0 recurrence rule. | |
1085 * | |
1086 * @link http://www.imc.org/pdi/vcal-10.txt | |
1087 * @link http://www.shuchow.com/vCalAddendum.html | |
1088 * | |
1089 * @param Horde_Icalendar $calendar A Horde_Icalendar object instance. | |
1090 * | |
1091 * @return string A vCalendar 1.0 conform RRULE value. | |
1092 */ | |
1093 public function toRRule10($calendar) | |
1094 { | |
1095 switch ($this->recurType) { | |
1096 case self::RECUR_NONE: | |
1097 return ''; | |
1098 | |
1099 case self::RECUR_DAILY: | |
1100 $rrule = 'D' . $this->recurInterval; | |
1101 break; | |
1102 | |
1103 case self::RECUR_WEEKLY: | |
1104 $rrule = 'W' . $this->recurInterval; | |
1105 $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'); | |
1106 | |
1107 for ($i = 0; $i <= 7; ++$i) { | |
1108 if ($this->recurOnDay(pow(2, $i))) { | |
1109 $rrule .= ' ' . $vcaldays[$i]; | |
1110 } | |
1111 } | |
1112 break; | |
1113 | |
1114 case self::RECUR_MONTHLY_DATE: | |
1115 $rrule = 'MD' . $this->recurInterval . ' ' . trim($this->start->mday); | |
1116 break; | |
1117 | |
1118 case self::RECUR_MONTHLY_WEEKDAY: | |
1119 $nth_weekday = (int)($this->start->mday / 7); | |
1120 if (($this->start->mday % 7) > 0) { | |
1121 $nth_weekday++; | |
1122 } | |
1123 | |
1124 $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'); | |
1125 $rrule = 'MP' . $this->recurInterval . ' ' . $nth_weekday . '+ ' . $vcaldays[$this->start->dayOfWeek()]; | |
1126 | |
1127 break; | |
1128 | |
1129 case self::RECUR_YEARLY_DATE: | |
1130 $rrule = 'YM' . $this->recurInterval . ' ' . trim($this->start->month); | |
1131 break; | |
1132 | |
1133 case self::RECUR_YEARLY_DAY: | |
1134 $rrule = 'YD' . $this->recurInterval . ' ' . $this->start->dayOfYear(); | |
1135 break; | |
1136 | |
1137 default: | |
1138 return ''; | |
1139 } | |
1140 | |
1141 if ($this->hasRecurEnd()) { | |
1142 $recurEnd = clone $this->recurEnd; | |
1143 return $rrule . ' ' . $calendar->_exportDateTime($recurEnd); | |
1144 } | |
1145 | |
1146 return $rrule . ' #' . (int)$this->getRecurCount(); | |
1147 } | |
1148 | |
1149 /** | |
1150 * Parses an iCalendar 2.0 recurrence rule. | |
1151 * | |
1152 * @link http://rfc.net/rfc2445.html#s4.3.10 | |
1153 * @link http://rfc.net/rfc2445.html#s4.8.5 | |
1154 * @link http://www.shuchow.com/vCalAddendum.html | |
1155 * | |
1156 * @param string $rrule An iCalendar 2.0 conform RRULE value. | |
1157 */ | |
1158 public function fromRRule20($rrule) | |
1159 { | |
1160 $this->reset(); | |
1161 | |
1162 // Parse the recurrence rule into keys and values. | |
1163 $rdata = array(); | |
1164 $parts = explode(';', $rrule); | |
1165 foreach ($parts as $part) { | |
1166 list($key, $value) = explode('=', $part, 2); | |
1167 $rdata[strtoupper($key)] = $value; | |
1168 } | |
1169 | |
1170 if (isset($rdata['FREQ'])) { | |
1171 // Always default the recurInterval to 1. | |
1172 $this->setRecurInterval(isset($rdata['INTERVAL']) ? $rdata['INTERVAL'] : 1); | |
1173 | |
1174 $maskdays = array( | |
1175 'SU' => Horde_Date::MASK_SUNDAY, | |
1176 'MO' => Horde_Date::MASK_MONDAY, | |
1177 'TU' => Horde_Date::MASK_TUESDAY, | |
1178 'WE' => Horde_Date::MASK_WEDNESDAY, | |
1179 'TH' => Horde_Date::MASK_THURSDAY, | |
1180 'FR' => Horde_Date::MASK_FRIDAY, | |
1181 'SA' => Horde_Date::MASK_SATURDAY, | |
1182 ); | |
1183 | |
1184 switch (strtoupper($rdata['FREQ'])) { | |
1185 case 'DAILY': | |
1186 $this->setRecurType(self::RECUR_DAILY); | |
1187 break; | |
1188 | |
1189 case 'WEEKLY': | |
1190 $this->setRecurType(self::RECUR_WEEKLY); | |
1191 if (isset($rdata['BYDAY'])) { | |
1192 $days = explode(',', $rdata['BYDAY']); | |
1193 $mask = 0; | |
1194 foreach ($days as $day) { | |
1195 $mask |= $maskdays[$day]; | |
1196 } | |
1197 $this->setRecurOnDay($mask); | |
1198 } else { | |
1199 // Recur on the day of the week of the original | |
1200 // recurrence. | |
1201 $maskdays = array( | |
1202 Horde_Date::DATE_SUNDAY => Horde_Date::MASK_SUNDAY, | |
1203 Horde_Date::DATE_MONDAY => Horde_Date::MASK_MONDAY, | |
1204 Horde_Date::DATE_TUESDAY => Horde_Date::MASK_TUESDAY, | |
1205 Horde_Date::DATE_WEDNESDAY => Horde_Date::MASK_WEDNESDAY, | |
1206 Horde_Date::DATE_THURSDAY => Horde_Date::MASK_THURSDAY, | |
1207 Horde_Date::DATE_FRIDAY => Horde_Date::MASK_FRIDAY, | |
1208 Horde_Date::DATE_SATURDAY => Horde_Date::MASK_SATURDAY); | |
1209 $this->setRecurOnDay($maskdays[$this->start->dayOfWeek()]); | |
1210 } | |
1211 break; | |
1212 | |
1213 case 'MONTHLY': | |
1214 if (isset($rdata['BYDAY'])) { | |
1215 $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY); | |
1216 if (preg_match('/(-?[1-4])([A-Z]+)/', $rdata['BYDAY'], $m)) { | |
1217 $this->setRecurOnDay($maskdays[$m[2]]); | |
1218 $this->setRecurNthWeekday($m[1]); | |
1219 } | |
1220 } else { | |
1221 $this->setRecurType(self::RECUR_MONTHLY_DATE); | |
1222 } | |
1223 break; | |
1224 | |
1225 case 'YEARLY': | |
1226 if (isset($rdata['BYYEARDAY'])) { | |
1227 $this->setRecurType(self::RECUR_YEARLY_DAY); | |
1228 } elseif (isset($rdata['BYDAY'])) { | |
1229 $this->setRecurType(self::RECUR_YEARLY_WEEKDAY); | |
1230 if (preg_match('/(-?[1-4])([A-Z]+)/', $rdata['BYDAY'], $m)) { | |
1231 $this->setRecurOnDay($maskdays[$m[2]]); | |
1232 $this->setRecurNthWeekday($m[1]); | |
1233 } | |
1234 if ($rdata['BYMONTH']) { | |
1235 $months = explode(',', $rdata['BYMONTH']); | |
1236 $this->setRecurByMonth($months); | |
1237 } | |
1238 } else { | |
1239 $this->setRecurType(self::RECUR_YEARLY_DATE); | |
1240 } | |
1241 break; | |
1242 } | |
1243 | |
1244 // MUST take into account the time portion if it is present. | |
1245 // See Bug: 12869 and Bug: 2813 | |
1246 if (isset($rdata['UNTIL'])) { | |
1247 if (preg_match('/^(\d{4})-?(\d{2})-?(\d{2})T? ?(\d{2}):?(\d{2}):?(\d{2})(?:\.\d+)?(Z?)$/', $rdata['UNTIL'], $parts)) { | |
1248 $until = new Horde_Date($rdata['UNTIL'], 'UTC'); | |
1249 $until->setTimezone($this->start->timezone); | |
1250 } else { | |
1251 list($year, $month, $mday) = sscanf($rdata['UNTIL'], | |
1252 '%04d%02d%02d'); | |
1253 $until = new Horde_Date( | |
1254 array('year' => $year, | |
1255 'month' => $month, | |
1256 'mday' => $mday + 1), | |
1257 $this->start->timezone | |
1258 ); | |
1259 } | |
1260 $this->setRecurEnd($until); | |
1261 } | |
1262 if (isset($rdata['COUNT'])) { | |
1263 $this->setRecurCount($rdata['COUNT']); | |
1264 } | |
1265 } else { | |
1266 // No recurrence data - event does not recur. | |
1267 $this->setRecurType(self::RECUR_NONE); | |
1268 } | |
1269 } | |
1270 | |
1271 /** | |
1272 * Creates an iCalendar 2.0 recurrence rule. | |
1273 * | |
1274 * @link http://rfc.net/rfc2445.html#s4.3.10 | |
1275 * @link http://rfc.net/rfc2445.html#s4.8.5 | |
1276 * @link http://www.shuchow.com/vCalAddendum.html | |
1277 * | |
1278 * @param Horde_Icalendar $calendar A Horde_Icalendar object instance. | |
1279 * | |
1280 * @return string An iCalendar 2.0 conform RRULE value. | |
1281 */ | |
1282 public function toRRule20($calendar) | |
1283 { | |
1284 switch ($this->recurType) { | |
1285 case self::RECUR_NONE: | |
1286 return ''; | |
1287 | |
1288 case self::RECUR_DAILY: | |
1289 $rrule = 'FREQ=DAILY;INTERVAL=' . $this->recurInterval; | |
1290 break; | |
1291 | |
1292 case self::RECUR_WEEKLY: | |
1293 $rrule = 'FREQ=WEEKLY;INTERVAL=' . $this->recurInterval; | |
1294 $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'); | |
1295 | |
1296 for ($i = $flag = 0; $i <= 7; ++$i) { | |
1297 if ($this->recurOnDay(pow(2, $i))) { | |
1298 if ($flag == 0) { | |
1299 $rrule .= ';BYDAY='; | |
1300 $flag = 1; | |
1301 } else { | |
1302 $rrule .= ','; | |
1303 } | |
1304 $rrule .= $vcaldays[$i]; | |
1305 } | |
1306 } | |
1307 break; | |
1308 | |
1309 case self::RECUR_MONTHLY_DATE: | |
1310 $rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval; | |
1311 break; | |
1312 | |
1313 case self::RECUR_MONTHLY_WEEKDAY: | |
1314 if (isset($this->recurNthDay)) { | |
1315 $nth_weekday = $this->recurNthDay; | |
1316 $day_of_week = log($this->recurData, 2); | |
1317 } else { | |
1318 $day_of_week = $this->start->dayOfWeek(); | |
1319 $nth_weekday = (int)($this->start->mday / 7); | |
1320 if (($this->start->mday % 7) > 0) { | |
1321 $nth_weekday++; | |
1322 } | |
1323 } | |
1324 $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'); | |
1325 $rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval | |
1326 . ';BYDAY=' . $nth_weekday . $vcaldays[$day_of_week]; | |
1327 break; | |
1328 | |
1329 case self::RECUR_YEARLY_DATE: | |
1330 $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval; | |
1331 break; | |
1332 | |
1333 case self::RECUR_YEARLY_DAY: | |
1334 $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval | |
1335 . ';BYYEARDAY=' . $this->start->dayOfYear(); | |
1336 break; | |
1337 | |
1338 case self::RECUR_YEARLY_WEEKDAY: | |
1339 if (isset($this->recurNthDay)) { | |
1340 $nth_weekday = $this->recurNthDay; | |
1341 $day_of_week = log($this->recurData, 2); | |
1342 } else { | |
1343 $day_of_week = $this->start->dayOfWeek(); | |
1344 $nth_weekday = (int)($this->start->mday / 7); | |
1345 if (($this->start->mday % 7) > 0) { | |
1346 $nth_weekday++; | |
1347 } | |
1348 } | |
1349 $months = !empty($this->recurMonths) ? join(',', $this->recurMonths) : $this->start->month; | |
1350 $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'); | |
1351 $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval | |
1352 . ';BYDAY=' | |
1353 . $nth_weekday | |
1354 . $vcaldays[$day_of_week] | |
1355 . ';BYMONTH=' . $this->start->month; | |
1356 break; | |
1357 } | |
1358 | |
1359 if ($this->hasRecurEnd()) { | |
1360 $recurEnd = clone $this->recurEnd; | |
1361 $rrule .= ';UNTIL=' . $calendar->_exportDateTime($recurEnd); | |
1362 } | |
1363 if ($count = $this->getRecurCount()) { | |
1364 $rrule .= ';COUNT=' . $count; | |
1365 } | |
1366 return $rrule; | |
1367 } | |
1368 | |
1369 /** | |
1370 * Parses the recurrence data from a Kolab hash. | |
1371 * | |
1372 * @param array $hash The hash to convert. | |
1373 * | |
1374 * @return boolean True if the hash seemed valid, false otherwise. | |
1375 */ | |
1376 public function fromKolab($hash) | |
1377 { | |
1378 $this->reset(); | |
1379 | |
1380 if (!isset($hash['interval']) || !isset($hash['cycle'])) { | |
1381 $this->setRecurType(self::RECUR_NONE); | |
1382 return false; | |
1383 } | |
1384 | |
1385 $this->setRecurInterval((int)$hash['interval']); | |
1386 | |
1387 $month2number = array( | |
1388 'january' => 1, | |
1389 'february' => 2, | |
1390 'march' => 3, | |
1391 'april' => 4, | |
1392 'may' => 5, | |
1393 'june' => 6, | |
1394 'july' => 7, | |
1395 'august' => 8, | |
1396 'september' => 9, | |
1397 'october' => 10, | |
1398 'november' => 11, | |
1399 'december' => 12, | |
1400 ); | |
1401 | |
1402 $parse_day = false; | |
1403 $set_daymask = false; | |
1404 $update_month = false; | |
1405 $update_daynumber = false; | |
1406 $update_weekday = false; | |
1407 $nth_weekday = -1; | |
1408 | |
1409 switch ($hash['cycle']) { | |
1410 case 'daily': | |
1411 $this->setRecurType(self::RECUR_DAILY); | |
1412 break; | |
1413 | |
1414 case 'weekly': | |
1415 $this->setRecurType(self::RECUR_WEEKLY); | |
1416 $parse_day = true; | |
1417 $set_daymask = true; | |
1418 break; | |
1419 | |
1420 case 'monthly': | |
1421 if (!isset($hash['daynumber'])) { | |
1422 $this->setRecurType(self::RECUR_NONE); | |
1423 return false; | |
1424 } | |
1425 | |
1426 switch ($hash['type']) { | |
1427 case 'daynumber': | |
1428 $this->setRecurType(self::RECUR_MONTHLY_DATE); | |
1429 $update_daynumber = true; | |
1430 break; | |
1431 | |
1432 case 'weekday': | |
1433 $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY); | |
1434 $this->setRecurNthWeekday($hash['daynumber']); | |
1435 $parse_day = true; | |
1436 $set_daymask = true; | |
1437 break; | |
1438 } | |
1439 break; | |
1440 | |
1441 case 'yearly': | |
1442 if (!isset($hash['type'])) { | |
1443 $this->setRecurType(self::RECUR_NONE); | |
1444 return false; | |
1445 } | |
1446 | |
1447 switch ($hash['type']) { | |
1448 case 'monthday': | |
1449 $this->setRecurType(self::RECUR_YEARLY_DATE); | |
1450 $update_month = true; | |
1451 $update_daynumber = true; | |
1452 break; | |
1453 | |
1454 case 'yearday': | |
1455 if (!isset($hash['daynumber'])) { | |
1456 $this->setRecurType(self::RECUR_NONE); | |
1457 return false; | |
1458 } | |
1459 | |
1460 $this->setRecurType(self::RECUR_YEARLY_DAY); | |
1461 // Start counting days in January. | |
1462 $hash['month'] = 'january'; | |
1463 $update_month = true; | |
1464 $update_daynumber = true; | |
1465 break; | |
1466 | |
1467 case 'weekday': | |
1468 if (!isset($hash['daynumber'])) { | |
1469 $this->setRecurType(self::RECUR_NONE); | |
1470 return false; | |
1471 } | |
1472 | |
1473 $this->setRecurType(self::RECUR_YEARLY_WEEKDAY); | |
1474 $this->setRecurNthWeekday($hash['daynumber']); | |
1475 $parse_day = true; | |
1476 $set_daymask = true; | |
1477 | |
1478 if ($hash['month'] && isset($month2number[$hash['month']])) { | |
1479 $this->setRecurByMonth($month2number[$hash['month']]); | |
1480 } | |
1481 break; | |
1482 } | |
1483 } | |
1484 | |
1485 if (isset($hash['range-type']) && isset($hash['range'])) { | |
1486 switch ($hash['range-type']) { | |
1487 case 'number': | |
1488 $this->setRecurCount((int)$hash['range']); | |
1489 break; | |
1490 | |
1491 case 'date': | |
1492 $recur_end = new Horde_Date($hash['range']); | |
1493 $recur_end->hour = 23; | |
1494 $recur_end->min = 59; | |
1495 $recur_end->sec = 59; | |
1496 $this->setRecurEnd($recur_end); | |
1497 break; | |
1498 } | |
1499 } | |
1500 | |
1501 // Need to parse <day>? | |
1502 $last_found_day = -1; | |
1503 if ($parse_day) { | |
1504 if (!isset($hash['day'])) { | |
1505 $this->setRecurType(self::RECUR_NONE); | |
1506 return false; | |
1507 } | |
1508 | |
1509 $mask = 0; | |
1510 $bits = array( | |
1511 'monday' => Horde_Date::MASK_MONDAY, | |
1512 'tuesday' => Horde_Date::MASK_TUESDAY, | |
1513 'wednesday' => Horde_Date::MASK_WEDNESDAY, | |
1514 'thursday' => Horde_Date::MASK_THURSDAY, | |
1515 'friday' => Horde_Date::MASK_FRIDAY, | |
1516 'saturday' => Horde_Date::MASK_SATURDAY, | |
1517 'sunday' => Horde_Date::MASK_SUNDAY, | |
1518 ); | |
1519 $days = array( | |
1520 'monday' => Horde_Date::DATE_MONDAY, | |
1521 'tuesday' => Horde_Date::DATE_TUESDAY, | |
1522 'wednesday' => Horde_Date::DATE_WEDNESDAY, | |
1523 'thursday' => Horde_Date::DATE_THURSDAY, | |
1524 'friday' => Horde_Date::DATE_FRIDAY, | |
1525 'saturday' => Horde_Date::DATE_SATURDAY, | |
1526 'sunday' => Horde_Date::DATE_SUNDAY, | |
1527 ); | |
1528 | |
1529 foreach ($hash['day'] as $day) { | |
1530 // Validity check. | |
1531 if (empty($day) || !isset($bits[$day])) { | |
1532 continue; | |
1533 } | |
1534 | |
1535 $mask |= $bits[$day]; | |
1536 $last_found_day = $days[$day]; | |
1537 } | |
1538 | |
1539 if ($set_daymask) { | |
1540 $this->setRecurOnDay($mask); | |
1541 } | |
1542 } | |
1543 | |
1544 if ($update_month || $update_daynumber || $update_weekday) { | |
1545 if ($update_month) { | |
1546 if (isset($month2number[$hash['month']])) { | |
1547 $this->start->month = $month2number[$hash['month']]; | |
1548 } | |
1549 } | |
1550 | |
1551 if ($update_daynumber) { | |
1552 if (!isset($hash['daynumber'])) { | |
1553 $this->setRecurType(self::RECUR_NONE); | |
1554 return false; | |
1555 } | |
1556 | |
1557 $this->start->mday = $hash['daynumber']; | |
1558 } | |
1559 | |
1560 if ($update_weekday) { | |
1561 $this->setNthWeekday($nth_weekday); | |
1562 } | |
1563 } | |
1564 | |
1565 // Exceptions. | |
1566 if (isset($hash['exclusion'])) { | |
1567 foreach ($hash['exclusion'] as $exception) { | |
1568 if ($exception instanceof DateTime) { | |
1569 $this->exceptions[] = $exception->format('Ymd'); | |
1570 } | |
1571 } | |
1572 } | |
1573 | |
1574 if (isset($hash['complete'])) { | |
1575 foreach ($hash['complete'] as $completion) { | |
1576 if ($exception instanceof DateTime) { | |
1577 $this->completions[] = $completion->format('Ymd'); | |
1578 } | |
1579 } | |
1580 } | |
1581 | |
1582 return true; | |
1583 } | |
1584 | |
1585 /** | |
1586 * Export this object into a Kolab hash. | |
1587 * | |
1588 * @return array The recurrence hash. | |
1589 */ | |
1590 public function toKolab() | |
1591 { | |
1592 if ($this->getRecurType() == self::RECUR_NONE) { | |
1593 return array(); | |
1594 } | |
1595 | |
1596 $day2number = array( | |
1597 0 => 'sunday', | |
1598 1 => 'monday', | |
1599 2 => 'tuesday', | |
1600 3 => 'wednesday', | |
1601 4 => 'thursday', | |
1602 5 => 'friday', | |
1603 6 => 'saturday' | |
1604 ); | |
1605 $month2number = array( | |
1606 1 => 'january', | |
1607 2 => 'february', | |
1608 3 => 'march', | |
1609 4 => 'april', | |
1610 5 => 'may', | |
1611 6 => 'june', | |
1612 7 => 'july', | |
1613 8 => 'august', | |
1614 9 => 'september', | |
1615 10 => 'october', | |
1616 11 => 'november', | |
1617 12 => 'december' | |
1618 ); | |
1619 | |
1620 $hash = array('interval' => $this->getRecurInterval()); | |
1621 $start = $this->getRecurStart(); | |
1622 | |
1623 switch ($this->getRecurType()) { | |
1624 case self::RECUR_DAILY: | |
1625 $hash['cycle'] = 'daily'; | |
1626 break; | |
1627 | |
1628 case self::RECUR_WEEKLY: | |
1629 $hash['cycle'] = 'weekly'; | |
1630 $bits = array( | |
1631 'monday' => Horde_Date::MASK_MONDAY, | |
1632 'tuesday' => Horde_Date::MASK_TUESDAY, | |
1633 'wednesday' => Horde_Date::MASK_WEDNESDAY, | |
1634 'thursday' => Horde_Date::MASK_THURSDAY, | |
1635 'friday' => Horde_Date::MASK_FRIDAY, | |
1636 'saturday' => Horde_Date::MASK_SATURDAY, | |
1637 'sunday' => Horde_Date::MASK_SUNDAY, | |
1638 ); | |
1639 $days = array(); | |
1640 foreach ($bits as $name => $bit) { | |
1641 if ($this->recurOnDay($bit)) { | |
1642 $days[] = $name; | |
1643 } | |
1644 } | |
1645 $hash['day'] = $days; | |
1646 break; | |
1647 | |
1648 case self::RECUR_MONTHLY_DATE: | |
1649 $hash['cycle'] = 'monthly'; | |
1650 $hash['type'] = 'daynumber'; | |
1651 $hash['daynumber'] = $start->mday; | |
1652 break; | |
1653 | |
1654 case self::RECUR_MONTHLY_WEEKDAY: | |
1655 $hash['cycle'] = 'monthly'; | |
1656 $hash['type'] = 'weekday'; | |
1657 $hash['daynumber'] = $start->weekOfMonth(); | |
1658 $hash['day'] = array ($day2number[$start->dayOfWeek()]); | |
1659 break; | |
1660 | |
1661 case self::RECUR_YEARLY_DATE: | |
1662 $hash['cycle'] = 'yearly'; | |
1663 $hash['type'] = 'monthday'; | |
1664 $hash['daynumber'] = $start->mday; | |
1665 $hash['month'] = $month2number[$start->month]; | |
1666 break; | |
1667 | |
1668 case self::RECUR_YEARLY_DAY: | |
1669 $hash['cycle'] = 'yearly'; | |
1670 $hash['type'] = 'yearday'; | |
1671 $hash['daynumber'] = $start->dayOfYear(); | |
1672 break; | |
1673 | |
1674 case self::RECUR_YEARLY_WEEKDAY: | |
1675 $hash['cycle'] = 'yearly'; | |
1676 $hash['type'] = 'weekday'; | |
1677 $hash['daynumber'] = $start->weekOfMonth(); | |
1678 $hash['day'] = array ($day2number[$start->dayOfWeek()]); | |
1679 $hash['month'] = $month2number[$start->month]; | |
1680 } | |
1681 | |
1682 if ($this->hasRecurCount()) { | |
1683 $hash['range-type'] = 'number'; | |
1684 $hash['range'] = $this->getRecurCount(); | |
1685 } elseif ($this->hasRecurEnd()) { | |
1686 $date = $this->getRecurEnd(); | |
1687 $hash['range-type'] = 'date'; | |
1688 $hash['range'] = $date->toDateTime(); | |
1689 } else { | |
1690 $hash['range-type'] = 'none'; | |
1691 $hash['range'] = ''; | |
1692 } | |
1693 | |
1694 // Recurrence exceptions | |
1695 $hash['exclusion'] = $hash['complete'] = array(); | |
1696 foreach ($this->exceptions as $exception) { | |
1697 $hash['exclusion'][] = new DateTime($exception); | |
1698 } | |
1699 foreach ($this->completions as $completionexception) { | |
1700 $hash['complete'][] = new DateTime($completionexception); | |
1701 } | |
1702 | |
1703 return $hash; | |
1704 } | |
1705 | |
1706 /** | |
1707 * Returns a simple object suitable for json transport representing this | |
1708 * object. | |
1709 * | |
1710 * Possible properties are: | |
1711 * - t: type | |
1712 * - i: interval | |
1713 * - e: end date | |
1714 * - c: count | |
1715 * - d: data | |
1716 * - co: completions | |
1717 * - ex: exceptions | |
1718 * | |
1719 * @return object A simple object. | |
1720 */ | |
1721 public function toJson() | |
1722 { | |
1723 $json = new stdClass; | |
1724 $json->t = $this->recurType; | |
1725 $json->i = $this->recurInterval; | |
1726 if ($this->hasRecurEnd()) { | |
1727 $json->e = $this->recurEnd->toJson(); | |
1728 } | |
1729 if ($this->recurCount) { | |
1730 $json->c = $this->recurCount; | |
1731 } | |
1732 if ($this->recurData) { | |
1733 $json->d = $this->recurData; | |
1734 } | |
1735 if ($this->completions) { | |
1736 $json->co = $this->completions; | |
1737 } | |
1738 if ($this->exceptions) { | |
1739 $json->ex = $this->exceptions; | |
1740 } | |
1741 return $json; | |
1742 } | |
1743 | |
1744 } |