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