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 }