Overview

Packages

  • Date

Classes

  • Horde_Date
  • Horde_Date_Exception
  • Horde_Date_Recurrence
  • Horde_Date_Repeater
  • Horde_Date_Repeater_Day
  • Horde_Date_Repeater_DayName
  • Horde_Date_Repeater_DayPortion
  • Horde_Date_Repeater_Fortnight
  • Horde_Date_Repeater_Hour
  • Horde_Date_Repeater_Minute
  • Horde_Date_Repeater_Month
  • Horde_Date_Repeater_MonthName
  • Horde_Date_Repeater_Season
  • Horde_Date_Repeater_SeasonName
  • Horde_Date_Repeater_Second
  • Horde_Date_Repeater_Time
  • Horde_Date_Repeater_Week
  • Horde_Date_Repeater_Weekend
  • Horde_Date_Repeater_Year
  • Horde_Date_Span
  • Horde_Date_Translation
  • Horde_Date_Utils

Exceptions

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