3
|
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 }
|