diff options
Diffstat (limited to 'vendor/simshaun/recurr/src/Recurr/Transformer/ArrayTransformer.php')
-rw-r--r-- | vendor/simshaun/recurr/src/Recurr/Transformer/ArrayTransformer.php | 736 |
1 files changed, 736 insertions, 0 deletions
diff --git a/vendor/simshaun/recurr/src/Recurr/Transformer/ArrayTransformer.php b/vendor/simshaun/recurr/src/Recurr/Transformer/ArrayTransformer.php new file mode 100644 index 0000000..414a3e6 --- /dev/null +++ b/vendor/simshaun/recurr/src/Recurr/Transformer/ArrayTransformer.php @@ -0,0 +1,736 @@ +<?php + +/* + * Copyright 2013 Shaun Simmons + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * Based on: + * rrule.js - Library for working with recurrence rules for calendar dates. + * Copyright 2010, Jakub Roztocil and Lars Schoning + * https://github.com/jkbr/rrule/blob/master/LICENCE + */ + +namespace Recurr\Transformer; + +use Recurr\DateExclusion; +use Recurr\DateInclusion; +use Recurr\Exception\InvalidWeekday; +use Recurr\Frequency; +use Recurr\Recurrence; +use Recurr\RecurrenceCollection; +use Recurr\Rule; +use Recurr\Time; +use Recurr\Weekday; +use Recurr\DateUtil; + +/** + * This class is responsible for transforming a Rule in to an array + * of \DateTimeInterface objects. + * + * If a recurrence rule is infinitely recurring, a virtual limit is imposed. + * + * @package Recurr + * @author Shaun Simmons <shaun@envysphere.com> + */ +class ArrayTransformer +{ + /** @var ArrayTransformerConfig */ + protected $config; + + /** + * Some versions of PHP are affected by a bug where + * \DateTimeInterface::createFromFormat('z Y', ...) does not account for leap years. + * + * @var bool + */ + protected $leapBug = false; + + /** + * Construct a new ArrayTransformer + * + * @param ArrayTransformerConfig $config + */ + public function __construct(ArrayTransformerConfig $config = null) + { + if (!$config instanceof ArrayTransformerConfig) { + $config = new ArrayTransformerConfig(); + } + + $this->config = $config; + + $this->leapBug = DateUtil::hasLeapYearBug(); + } + + /** + * @param ArrayTransformerConfig $config + */ + public function setConfig($config) + { + $this->config = $config; + } + + /** + * Transform a Rule in to an array of \DateTimeInterface objects + * + * @param Rule $rule the Rule object + * @param ConstraintInterface|null $constraint Potential recurrences must pass the constraint, else + * they will not be included in the returned collection. + * @param bool $countConstraintFailures Whether recurrences that fail the constraint's test + * should count towards a rule's COUNT limit. + * + * @return RecurrenceCollection|Recurrence[] + * @throws InvalidWeekday + */ + public function transform(Rule $rule, ConstraintInterface $constraint = null, $countConstraintFailures = true) + { + $start = $rule->getStartDate(); + $end = $rule->getEndDate(); + $until = $rule->getUntil(); + + if (null === $start) { + $start = new \DateTime( + 'now', $until instanceof \DateTimeInterface ? $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 = $this->config->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'); + $hour = $dt->format('G'); + $minute = $dt->format('i'); + $second = $dt->format('s'); + + $dates = array(); + $total = 1; + $count = $maxCount; + $continue = true; + $iterations = 0; + 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 = $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) && !empty($daySet)) { + $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); + if (count($nextInSet) === 0) { + continue; + } + $nextInSet = $nextInSet[0]; + } else { + $nextInSet = isset($tmp[$dayPos]) ? $tmp[$dayPos] : null; + } + + if (null !== $nextInSet) { + /** @var Time $time */ + $time = $timeSet[$timePos]; + + $dtTmp = DateUtil::getDateTimeByDayOfYear($nextInSet, $dt->format('Y'), $start->getTimezone()); + + $dtTmp = $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 (!$countConstraintFailures) { + if ($constraint->stopsTransformer()) { + $continue = false; + break; + } else { + continue; + } + } + } else { + $dates[$total] = $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 = $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 (!$countConstraintFailures) { + if ($constraint->stopsTransformer()) { + $continue = false; + break; + } else { + continue; + } + } + } else { + $dates[$total] = 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'); + $dt = $dt->setDate($year, $month, 1); + + // Stop an infinite loop w/ a sane limit + ++$iterations; + if ($iterations > 300 && !count($dates)) { + break 2; + } + 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 = $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 = $dt->modify("+$delta day"); + $year = $dt->format('Y'); + $month = $dt->format('n'); + break; + case Frequency::DAILY: + $dt = $dt->modify('+'.$rule->getInterval().' day'); + $year = $dt->format('Y'); + $month = $dt->format('n'); + break; + case Frequency::HOURLY: + $dt = $dt->modify('+'.$rule->getInterval().' hours'); + $year = $dt->format('Y'); + $month = $dt->format('n'); + $hour = $dt->format('G'); + break; + case Frequency::MINUTELY: + $dt = $dt->modify('+'.$rule->getInterval().' minutes'); + $year = $dt->format('Y'); + $month = $dt->format('n'); + $hour = $dt->format('G'); + $minute = $dt->format('i'); + break; + case Frequency::SECONDLY: + $dt = $dt->modify('+'.$rule->getInterval().' seconds'); + $year = $dt->format('Y'); + $month = $dt->format('n'); + $hour = $dt->format('G'); + $minute = $dt->format('i'); + $second = $dt->format('s'); + break; + } + } + + /** @var Recurrence[] $recurrences */ + $recurrences = array(); + foreach ($dates as $key => $start) { + /** @var \DateTimeInterface $end */ + $end = clone $start; + + $recurrences[] = new Recurrence($start, $end->add($durationInterval), $key); + } + + $recurrences = $this->handleInclusions($rule->getRDates(), $recurrences); + $recurrences = $this->handleExclusions($rule->getExDates(), $recurrences); + + return new RecurrenceCollection($recurrences); + } + + /** + * @param DateExclusion[] $exclusions + * @param Recurrence[] $recurrences + * + * @return Recurrence[] + */ + protected function handleExclusions(array $exclusions, array $recurrences) + { + foreach ($exclusions as $exclusion) { + $exclusionDate = $exclusion->date->format('Ymd'); + $exclusionTime = $exclusion->date->format('Ymd\THis'); + $exclusionTimezone = $exclusion->date->getTimezone(); + + foreach ($recurrences as $key => $recurrence) { + $recurrenceDate = $recurrence->getStart(); + + if ($recurrenceDate->getTimezone()->getName() !== $exclusionTimezone->getName()) { + $recurrenceDate = clone $recurrenceDate; + $recurrenceDate = $recurrenceDate->setTimezone($exclusionTimezone); + } + + if (!$exclusion->hasTime && $recurrenceDate->format('Ymd') == $exclusionDate) { + unset($recurrences[$key]); + continue; + } + + if ($exclusion->hasTime && $recurrenceDate->format('Ymd\THis') == $exclusionTime) { + unset($recurrences[$key]); + } + } + } + + return array_values($recurrences); + } + + /** + * @param DateInclusion[] $inclusions + * @param Recurrence[] $recurrences + * + * @return Recurrence[] + */ + protected function handleInclusions(array $inclusions, array $recurrences) + { + foreach ($inclusions as $inclusion) { + $recurrence = new Recurrence(clone $inclusion->date, clone $inclusion->date); + $recurrences[] = $recurrence; + } + + return array_values($recurrences); + } +} |