Mercurial > hg > rc1
comparison vendor/sabre/vobject/lib/Recur/RRuleIterator.php @ 7:430dbd5346f7
vendor sabre as distributed
| author | Charlie Root |
|---|---|
| date | Sat, 13 Jan 2018 09:06:10 -0500 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 6:cec75ba50afc | 7:430dbd5346f7 |
|---|---|
| 1 <?php | |
| 2 | |
| 3 namespace Sabre\VObject\Recur; | |
| 4 | |
| 5 use DateTime; | |
| 6 use InvalidArgumentException; | |
| 7 use Iterator; | |
| 8 use Sabre\VObject\DateTimeParser; | |
| 9 use Sabre\VObject\Property; | |
| 10 | |
| 11 | |
| 12 /** | |
| 13 * RRuleParser | |
| 14 * | |
| 15 * This class receives an RRULE string, and allows you to iterate to get a list | |
| 16 * of dates in that recurrence. | |
| 17 * | |
| 18 * For instance, passing: FREQ=DAILY;LIMIT=5 will cause the iterator to contain | |
| 19 * 5 items, one for each day. | |
| 20 * | |
| 21 * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/). | |
| 22 * @author Evert Pot (http://evertpot.com/) | |
| 23 * @license http://sabre.io/license/ Modified BSD License | |
| 24 */ | |
| 25 class RRuleIterator implements Iterator { | |
| 26 | |
| 27 /** | |
| 28 * Creates the Iterator | |
| 29 * | |
| 30 * @param string|array $rrule | |
| 31 * @param DateTime $start | |
| 32 */ | |
| 33 public function __construct($rrule, DateTime $start) { | |
| 34 | |
| 35 $this->startDate = $start; | |
| 36 $this->parseRRule($rrule); | |
| 37 $this->currentDate = clone $this->startDate; | |
| 38 | |
| 39 } | |
| 40 | |
| 41 /* Implementation of the Iterator interface {{{ */ | |
| 42 | |
| 43 public function current() { | |
| 44 | |
| 45 if (!$this->valid()) return null; | |
| 46 return clone $this->currentDate; | |
| 47 | |
| 48 } | |
| 49 | |
| 50 /** | |
| 51 * Returns the current item number | |
| 52 * | |
| 53 * @return int | |
| 54 */ | |
| 55 public function key() { | |
| 56 | |
| 57 return $this->counter; | |
| 58 | |
| 59 } | |
| 60 | |
| 61 /** | |
| 62 * Returns whether the current item is a valid item for the recurrence | |
| 63 * iterator. This will return false if we've gone beyond the UNTIL or COUNT | |
| 64 * statements. | |
| 65 * | |
| 66 * @return bool | |
| 67 */ | |
| 68 public function valid() { | |
| 69 | |
| 70 if (!is_null($this->count)) { | |
| 71 return $this->counter < $this->count; | |
| 72 } | |
| 73 return is_null($this->until) || $this->currentDate <= $this->until; | |
| 74 | |
| 75 } | |
| 76 | |
| 77 /** | |
| 78 * Resets the iterator | |
| 79 * | |
| 80 * @return void | |
| 81 */ | |
| 82 public function rewind() { | |
| 83 | |
| 84 $this->currentDate = clone $this->startDate; | |
| 85 $this->counter = 0; | |
| 86 | |
| 87 } | |
| 88 | |
| 89 /** | |
| 90 * Goes on to the next iteration | |
| 91 * | |
| 92 * @return void | |
| 93 */ | |
| 94 public function next() { | |
| 95 | |
| 96 $previousStamp = $this->currentDate->getTimeStamp(); | |
| 97 | |
| 98 // Otherwise, we find the next event in the normal RRULE | |
| 99 // sequence. | |
| 100 switch($this->frequency) { | |
| 101 | |
| 102 case 'hourly' : | |
| 103 $this->nextHourly(); | |
| 104 break; | |
| 105 | |
| 106 case 'daily' : | |
| 107 $this->nextDaily(); | |
| 108 break; | |
| 109 | |
| 110 case 'weekly' : | |
| 111 $this->nextWeekly(); | |
| 112 break; | |
| 113 | |
| 114 case 'monthly' : | |
| 115 $this->nextMonthly(); | |
| 116 break; | |
| 117 | |
| 118 case 'yearly' : | |
| 119 $this->nextYearly(); | |
| 120 break; | |
| 121 | |
| 122 } | |
| 123 $this->counter++; | |
| 124 | |
| 125 } | |
| 126 | |
| 127 /* End of Iterator implementation }}} */ | |
| 128 | |
| 129 /** | |
| 130 * Returns true if this recurring event never ends. | |
| 131 * | |
| 132 * @return bool | |
| 133 */ | |
| 134 public function isInfinite() { | |
| 135 | |
| 136 return !$this->count && !$this->until; | |
| 137 | |
| 138 } | |
| 139 | |
| 140 /** | |
| 141 * This method allows you to quickly go to the next occurrence after the | |
| 142 * specified date. | |
| 143 * | |
| 144 * @param DateTime $dt | |
| 145 * @return void | |
| 146 */ | |
| 147 public function fastForward(\DateTime $dt) { | |
| 148 | |
| 149 while($this->valid() && $this->currentDate < $dt ) { | |
| 150 $this->next(); | |
| 151 } | |
| 152 | |
| 153 } | |
| 154 | |
| 155 /** | |
| 156 * The reference start date/time for the rrule. | |
| 157 * | |
| 158 * All calculations are based on this initial date. | |
| 159 * | |
| 160 * @var DateTime | |
| 161 */ | |
| 162 protected $startDate; | |
| 163 | |
| 164 /** | |
| 165 * The date of the current iteration. You can get this by calling | |
| 166 * ->current(). | |
| 167 * | |
| 168 * @var DateTime | |
| 169 */ | |
| 170 protected $currentDate; | |
| 171 | |
| 172 /** | |
| 173 * Frequency is one of: secondly, minutely, hourly, daily, weekly, monthly, | |
| 174 * yearly. | |
| 175 * | |
| 176 * @var string | |
| 177 */ | |
| 178 protected $frequency; | |
| 179 | |
| 180 /** | |
| 181 * The number of recurrences, or 'null' if infinitely recurring. | |
| 182 * | |
| 183 * @var int | |
| 184 */ | |
| 185 protected $count; | |
| 186 | |
| 187 /** | |
| 188 * The interval. | |
| 189 * | |
| 190 * If for example frequency is set to daily, interval = 2 would mean every | |
| 191 * 2 days. | |
| 192 * | |
| 193 * @var int | |
| 194 */ | |
| 195 protected $interval = 1; | |
| 196 | |
| 197 /** | |
| 198 * The last instance of this recurrence, inclusively | |
| 199 * | |
| 200 * @var \DateTime|null | |
| 201 */ | |
| 202 protected $until; | |
| 203 | |
| 204 /** | |
| 205 * Which seconds to recur. | |
| 206 * | |
| 207 * This is an array of integers (between 0 and 60) | |
| 208 * | |
| 209 * @var array | |
| 210 */ | |
| 211 protected $bySecond; | |
| 212 | |
| 213 /** | |
| 214 * Which minutes to recur | |
| 215 * | |
| 216 * This is an array of integers (between 0 and 59) | |
| 217 * | |
| 218 * @var array | |
| 219 */ | |
| 220 protected $byMinute; | |
| 221 | |
| 222 /** | |
| 223 * Which hours to recur | |
| 224 * | |
| 225 * This is an array of integers (between 0 and 23) | |
| 226 * | |
| 227 * @var array | |
| 228 */ | |
| 229 protected $byHour; | |
| 230 | |
| 231 /** | |
| 232 * The current item in the list. | |
| 233 * | |
| 234 * You can get this number with the key() method. | |
| 235 * | |
| 236 * @var int | |
| 237 */ | |
| 238 protected $counter = 0; | |
| 239 | |
| 240 /** | |
| 241 * Which weekdays to recur. | |
| 242 * | |
| 243 * This is an array of weekdays | |
| 244 * | |
| 245 * This may also be preceeded by a positive or negative integer. If present, | |
| 246 * this indicates the nth occurrence of a specific day within the monthly or | |
| 247 * yearly rrule. For instance, -2TU indicates the second-last tuesday of | |
| 248 * the month, or year. | |
| 249 * | |
| 250 * @var array | |
| 251 */ | |
| 252 protected $byDay; | |
| 253 | |
| 254 /** | |
| 255 * Which days of the month to recur | |
| 256 * | |
| 257 * This is an array of days of the months (1-31). The value can also be | |
| 258 * negative. -5 for instance means the 5th last day of the month. | |
| 259 * | |
| 260 * @var array | |
| 261 */ | |
| 262 protected $byMonthDay; | |
| 263 | |
| 264 /** | |
| 265 * Which days of the year to recur. | |
| 266 * | |
| 267 * This is an array with days of the year (1 to 366). The values can also | |
| 268 * be negative. For instance, -1 will always represent the last day of the | |
| 269 * year. (December 31st). | |
| 270 * | |
| 271 * @var array | |
| 272 */ | |
| 273 protected $byYearDay; | |
| 274 | |
| 275 /** | |
| 276 * Which week numbers to recur. | |
| 277 * | |
| 278 * This is an array of integers from 1 to 53. The values can also be | |
| 279 * negative. -1 will always refer to the last week of the year. | |
| 280 * | |
| 281 * @var array | |
| 282 */ | |
| 283 protected $byWeekNo; | |
| 284 | |
| 285 /** | |
| 286 * Which months to recur. | |
| 287 * | |
| 288 * This is an array of integers from 1 to 12. | |
| 289 * | |
| 290 * @var array | |
| 291 */ | |
| 292 protected $byMonth; | |
| 293 | |
| 294 /** | |
| 295 * Which items in an existing st to recur. | |
| 296 * | |
| 297 * These numbers work together with an existing by* rule. It specifies | |
| 298 * exactly which items of the existing by-rule to filter. | |
| 299 * | |
| 300 * Valid values are 1 to 366 and -1 to -366. As an example, this can be | |
| 301 * used to recur the last workday of the month. | |
| 302 * | |
| 303 * This would be done by setting frequency to 'monthly', byDay to | |
| 304 * 'MO,TU,WE,TH,FR' and bySetPos to -1. | |
| 305 * | |
| 306 * @var array | |
| 307 */ | |
| 308 protected $bySetPos; | |
| 309 | |
| 310 /** | |
| 311 * When the week starts. | |
| 312 * | |
| 313 * @var string | |
| 314 */ | |
| 315 protected $weekStart = 'MO'; | |
| 316 | |
| 317 /* Functions that advance the iterator {{{ */ | |
| 318 | |
| 319 /** | |
| 320 * Does the processing for advancing the iterator for hourly frequency. | |
| 321 * | |
| 322 * @return void | |
| 323 */ | |
| 324 protected function nextHourly() { | |
| 325 | |
| 326 $this->currentDate->modify('+' . $this->interval . ' hours'); | |
| 327 | |
| 328 } | |
| 329 | |
| 330 /** | |
| 331 * Does the processing for advancing the iterator for daily frequency. | |
| 332 * | |
| 333 * @return void | |
| 334 */ | |
| 335 protected function nextDaily() { | |
| 336 | |
| 337 if (!$this->byHour && !$this->byDay) { | |
| 338 $this->currentDate->modify('+' . $this->interval . ' days'); | |
| 339 return; | |
| 340 } | |
| 341 | |
| 342 if (isset($this->byHour)) { | |
| 343 $recurrenceHours = $this->getHours(); | |
| 344 } | |
| 345 | |
| 346 if (isset($this->byDay)) { | |
| 347 $recurrenceDays = $this->getDays(); | |
| 348 } | |
| 349 | |
| 350 if (isset($this->byMonth)) { | |
| 351 $recurrenceMonths = $this->getMonths(); | |
| 352 } | |
| 353 | |
| 354 do { | |
| 355 if ($this->byHour) { | |
| 356 if ($this->currentDate->format('G') == '23') { | |
| 357 // to obey the interval rule | |
| 358 $this->currentDate->modify('+' . $this->interval-1 . ' days'); | |
| 359 } | |
| 360 | |
| 361 $this->currentDate->modify('+1 hours'); | |
| 362 | |
| 363 } else { | |
| 364 $this->currentDate->modify('+' . $this->interval . ' days'); | |
| 365 | |
| 366 } | |
| 367 | |
| 368 // Current month of the year | |
| 369 $currentMonth = $this->currentDate->format('n'); | |
| 370 | |
| 371 // Current day of the week | |
| 372 $currentDay = $this->currentDate->format('w'); | |
| 373 | |
| 374 // Current hour of the day | |
| 375 $currentHour = $this->currentDate->format('G'); | |
| 376 | |
| 377 } while ( | |
| 378 ($this->byDay && !in_array($currentDay, $recurrenceDays)) || | |
| 379 ($this->byHour && !in_array($currentHour, $recurrenceHours)) || | |
| 380 ($this->byMonth && !in_array($currentMonth, $recurrenceMonths)) | |
| 381 ); | |
| 382 | |
| 383 } | |
| 384 | |
| 385 /** | |
| 386 * Does the processing for advancing the iterator for weekly frequency. | |
| 387 * | |
| 388 * @return void | |
| 389 */ | |
| 390 protected function nextWeekly() { | |
| 391 | |
| 392 if (!$this->byHour && !$this->byDay) { | |
| 393 $this->currentDate->modify('+' . $this->interval . ' weeks'); | |
| 394 return; | |
| 395 } | |
| 396 | |
| 397 if ($this->byHour) { | |
| 398 $recurrenceHours = $this->getHours(); | |
| 399 } | |
| 400 | |
| 401 if ($this->byDay) { | |
| 402 $recurrenceDays = $this->getDays(); | |
| 403 } | |
| 404 | |
| 405 // First day of the week: | |
| 406 $firstDay = $this->dayMap[$this->weekStart]; | |
| 407 | |
| 408 do { | |
| 409 | |
| 410 if ($this->byHour) { | |
| 411 $this->currentDate->modify('+1 hours'); | |
| 412 } else { | |
| 413 $this->currentDate->modify('+1 days'); | |
| 414 } | |
| 415 | |
| 416 // Current day of the week | |
| 417 $currentDay = (int) $this->currentDate->format('w'); | |
| 418 | |
| 419 // Current hour of the day | |
| 420 $currentHour = (int) $this->currentDate->format('G'); | |
| 421 | |
| 422 // We need to roll over to the next week | |
| 423 if ($currentDay === $firstDay && (!$this->byHour || $currentHour == '0')) { | |
| 424 $this->currentDate->modify('+' . $this->interval-1 . ' weeks'); | |
| 425 | |
| 426 // We need to go to the first day of this week, but only if we | |
| 427 // are not already on this first day of this week. | |
| 428 if($this->currentDate->format('w') != $firstDay) { | |
| 429 $this->currentDate->modify('last ' . $this->dayNames[$this->dayMap[$this->weekStart]]); | |
| 430 } | |
| 431 } | |
| 432 | |
| 433 // We have a match | |
| 434 } while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours))); | |
| 435 } | |
| 436 | |
| 437 /** | |
| 438 * Does the processing for advancing the iterator for monthly frequency. | |
| 439 * | |
| 440 * @return void | |
| 441 */ | |
| 442 protected function nextMonthly() { | |
| 443 | |
| 444 $currentDayOfMonth = $this->currentDate->format('j'); | |
| 445 if (!$this->byMonthDay && !$this->byDay) { | |
| 446 | |
| 447 // If the current day is higher than the 28th, rollover can | |
| 448 // occur to the next month. We Must skip these invalid | |
| 449 // entries. | |
| 450 if ($currentDayOfMonth < 29) { | |
| 451 $this->currentDate->modify('+' . $this->interval . ' months'); | |
| 452 } else { | |
| 453 $increase = 0; | |
| 454 do { | |
| 455 $increase++; | |
| 456 $tempDate = clone $this->currentDate; | |
| 457 $tempDate->modify('+ ' . ($this->interval*$increase) . ' months'); | |
| 458 } while ($tempDate->format('j') != $currentDayOfMonth); | |
| 459 $this->currentDate = $tempDate; | |
| 460 } | |
| 461 return; | |
| 462 } | |
| 463 | |
| 464 while(true) { | |
| 465 | |
| 466 $occurrences = $this->getMonthlyOccurrences(); | |
| 467 | |
| 468 foreach($occurrences as $occurrence) { | |
| 469 | |
| 470 // The first occurrence thats higher than the current | |
| 471 // day of the month wins. | |
| 472 if ($occurrence > $currentDayOfMonth) { | |
| 473 break 2; | |
| 474 } | |
| 475 | |
| 476 } | |
| 477 | |
| 478 // If we made it all the way here, it means there were no | |
| 479 // valid occurrences, and we need to advance to the next | |
| 480 // month. | |
| 481 // | |
| 482 // This line does not currently work in hhvm. Temporary workaround | |
| 483 // follows: | |
| 484 // $this->currentDate->modify('first day of this month'); | |
| 485 $this->currentDate = new \DateTime($this->currentDate->format('Y-m-1 H:i:s'), $this->currentDate->getTimezone()); | |
| 486 // end of workaround | |
| 487 $this->currentDate->modify('+ ' . $this->interval . ' months'); | |
| 488 | |
| 489 // This goes to 0 because we need to start counting at the | |
| 490 // beginning. | |
| 491 $currentDayOfMonth = 0; | |
| 492 | |
| 493 } | |
| 494 | |
| 495 $this->currentDate->setDate($this->currentDate->format('Y'), $this->currentDate->format('n'), $occurrence); | |
| 496 | |
| 497 } | |
| 498 | |
| 499 /** | |
| 500 * Does the processing for advancing the iterator for yearly frequency. | |
| 501 * | |
| 502 * @return void | |
| 503 */ | |
| 504 protected function nextYearly() { | |
| 505 | |
| 506 $currentMonth = $this->currentDate->format('n'); | |
| 507 $currentYear = $this->currentDate->format('Y'); | |
| 508 $currentDayOfMonth = $this->currentDate->format('j'); | |
| 509 | |
| 510 // No sub-rules, so we just advance by year | |
| 511 if (!$this->byMonth) { | |
| 512 | |
| 513 // Unless it was a leap day! | |
| 514 if ($currentMonth==2 && $currentDayOfMonth==29) { | |
| 515 | |
| 516 $counter = 0; | |
| 517 do { | |
| 518 $counter++; | |
| 519 // Here we increase the year count by the interval, until | |
| 520 // we hit a date that's also in a leap year. | |
| 521 // | |
| 522 // We could just find the next interval that's dividable by | |
| 523 // 4, but that would ignore the rule that there's no leap | |
| 524 // year every year that's dividable by a 100, but not by | |
| 525 // 400. (1800, 1900, 2100). So we just rely on the datetime | |
| 526 // functions instead. | |
| 527 $nextDate = clone $this->currentDate; | |
| 528 $nextDate->modify('+ ' . ($this->interval*$counter) . ' years'); | |
| 529 } while ($nextDate->format('n')!=2); | |
| 530 $this->currentDate = $nextDate; | |
| 531 | |
| 532 return; | |
| 533 | |
| 534 } | |
| 535 | |
| 536 // The easiest form | |
| 537 $this->currentDate->modify('+' . $this->interval . ' years'); | |
| 538 return; | |
| 539 | |
| 540 } | |
| 541 | |
| 542 $currentMonth = $this->currentDate->format('n'); | |
| 543 $currentYear = $this->currentDate->format('Y'); | |
| 544 $currentDayOfMonth = $this->currentDate->format('j'); | |
| 545 | |
| 546 $advancedToNewMonth = false; | |
| 547 | |
| 548 // If we got a byDay or getMonthDay filter, we must first expand | |
| 549 // further. | |
| 550 if ($this->byDay || $this->byMonthDay) { | |
| 551 | |
| 552 while(true) { | |
| 553 | |
| 554 $occurrences = $this->getMonthlyOccurrences(); | |
| 555 | |
| 556 foreach($occurrences as $occurrence) { | |
| 557 | |
| 558 // The first occurrence that's higher than the current | |
| 559 // day of the month wins. | |
| 560 // If we advanced to the next month or year, the first | |
| 561 // occurrence is always correct. | |
| 562 if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) { | |
| 563 break 2; | |
| 564 } | |
| 565 | |
| 566 } | |
| 567 | |
| 568 // If we made it here, it means we need to advance to | |
| 569 // the next month or year. | |
| 570 $currentDayOfMonth = 1; | |
| 571 $advancedToNewMonth = true; | |
| 572 do { | |
| 573 | |
| 574 $currentMonth++; | |
| 575 if ($currentMonth>12) { | |
| 576 $currentYear+=$this->interval; | |
| 577 $currentMonth = 1; | |
| 578 } | |
| 579 } while (!in_array($currentMonth, $this->byMonth)); | |
| 580 | |
| 581 $this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth); | |
| 582 | |
| 583 } | |
| 584 | |
| 585 // If we made it here, it means we got a valid occurrence | |
| 586 $this->currentDate->setDate($currentYear, $currentMonth, $occurrence); | |
| 587 return; | |
| 588 | |
| 589 } else { | |
| 590 | |
| 591 // These are the 'byMonth' rules, if there are no byDay or | |
| 592 // byMonthDay sub-rules. | |
| 593 do { | |
| 594 | |
| 595 $currentMonth++; | |
| 596 if ($currentMonth>12) { | |
| 597 $currentYear+=$this->interval; | |
| 598 $currentMonth = 1; | |
| 599 } | |
| 600 } while (!in_array($currentMonth, $this->byMonth)); | |
| 601 $this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth); | |
| 602 | |
| 603 return; | |
| 604 | |
| 605 } | |
| 606 | |
| 607 } | |
| 608 | |
| 609 /* }}} */ | |
| 610 | |
| 611 /** | |
| 612 * This method receives a string from an RRULE property, and populates this | |
| 613 * class with all the values. | |
| 614 * | |
| 615 * @param string|array $rrule | |
| 616 * @return void | |
| 617 */ | |
| 618 protected function parseRRule($rrule) { | |
| 619 | |
| 620 if (is_string($rrule)) { | |
| 621 $rrule = Property\ICalendar\Recur::stringToArray($rrule); | |
| 622 } | |
| 623 | |
| 624 foreach($rrule as $key=>$value) { | |
| 625 | |
| 626 $key = strtoupper($key); | |
| 627 switch($key) { | |
| 628 | |
| 629 case 'FREQ' : | |
| 630 $value = strtolower($value); | |
| 631 if (!in_array( | |
| 632 $value, | |
| 633 array('secondly','minutely','hourly','daily','weekly','monthly','yearly') | |
| 634 )) { | |
| 635 throw new InvalidArgumentException('Unknown value for FREQ=' . strtoupper($value)); | |
| 636 } | |
| 637 $this->frequency = $value; | |
| 638 break; | |
| 639 | |
| 640 case 'UNTIL' : | |
| 641 $this->until = DateTimeParser::parse($value, $this->startDate->getTimezone()); | |
| 642 | |
| 643 // In some cases events are generated with an UNTIL= | |
| 644 // parameter before the actual start of the event. | |
| 645 // | |
| 646 // Not sure why this is happening. We assume that the | |
| 647 // intention was that the event only recurs once. | |
| 648 // | |
| 649 // So we are modifying the parameter so our code doesn't | |
| 650 // break. | |
| 651 if($this->until < $this->startDate) { | |
| 652 $this->until = $this->startDate; | |
| 653 } | |
| 654 break; | |
| 655 | |
| 656 case 'INTERVAL' : | |
| 657 // No break | |
| 658 | |
| 659 case 'COUNT' : | |
| 660 $val = (int)$value; | |
| 661 if ($val < 1) { | |
| 662 throw new \InvalidArgumentException(strtoupper($key) . ' in RRULE must be a positive integer!'); | |
| 663 } | |
| 664 $key = strtolower($key); | |
| 665 $this->$key = $val; | |
| 666 break; | |
| 667 | |
| 668 case 'BYSECOND' : | |
| 669 $this->bySecond = (array)$value; | |
| 670 break; | |
| 671 | |
| 672 case 'BYMINUTE' : | |
| 673 $this->byMinute = (array)$value; | |
| 674 break; | |
| 675 | |
| 676 case 'BYHOUR' : | |
| 677 $this->byHour = (array)$value; | |
| 678 break; | |
| 679 | |
| 680 case 'BYDAY' : | |
| 681 $value = (array)$value; | |
| 682 foreach($value as $part) { | |
| 683 if (!preg_match('#^ (-|\+)? ([1-5])? (MO|TU|WE|TH|FR|SA|SU) $# xi', $part)) { | |
| 684 throw new \InvalidArgumentException('Invalid part in BYDAY clause: ' . $part); | |
| 685 } | |
| 686 } | |
| 687 $this->byDay = $value; | |
| 688 break; | |
| 689 | |
| 690 case 'BYMONTHDAY' : | |
| 691 $this->byMonthDay = (array)$value; | |
| 692 break; | |
| 693 | |
| 694 case 'BYYEARDAY' : | |
| 695 $this->byYearDay = (array)$value; | |
| 696 break; | |
| 697 | |
| 698 case 'BYWEEKNO' : | |
| 699 $this->byWeekNo = (array)$value; | |
| 700 break; | |
| 701 | |
| 702 case 'BYMONTH' : | |
| 703 $this->byMonth = (array)$value; | |
| 704 break; | |
| 705 | |
| 706 case 'BYSETPOS' : | |
| 707 $this->bySetPos = (array)$value; | |
| 708 break; | |
| 709 | |
| 710 case 'WKST' : | |
| 711 $this->weekStart = strtoupper($value); | |
| 712 break; | |
| 713 | |
| 714 default: | |
| 715 throw new \InvalidArgumentException('Not supported: ' . strtoupper($key)); | |
| 716 | |
| 717 } | |
| 718 | |
| 719 } | |
| 720 | |
| 721 } | |
| 722 | |
| 723 /** | |
| 724 * Mappings between the day number and english day name. | |
| 725 * | |
| 726 * @var array | |
| 727 */ | |
| 728 protected $dayNames = array( | |
| 729 0 => 'Sunday', | |
| 730 1 => 'Monday', | |
| 731 2 => 'Tuesday', | |
| 732 3 => 'Wednesday', | |
| 733 4 => 'Thursday', | |
| 734 5 => 'Friday', | |
| 735 6 => 'Saturday', | |
| 736 ); | |
| 737 | |
| 738 /** | |
| 739 * Returns all the occurrences for a monthly frequency with a 'byDay' or | |
| 740 * 'byMonthDay' expansion for the current month. | |
| 741 * | |
| 742 * The returned list is an array of integers with the day of month (1-31). | |
| 743 * | |
| 744 * @return array | |
| 745 */ | |
| 746 protected function getMonthlyOccurrences() { | |
| 747 | |
| 748 $startDate = clone $this->currentDate; | |
| 749 | |
| 750 $byDayResults = array(); | |
| 751 | |
| 752 // Our strategy is to simply go through the byDays, advance the date to | |
| 753 // that point and add it to the results. | |
| 754 if ($this->byDay) foreach($this->byDay as $day) { | |
| 755 | |
| 756 $dayName = $this->dayNames[$this->dayMap[substr($day,-2)]]; | |
| 757 | |
| 758 | |
| 759 // Dayname will be something like 'wednesday'. Now we need to find | |
| 760 // all wednesdays in this month. | |
| 761 $dayHits = array(); | |
| 762 | |
| 763 // workaround for missing 'first day of the month' support in hhvm | |
| 764 $checkDate = new \DateTime($startDate->format('Y-m-1')); | |
| 765 // workaround modify always advancing the date even if the current day is a $dayName in hhvm | |
| 766 if ($checkDate->format('l') !== $dayName) { | |
| 767 $checkDate->modify($dayName); | |
| 768 } | |
| 769 | |
| 770 do { | |
| 771 $dayHits[] = $checkDate->format('j'); | |
| 772 $checkDate->modify('next ' . $dayName); | |
| 773 } while ($checkDate->format('n') === $startDate->format('n')); | |
| 774 | |
| 775 // So now we have 'all wednesdays' for month. It is however | |
| 776 // possible that the user only really wanted the 1st, 2nd or last | |
| 777 // wednesday. | |
| 778 if (strlen($day)>2) { | |
| 779 $offset = (int)substr($day,0,-2); | |
| 780 | |
| 781 if ($offset>0) { | |
| 782 // It is possible that the day does not exist, such as a | |
| 783 // 5th or 6th wednesday of the month. | |
| 784 if (isset($dayHits[$offset-1])) { | |
| 785 $byDayResults[] = $dayHits[$offset-1]; | |
| 786 } | |
| 787 } else { | |
| 788 | |
| 789 // if it was negative we count from the end of the array | |
| 790 $byDayResults[] = $dayHits[count($dayHits) + $offset]; | |
| 791 } | |
| 792 } else { | |
| 793 // There was no counter (first, second, last wednesdays), so we | |
| 794 // just need to add the all to the list). | |
| 795 $byDayResults = array_merge($byDayResults, $dayHits); | |
| 796 | |
| 797 } | |
| 798 | |
| 799 } | |
| 800 | |
| 801 $byMonthDayResults = array(); | |
| 802 if ($this->byMonthDay) foreach($this->byMonthDay as $monthDay) { | |
| 803 | |
| 804 // Removing values that are out of range for this month | |
| 805 if ($monthDay > $startDate->format('t') || | |
| 806 $monthDay < 0-$startDate->format('t')) { | |
| 807 continue; | |
| 808 } | |
| 809 if ($monthDay>0) { | |
| 810 $byMonthDayResults[] = $monthDay; | |
| 811 } else { | |
| 812 // Negative values | |
| 813 $byMonthDayResults[] = $startDate->format('t') + 1 + $monthDay; | |
| 814 } | |
| 815 } | |
| 816 | |
| 817 // If there was just byDay or just byMonthDay, they just specify our | |
| 818 // (almost) final list. If both were provided, then byDay limits the | |
| 819 // list. | |
| 820 if ($this->byMonthDay && $this->byDay) { | |
| 821 $result = array_intersect($byMonthDayResults, $byDayResults); | |
| 822 } elseif ($this->byMonthDay) { | |
| 823 $result = $byMonthDayResults; | |
| 824 } else { | |
| 825 $result = $byDayResults; | |
| 826 } | |
| 827 $result = array_unique($result); | |
| 828 sort($result, SORT_NUMERIC); | |
| 829 | |
| 830 // The last thing that needs checking is the BYSETPOS. If it's set, it | |
| 831 // means only certain items in the set survive the filter. | |
| 832 if (!$this->bySetPos) { | |
| 833 return $result; | |
| 834 } | |
| 835 | |
| 836 $filteredResult = array(); | |
| 837 foreach($this->bySetPos as $setPos) { | |
| 838 | |
| 839 if ($setPos<0) { | |
| 840 $setPos = count($result)-($setPos+1); | |
| 841 } | |
| 842 if (isset($result[$setPos-1])) { | |
| 843 $filteredResult[] = $result[$setPos-1]; | |
| 844 } | |
| 845 } | |
| 846 | |
| 847 sort($filteredResult, SORT_NUMERIC); | |
| 848 return $filteredResult; | |
| 849 | |
| 850 } | |
| 851 | |
| 852 /** | |
| 853 * Simple mapping from iCalendar day names to day numbers | |
| 854 * | |
| 855 * @var array | |
| 856 */ | |
| 857 protected $dayMap = array( | |
| 858 'SU' => 0, | |
| 859 'MO' => 1, | |
| 860 'TU' => 2, | |
| 861 'WE' => 3, | |
| 862 'TH' => 4, | |
| 863 'FR' => 5, | |
| 864 'SA' => 6, | |
| 865 ); | |
| 866 | |
| 867 protected function getHours() | |
| 868 { | |
| 869 $recurrenceHours = array(); | |
| 870 foreach($this->byHour as $byHour) { | |
| 871 $recurrenceHours[] = $byHour; | |
| 872 } | |
| 873 | |
| 874 return $recurrenceHours; | |
| 875 } | |
| 876 | |
| 877 protected function getDays() { | |
| 878 | |
| 879 $recurrenceDays = array(); | |
| 880 foreach($this->byDay as $byDay) { | |
| 881 | |
| 882 // The day may be preceeded with a positive (+n) or | |
| 883 // negative (-n) integer. However, this does not make | |
| 884 // sense in 'weekly' so we ignore it here. | |
| 885 $recurrenceDays[] = $this->dayMap[substr($byDay,-2)]; | |
| 886 | |
| 887 } | |
| 888 | |
| 889 return $recurrenceDays; | |
| 890 } | |
| 891 | |
| 892 protected function getMonths() { | |
| 893 | |
| 894 $recurrenceMonths = array(); | |
| 895 foreach($this->byMonth as $byMonth) { | |
| 896 $recurrenceMonths[] = $byMonth; | |
| 897 } | |
| 898 | |
| 899 return $recurrenceMonths; | |
| 900 } | |
| 901 } |
