Mercurial > hg > rc1
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 } |