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