public function testLastDayOfMonthFixLeapYear()
 {
     $transformerConfig = new ArrayTransformerConfig();
     $transformerConfig->enableLastDayOfMonthFix();
     $this->transformer->setConfig($transformerConfig);
     $rule = new Rule('FREQ=YEARLY;COUNT=5;INTERVAL=1', new \DateTime('2016-02-29'));
     $computed = $this->transformer->transform($rule);
     $this->assertCount(5, $computed);
     $this->assertEquals(new \DateTime('2016-02-29'), $computed[0]->getStart());
     $this->assertEquals(new \DateTime('2017-02-28'), $computed[1]->getStart());
     $this->assertEquals(new \DateTime('2018-02-28'), $computed[2]->getStart());
     $this->assertEquals(new \DateTime('2019-02-28'), $computed[3]->getStart());
     $this->assertEquals(new \DateTime('2020-02-29'), $computed[4]->getStart());
 }
 public function testBySetPositionVirtualLimit()
 {
     $rule = new Rule('FREQ=MONTHLY;BYSETPOS=-1;BYDAY=MO,TU,WE,TH,FR', new \DateTime('2013-01-24'));
     $config = new ArrayTransformerConfig();
     $config->setVirtualLimit(5);
     $this->transformer->setConfig($config);
     $computed = $this->transformer->transform($rule);
     $this->assertCount(5, $computed);
     $this->assertEquals(new \DateTime('2013-01-31'), $computed[0]->getStart());
     $this->assertEquals(new \DateTime('2013-02-28'), $computed[1]->getStart());
     $this->assertEquals(new \DateTime('2013-03-29'), $computed[2]->getStart());
     $this->assertEquals(new \DateTime('2013-04-30'), $computed[3]->getStart());
     $this->assertEquals(new \DateTime('2013-05-31'), $computed[4]->getStart());
 }
 public function testMonthlyWithLastDayFixEnabledOnLeapYear()
 {
     $rule = new Rule('FREQ=MONTHLY;COUNT=8', new \DateTime('2016-01-31'));
     $transformerConfig = new ArrayTransformerConfig();
     $transformerConfig->enableLastDayOfMonthFix();
     $this->transformer->setConfig($transformerConfig);
     $computed = $this->transformer->transform($rule);
     $this->assertCount(8, $computed);
     $this->assertEquals(new \DateTime('2016-01-31'), $computed[0]->getStart());
     $this->assertEquals(new \DateTime('2016-02-29'), $computed[1]->getStart());
     $this->assertEquals(new \DateTime('2016-03-31'), $computed[2]->getStart());
     $this->assertEquals(new \DateTime('2016-04-30'), $computed[3]->getStart());
     $this->assertEquals(new \DateTime('2016-05-31'), $computed[4]->getStart());
     $this->assertEquals(new \DateTime('2016-06-30'), $computed[5]->getStart());
     $this->assertEquals(new \DateTime('2016-07-31'), $computed[6]->getStart());
     $this->assertEquals(new \DateTime('2016-08-31'), $computed[7]->getStart());
 }
 public function testDtendWithLastDayOfMonthFix()
 {
     $rule = new Rule('FREQ=MONTHLY;COUNT=5;DTEND=20140201T040000', new \DateTime('2014-01-31 04:00:00'));
     $config = new ArrayTransformerConfig();
     $config->enableLastDayOfMonthFix();
     $this->transformer->setConfig($config);
     $computed = $this->transformer->transform($rule);
     $this->assertCount(5, $computed);
     $this->assertEquals(new \DateTime('2014-01-31 04:00:00'), $computed[0]->getStart());
     $this->assertEquals(new \DateTime('2014-02-01 04:00:00'), $computed[0]->getEnd());
     $this->assertEquals(new \DateTime('2014-02-28 04:00:00'), $computed[1]->getStart());
     $this->assertEquals(new \DateTime('2014-03-01 04:00:00'), $computed[1]->getEnd());
     $this->assertEquals(new \DateTime('2014-03-31 04:00:00'), $computed[2]->getStart());
     $this->assertEquals(new \DateTime('2014-04-01 04:00:00'), $computed[2]->getEnd());
     $this->assertEquals(new \DateTime('2014-04-30 04:00:00'), $computed[3]->getStart());
     $this->assertEquals(new \DateTime('2014-05-01 04:00:00'), $computed[3]->getEnd());
     $this->assertEquals(new \DateTime('2014-05-31 04:00:00'), $computed[4]->getStart());
     $this->assertEquals(new \DateTime('2014-06-01 04:00:00'), $computed[4]->getEnd());
 }
Exemple #5
0
 /**
  * Transform a Rule in to an array of \DateTimes
  *
  * @param Rule $rule the Rule
  * @param int|null $virtualLimit imposed upon infinitely recurring events.
  * @param ConstraintInterface|null $constraint Potential recurrences must pass the constraint, else
  *                                             they will not be included in the returned collection.
  *
  * @return RecurrenceCollection
  * @throws MissingData
  */
 public function transform($rule, $virtualLimit = null, ConstraintInterface $constraint = null)
 {
     if (null === $rule) {
         throw new MissingData('Rule has not been set');
     }
     $start = $rule->getStartDate();
     $end = $rule->getEndDate();
     $until = $rule->getUntil();
     if (null === $start) {
         $start = new \DateTime('now', $until instanceof \DateTime ? $until->getTimezone() : null);
     }
     if (null === $end) {
         $end = $start;
     }
     $durationInterval = $start->diff($end);
     $startDay = $start->format('j');
     $startMonthLength = $start->format('t');
     $fixLastDayOfMonth = false;
     $dt = clone $start;
     $maxCount = $rule->getCount();
     $vLimit = !empty($virtualLimit) && is_int($virtualLimit) ? $virtualLimit : $this->getVirtualLimit();
     $freq = $rule->getFreq();
     $weekStart = $rule->getWeekStartAsNum();
     $bySecond = $rule->getBySecond();
     $byMinute = $rule->getByMinute();
     $byHour = $rule->getByHour();
     $byMonth = $rule->getByMonth();
     $byWeekNum = $rule->getByWeekNumber();
     $byYearDay = $rule->getByYearDay();
     $byMonthDay = $rule->getByMonthDay();
     $byMonthDayNeg = array();
     $byWeekDay = $rule->getByDayTransformedToWeekdays();
     $byWeekDayRel = array();
     $bySetPos = $rule->getBySetPosition();
     $implicitByMonthDay = false;
     if (!(!empty($byWeekNum) || !empty($byYearDay) || !empty($byMonthDay) || !empty($byWeekDay))) {
         switch ($freq) {
             case Frequency::YEARLY:
                 if (empty($byMonth)) {
                     $byMonth = array($start->format('n'));
                 }
                 if ($startDay > 28) {
                     $fixLastDayOfMonth = true;
                 }
                 $implicitByMonthDay = true;
                 $byMonthDay = array($startDay);
                 break;
             case Frequency::MONTHLY:
                 if ($startDay > 28) {
                     $fixLastDayOfMonth = true;
                 }
                 $implicitByMonthDay = true;
                 $byMonthDay = array($startDay);
                 break;
             case Frequency::WEEKLY:
                 $byWeekDay = array(new Weekday(DateUtil::getDayOfWeek($start), null));
                 break;
         }
     }
     if (!$this->config->isLastDayOfMonthFixEnabled()) {
         $fixLastDayOfMonth = false;
     }
     if (is_array($byMonthDay) && count($byMonthDay)) {
         foreach ($byMonthDay as $idx => $day) {
             if ($day < 0) {
                 unset($byMonthDay[$idx]);
                 $byMonthDayNeg[] = $day;
             }
         }
     }
     if (!empty($byWeekDay)) {
         foreach ($byWeekDay as $idx => $day) {
             /** @var $day Weekday */
             if (!empty($day->num)) {
                 $byWeekDayRel[] = $day;
                 unset($byWeekDay[$idx]);
             } else {
                 $byWeekDay[$idx] = $day->weekday;
             }
         }
     }
     if (empty($byYearDay)) {
         $byYearDay = null;
     }
     if (empty($byMonthDay)) {
         $byMonthDay = null;
     }
     if (empty($byMonthDayNeg)) {
         $byMonthDayNeg = null;
     }
     if (empty($byWeekDay)) {
         $byWeekDay = null;
     }
     if (!count($byWeekDayRel)) {
         $byWeekDayRel = null;
     }
     $year = $dt->format('Y');
     $month = $dt->format('n');
     $day = $dt->format('j');
     $hour = $dt->format('G');
     $minute = $dt->format('i');
     $second = $dt->format('s');
     $dates = array();
     $total = 1;
     $count = $maxCount;
     $continue = true;
     while ($continue) {
         $dtInfo = DateUtil::getDateInfo($dt);
         $tmp = DateUtil::getDaySet($rule, $dt, $dtInfo, $start);
         $daySet = $tmp->set;
         $daySetStart = $tmp->start;
         $daySetEnd = $tmp->end;
         $wNoMask = array();
         $wDayMaskRel = array();
         $timeSet = DateUtil::getTimeSet($rule, $dt);
         if ($freq >= Frequency::HOURLY) {
             if ($freq >= Frequency::HOURLY && !empty($byHour) && !in_array($hour, $byHour) || $freq >= Frequency::MINUTELY && !empty($byMinute) && !in_array($minute, $byMinute) || $freq >= Frequency::SECONDLY && !empty($bySecond) && !in_array($second, $bySecond)) {
                 $timeSet = array();
             } else {
                 switch ($freq) {
                     case Frequency::HOURLY:
                         $timeSet = DateUtil::getTimeSetOfHour($rule, $dt);
                         break;
                     case Frequency::MINUTELY:
                         $timeSet = DateUtil::getTimeSetOfMinute($rule, $dt);
                         break;
                     case Frequency::SECONDLY:
                         $timeSet = DateUtil::getTimeSetOfSecond($dt);
                         break;
                 }
             }
         }
         // Handle byWeekNum
         if (!empty($byWeekNum)) {
             $no1WeekStart = $firstWeekStart = DateUtil::pymod(7 - $dtInfo->dayOfWeekYearDay1 + $weekStart, 7);
             if ($no1WeekStart >= 4) {
                 $no1WeekStart = 0;
                 $wYearLength = $dtInfo->yearLength + DateUtil::pymod($dtInfo->dayOfWeekYearDay1 - $weekStart, 7);
             } else {
                 $wYearLength = $dtInfo->yearLength - $no1WeekStart;
             }
             $div = floor($wYearLength / 7);
             $mod = DateUtil::pymod($wYearLength, 7);
             $numWeeks = floor($div + $mod / 4);
             foreach ($byWeekNum as $weekNum) {
                 if ($weekNum < 0) {
                     $weekNum += $numWeeks + 1;
                 }
                 if (!(0 < $weekNum && $weekNum <= $numWeeks)) {
                     continue;
                 }
                 if ($weekNum > 1) {
                     $offset = $no1WeekStart + ($weekNum - 1) * 7;
                     if ($no1WeekStart != $firstWeekStart) {
                         $offset -= 7 - $firstWeekStart;
                     }
                 } else {
                     $offset = $no1WeekStart;
                 }
                 for ($i = 0; $i < 7; $i++) {
                     $wNoMask[] = $offset;
                     $offset++;
                     if ($dtInfo->wDayMask[$offset] == $weekStart) {
                         break;
                     }
                 }
             }
             // Check week number 1 of next year as well
             if (in_array(1, $byWeekNum)) {
                 $offset = $no1WeekStart + $numWeeks * 7;
                 if ($no1WeekStart != $firstWeekStart) {
                     $offset -= 7 - $firstWeekStart;
                 }
                 // If week starts in next year, we don't care about it.
                 if ($offset < $dtInfo->yearLength) {
                     for ($k = 0; $k < 7; $k++) {
                         $wNoMask[] = $offset;
                         $offset += 1;
                         if ($dtInfo->wDayMask[$offset] == $weekStart) {
                             break;
                         }
                     }
                 }
             }
             if ($no1WeekStart) {
                 // Check last week number of last year as well.
                 // If $no1WeekStart is 0, either the year started on week start,
                 // or week number 1 got days from last year, so there are no
                 // days from last year's last week number in this year.
                 if (!in_array(-1, $byWeekNum)) {
                     $dtTmp = new \DateTime();
                     $dtTmp->setDate($year - 1, 1, 1);
                     $lastYearWeekDay = DateUtil::getDayOfWeek($dtTmp);
                     $lastYearNo1WeekStart = DateUtil::pymod(7 - $lastYearWeekDay + $weekStart, 7);
                     $lastYearLength = DateUtil::getYearLength($dtTmp);
                     if ($lastYearNo1WeekStart >= 4) {
                         $lastYearNo1WeekStart = 0;
                         $lastYearNumWeeks = floor(52 + DateUtil::pymod($lastYearLength + DateUtil::pymod($lastYearWeekDay - $weekStart, 7), 7) / 4);
                     } else {
                         $lastYearNumWeeks = floor(52 + DateUtil::pymod($dtInfo->yearLength - $no1WeekStart, 7) / 4);
                     }
                 } else {
                     $lastYearNumWeeks = -1;
                 }
                 if (in_array($lastYearNumWeeks, $byWeekNum)) {
                     for ($i = 0; $i < $no1WeekStart; $i++) {
                         $wNoMask[] = $i;
                     }
                 }
             }
         }
         // Handle relative weekdays (e.g. 3rd Friday of month)
         if (!empty($byWeekDayRel)) {
             $ranges = array();
             if (Frequency::YEARLY == $freq) {
                 if (!empty($byMonth)) {
                     foreach ($byMonth as $mo) {
                         $ranges[] = array_slice($dtInfo->mRanges, $mo - 1, 2);
                     }
                 } else {
                     $ranges[] = array(0, $dtInfo->yearLength);
                 }
             } elseif (Frequency::MONTHLY == $freq) {
                 $ranges[] = array_slice($dtInfo->mRanges, $month - 1, 2);
             }
             if (!empty($ranges)) {
                 foreach ($ranges as $range) {
                     $rangeStart = $range[0];
                     $rangeEnd = $range[1];
                     --$rangeEnd;
                     reset($byWeekDayRel);
                     foreach ($byWeekDayRel as $weekday) {
                         /** @var Weekday $weekday */
                         if ($weekday->num < 0) {
                             $i = $rangeEnd + ($weekday->num + 1) * 7;
                             $i -= DateUtil::pymod($dtInfo->wDayMask[$i] - $weekday->weekday, 7);
                         } else {
                             $i = $rangeStart + ($weekday->num - 1) * 7;
                             $i += DateUtil::pymod(7 - $dtInfo->wDayMask[$i] + $weekday->weekday, 7);
                         }
                         if ($rangeStart <= $i && $i <= $rangeEnd) {
                             $wDayMaskRel[] = $i;
                         }
                     }
                 }
             }
         }
         $numMatched = 0;
         foreach ($daySet as $i => $dayOfYear) {
             $dayOfMonth = $dtInfo->mDayMask[$dayOfYear];
             $ifByMonth = $byMonth !== null && !in_array($dtInfo->mMask[$dayOfYear], $byMonth);
             $ifByWeekNum = $byWeekNum !== null && !in_array($i, $wNoMask);
             $ifByYearDay = $byYearDay !== null && ($i < $dtInfo->yearLength && !in_array($i + 1, $byYearDay) && !in_array(-$dtInfo->yearLength + $i, $byYearDay) || $i >= $dtInfo->yearLength && !in_array($i + 1 - $dtInfo->yearLength, $byYearDay) && !in_array(-$dtInfo->nextYearLength + $i - $dtInfo->yearLength, $byYearDay));
             $ifByMonthDay = $byMonthDay !== null && !in_array($dtInfo->mDayMask[$dayOfYear], $byMonthDay);
             // Handle "last day of next month" problem.
             if ($fixLastDayOfMonth && $ifByMonthDay && $implicitByMonthDay && $startMonthLength > $dtInfo->monthLength && $dayOfMonth == $dtInfo->monthLength && $dayOfMonth < $startMonthLength && !$numMatched) {
                 $ifByMonthDay = false;
             }
             $ifByMonthDayNeg = $byMonthDayNeg !== null && !in_array($dtInfo->mDayMaskNeg[$dayOfYear], $byMonthDayNeg);
             $ifByDay = $byWeekDay !== null && count($byWeekDay) && !in_array($dtInfo->wDayMask[$dayOfYear], $byWeekDay);
             $ifWDayMaskRel = $byWeekDayRel !== null && !in_array($dayOfYear, $wDayMaskRel);
             if ($byMonthDay !== null && $byMonthDayNeg !== null) {
                 if ($ifByMonthDay && $ifByMonthDayNeg) {
                     unset($daySet[$i]);
                 }
             } elseif ($ifByMonth || $ifByWeekNum || $ifByYearDay || $ifByMonthDay || $ifByMonthDayNeg || $ifByDay || $ifWDayMaskRel) {
                 unset($daySet[$i]);
             } else {
                 ++$numMatched;
             }
         }
         if (!empty($bySetPos)) {
             $datesAdj = array();
             $tmpDaySet = array_combine($daySet, $daySet);
             foreach ($bySetPos as $setPos) {
                 if ($setPos < 0) {
                     $dayPos = floor($setPos / count($timeSet));
                     $timePos = DateUtil::pymod($setPos, count($timeSet));
                 } else {
                     $dayPos = floor(($setPos - 1) / count($timeSet));
                     $timePos = DateUtil::pymod($setPos - 1, count($timeSet));
                 }
                 $tmp = array();
                 for ($k = $daySetStart; $k <= $daySetEnd; $k++) {
                     if (!array_key_exists($k, $tmpDaySet)) {
                         continue;
                     }
                     $tmp[] = $tmpDaySet[$k];
                 }
                 if ($dayPos < 0) {
                     $nextInSet = array_slice($tmp, $dayPos, 1);
                     $nextInSet = $nextInSet[0];
                 } else {
                     $nextInSet = $tmp[$dayPos];
                 }
                 /** @var Time $time */
                 $time = $timeSet[$timePos];
                 $dtTmp = DateUtil::getDateTimeByDayOfYear($nextInSet, $dt->format('Y'), $start->getTimezone());
                 $dtTmp->setTime($time->hour, $time->minute, $time->second);
                 $datesAdj[] = $dtTmp;
             }
             foreach ($datesAdj as $dtTmp) {
                 if (null !== $until && $dtTmp > $until) {
                     $continue = false;
                     break;
                 }
                 if ($dtTmp < $start) {
                     continue;
                 }
                 if ($constraint instanceof ConstraintInterface && !$constraint->test($dtTmp)) {
                     if ($constraint->stopsTransformer()) {
                         $continue = false;
                         break;
                     } else {
                         continue;
                     }
                 }
                 $dates[] = $dtTmp;
                 if (null !== $count) {
                     --$count;
                     if ($count <= 0) {
                         $continue = false;
                         break;
                     }
                 }
                 ++$total;
                 if ($total > $vLimit) {
                     $continue = false;
                     break;
                 }
             }
         } else {
             foreach ($daySet as $dayOfYear) {
                 $dtTmp = DateUtil::getDateTimeByDayOfYear($dayOfYear, $dt->format('Y'), $start->getTimezone());
                 foreach ($timeSet as $time) {
                     /** @var Time $time */
                     $dtTmp->setTime($time->hour, $time->minute, $time->second);
                     if (null !== $until && $dtTmp > $until) {
                         $continue = false;
                         break;
                     }
                     if ($dtTmp < $start) {
                         continue;
                     }
                     if ($constraint instanceof ConstraintInterface && !$constraint->test($dtTmp)) {
                         if ($constraint->stopsTransformer()) {
                             $continue = false;
                             break;
                         } else {
                             continue;
                         }
                     }
                     $dates[] = clone $dtTmp;
                     if (null !== $count) {
                         --$count;
                         if ($count <= 0) {
                             $continue = false;
                             break;
                         }
                     }
                     ++$total;
                     if ($total > $vLimit) {
                         $continue = false;
                         break;
                     }
                 }
                 if (!$continue) {
                     break;
                 }
             }
             if ($total > $vLimit) {
                 $continue = false;
                 break;
             }
         }
         switch ($freq) {
             case Frequency::YEARLY:
                 $year += $rule->getInterval();
                 $month = $dt->format('n');
                 $day = $dt->format('j');
                 $dt->setDate($year, $month, 1);
                 break;
             case Frequency::MONTHLY:
                 $month += $rule->getInterval();
                 if ($month > 12) {
                     $delta = floor($month / 12);
                     $mod = DateUtil::pymod($month, 12);
                     $month = $mod;
                     $year += $delta;
                     if ($month == 0) {
                         $month = 12;
                         --$year;
                     }
                 }
                 $dt->setDate($year, $month, 1);
                 break;
             case Frequency::WEEKLY:
                 if ($weekStart > $dtInfo->dayOfWeek) {
                     $delta = ($dtInfo->dayOfWeek + 1 + (6 - $weekStart)) * -1 + $rule->getInterval() * 7;
                 } else {
                     $delta = ($dtInfo->dayOfWeek - $weekStart) * -1 + $rule->getInterval() * 7;
                 }
                 $dt->modify("+{$delta} day");
                 $year = $dt->format('Y');
                 $month = $dt->format('n');
                 $day = $dt->format('j');
                 break;
             case Frequency::DAILY:
                 $dt->modify('+' . $rule->getInterval() . ' day');
                 $year = $dt->format('Y');
                 $month = $dt->format('n');
                 $day = $dt->format('j');
                 break;
             case Frequency::HOURLY:
                 $dt->modify('+' . $rule->getInterval() . ' hours');
                 $year = $dt->format('Y');
                 $month = $dt->format('n');
                 $day = $dt->format('j');
                 $hour = $dt->format('G');
                 break;
             case Frequency::MINUTELY:
                 $dt->modify('+' . $rule->getInterval() . ' minutes');
                 $year = $dt->format('Y');
                 $month = $dt->format('n');
                 $day = $dt->format('j');
                 $hour = $dt->format('G');
                 $minute = $dt->format('i');
                 break;
             case Frequency::SECONDLY:
                 $dt->modify('+' . $rule->getInterval() . ' seconds');
                 $year = $dt->format('Y');
                 $month = $dt->format('n');
                 $day = $dt->format('j');
                 $hour = $dt->format('G');
                 $minute = $dt->format('i');
                 $second = $dt->format('s');
                 break;
         }
     }
     /** @var Recurrence[] $recurrences */
     $recurrences = array();
     foreach ($dates as $start) {
         /** @var \DateTime $end */
         $end = clone $start;
         $recurrences[] = new Recurrence($start, $end->add($durationInterval));
     }
     $recurrences = $this->handleExclusions($rule->getExDates(), $recurrences);
     return new RecurrenceCollection($recurrences);
 }
 public function testYearlyOnLeapYearWithLastDayOfMonthFix()
 {
     $transformerConfig = new ArrayTransformerConfig();
     $transformerConfig->enableLastDayOfMonthFix();
     $this->transformer->setConfig($transformerConfig);
     $rule = new Rule('FREQ=YEARLY;COUNT=5;BYHOUR=14,15', new \DateTime('2016-02-29 12:00:00'));
     $computed = $this->transformer->transform($rule);
     $this->assertCount(5, $computed);
     $this->assertEquals(new \DateTime('2016-02-29 14:00:00'), $computed[0]->getStart());
     $this->assertEquals(new \DateTime('2016-02-29 15:00:00'), $computed[1]->getStart());
     $this->assertEquals(new \DateTime('2017-02-28 14:00:00'), $computed[2]->getStart());
     $this->assertEquals(new \DateTime('2017-02-28 15:00:00'), $computed[3]->getStart());
     $this->assertEquals(new \DateTime('2018-02-28 14:00:00'), $computed[4]->getStart());
 }
 public function testLastDayOfMonthFixOn29thDayIn30DayMonth()
 {
     $transformerConfig = new ArrayTransformerConfig();
     $transformerConfig->enableLastDayOfMonthFix();
     $this->transformer->setConfig($transformerConfig);
     $rule = new Rule('FREQ=MONTHLY;COUNT=4;INTERVAL=1', new \DateTime('2014-08-29'));
     $computed = $this->transformer->transform($rule);
     $this->assertCount(4, $computed);
     $this->assertEquals(new \DateTime('2014-08-29'), $computed[0]->getStart());
     $this->assertEquals(new \DateTime('2014-09-29'), $computed[1]->getStart());
     $this->assertEquals(new \DateTime('2014-10-29'), $computed[2]->getStart());
     $this->assertEquals(new \DateTime('2014-11-29'), $computed[3]->getStart());
 }