comparison plugins/calendar/lib/Horde_Date_Recurrence.php @ 3:f6fe4b6ae66a

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