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