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