vendor/rlanvin/php-rrule/src/RSet.php line 17

Open in your IDE?
  1. <?php
  2. /**
  3.  * Licensed under the MIT license.
  4.  *
  5.  * For the full copyright and license information, please view the LICENSE file.
  6.  *
  7.  * @author RĂ©mi Lanvin <remi@cloudconnected.fr>
  8.  * @link https://github.com/rlanvin/php-rrule
  9.  */
  10. namespace RRule;
  11. /**
  12.  * Recurrence set
  13.  */
  14. class RSet implements RRuleInterface
  15. {
  16.     /**
  17.      * @var array List of RDATE (single dates)
  18.      */
  19.     protected $rdates = array();
  20.     /**
  21.      * @var array List of RRULE
  22.      */
  23.     protected $rrules = array();
  24.     /**
  25.      * @var array List of EXDATE (single dates to be excluded)
  26.      */
  27.     protected $exdates = array();
  28.     /**
  29.      * @var array List of EXRULES (single rules to be excluded)
  30.      */
  31.     protected $exrules = array();
  32.     // cache variable
  33.     /**
  34.      * @var int|null Cache for the total number of occurrences
  35.      */
  36.     protected $total null;
  37.     /**
  38.      * @var int|null Cache for the finite status of the RSet
  39.      */
  40.     protected $infinite null;
  41.     /**
  42.      * @var array Cache for all the occurrences
  43.      */
  44.     protected $cache = array();
  45.     /**
  46.      * Constructor
  47.      *
  48.      * @param string $string a RFC compliant text block
  49.      */
  50.     public function __construct($string null$default_dtstart null)
  51.     {
  52.         if ( $string && is_string($string) ) {
  53.             $string trim($string);
  54.             $rrules = array();
  55.             $exrules = array();
  56.             $rdates = array();
  57.             $exdates = array();
  58.             $dtstart null;
  59.             // parse
  60.             $lines explode("\n"$string);
  61.             foreach ( $lines as $line ) {
  62.                 $line trim($line);
  63.                 if ( strpos($line,':') === false ) {
  64.                     throw new \InvalidArgumentException('Failed to parse RFC string, line is not starting with a property name followed by ":"');
  65.                 }
  66.                 list($property_name,$property_value) = explode(':',$line);
  67.                 $tmp explode(";",$property_name);
  68.                 $property_name $tmp[0];
  69.                 switch ( strtoupper($property_name) ) {
  70.                     case 'DTSTART':
  71.                         if ( $default_dtstart || $dtstart !== null ) {
  72.                             throw new \InvalidArgumentException('Failed to parse RFC string, multiple DTSTART found');
  73.                         }
  74.                         $dtstart $line;
  75.                     break;
  76.                     case 'RRULE':
  77.                         $rrules[] = $line;
  78.                     break;
  79.                     case 'EXRULE':
  80.                         $exrules[] = $line;
  81.                     break;
  82.                     case 'RDATE':
  83.                         $rdates array_merge($rdatesRfcParser::parseRDate($line));
  84.                     break;
  85.                     case 'EXDATE':
  86.                         $exdates array_merge($exdatesRfcParser::parseExDate($line));
  87.                     break;
  88.                     default:
  89.                         throw new \InvalidArgumentException("Failed to parse RFC, unknown property: $property_name");
  90.                 }
  91.             }
  92.             foreach ( $rrules as $rrule ) {
  93.                 if ( $dtstart ) {
  94.                     $rrule $dtstart."\n".$rrule;
  95.                 }
  96.                 $this->addRRule(new RRule($rrule$default_dtstart));
  97.             }
  98.             foreach ( $exrules as $rrule ) {
  99.                 if ( $dtstart ) {
  100.                     $rrule $dtstart."\n".$rrule;
  101.                 }
  102.                 $this->addExRule(new RRule($rrule$default_dtstart));
  103.             }
  104.             foreach ( $rdates as $date ) {
  105.                 $this->addDate($date);
  106.             }
  107.             foreach ( $exdates as $date ) {
  108.                 $this->addExDate($date);
  109.             }
  110.         }
  111.     }
  112.     /**
  113.      * Add a RRule (or another RSet)
  114.      *
  115.      * @param mixed $rrule an instance of RRuleInterface or something that can be transformed into a RRule (string or array)
  116.      * @return $this
  117.      */
  118.     public function addRRule($rrule)
  119.     {
  120.         if ( is_string($rrule) || is_array($rrule) ) {
  121.             $rrule = new RRule($rrule);
  122.         }
  123.         elseif ( ! $rrule instanceof RRuleInterface ) {
  124.             throw new \InvalidArgumentException('The rule must be a string, an array, or implement RRuleInterface');
  125.         }
  126.         // cloning because I want to iterate it without being disturbed
  127.         $this->rrules[] = clone $rrule;
  128.         $this->clearCache();
  129.         return $this;
  130.     }
  131.     /**
  132.      * Return the RRULE(s) contained in this set
  133.      *
  134.      * @todo check if a deep copy is needed.
  135.      *
  136.      * @return array Array of RRule
  137.      */
  138.     public function getRRules()
  139.     {
  140.         return $this->rrules;
  141.     }
  142.     /**
  143.      * Add a RRule with exclusion rules.
  144.      * In RFC 2445 but deprecated in RFC 5545
  145.      *
  146.      * @param mixed $rrule an instance of RRuleInterface or something that can be transformed into a RRule (string or array)
  147.      * @return $this
  148.      */
  149.     public function addExRule($rrule)
  150.     {
  151.         if ( is_string($rrule) || is_array($rrule) ) {
  152.             $rrule = new RRule($rrule);
  153.         }
  154.         elseif ( ! $rrule instanceof RRuleInterface ) {
  155.             throw new \InvalidArgumentException('The rule must be a string, an array or implement RRuleInterface');
  156.         }
  157.         // cloning because I want to iterate it without being disturbed
  158.         $this->exrules[] = clone $rrule;
  159.         $this->clearCache();
  160.         return $this;
  161.     }
  162.     /**
  163.      * Return the EXRULE(s) contained in this set
  164.      *
  165.      * @todo check if a deep copy is needed.
  166.      *
  167.      * @return array Array of RRule
  168.      */
  169.     public function getExRules()
  170.     {
  171.         return $this->exrules;
  172.     }
  173.     /**
  174.      * Add a RDATE (renamed Date for simplicy, since we don't support full RDATE syntax at the moment)
  175.      *
  176.      * @param mixed $date a valid date representation or a \DateTime object
  177.      * @return $this
  178.      */
  179.     public function addDate($date)
  180.     {
  181.         try {
  182.             $this->rdates[] = RRule::parseDate($date);
  183.             sort($this->rdates);
  184.         } catch (\Exception $e) {
  185.             throw new \InvalidArgumentException(
  186.                 'Failed to parse RDATE - it must be a valid date, timestamp or \DateTime object'
  187.             );
  188.         }
  189.         $this->clearCache();
  190.         return $this;
  191.     }
  192.     /**
  193.      * Return the RDATE(s) contained in this set
  194.      *
  195.      * @todo check if a deep copy is needed.
  196.      *
  197.      * @return array Array of \DateTime
  198.      */
  199.     public function getDates()
  200.     {
  201.         return $this->rdates;
  202.     }
  203.     /**
  204.      * Add a EXDATE
  205.      *
  206.      * @param mixed $date a valid date representation or a \DateTime object
  207.      * @return $this
  208.      */
  209.     public function addExDate($date)
  210.     {
  211.         try {
  212.             $this->exdates[] = RRule::parseDate($date);
  213.             sort($this->exdates);
  214.         } catch (\Exception $e) {
  215.             throw new \InvalidArgumentException(
  216.                 'Failed to parse EXDATE - it must be a valid date, timestamp or \DateTime object'
  217.             );
  218.         }
  219.         $this->clearCache();
  220.         return $this;
  221.     }
  222.     /**
  223.      * Return the EXDATE(s) contained in this set
  224.      *
  225.      * @todo check if a deep copy is needed.
  226.      *
  227.      * @return array Array of \DateTime
  228.      */
  229.     public function getExDates()
  230.     {
  231.         return $this->exdates;
  232.     }
  233.     /**
  234.      * Clear the cache.
  235.      * Do NOT use while the class is iterating.
  236.      * @return $this
  237.      */
  238.     public function clearCache()
  239.     {
  240.         $this->total null;
  241.         $this->infinite null;
  242.         $this->cache = array();
  243.         $this->rlist_heap null;
  244.         $this->rlist_iterator null;
  245.         $this->exlist_heap null;
  246.         $this->exlist_iterator null;
  247.         return $this;
  248.     }
  249. ///////////////////////////////////////////////////////////////////////////////
  250. // RRule interface
  251.     /**
  252.      * Return true if the rrule has an end condition, false otherwise
  253.      *
  254.      * @return bool
  255.      */
  256.     public function isFinite()
  257.     {
  258.         return ! $this->isInfinite();
  259.     }
  260.     /**
  261.      * Return true if the rrule has no end condition (infite)
  262.      *
  263.      * @return bool
  264.      */
  265.     public function isInfinite()
  266.     {
  267.         if ( $this->infinite === null ) {
  268.             $this->infinite false;
  269.             foreach ( $this->rrules as $rrule ) {
  270.                 if ( $rrule->isInfinite() ) {
  271.                     $this->infinite true;
  272.                     break;
  273.                 }
  274.             }
  275.         }
  276.         return $this->infinite;
  277.     }
  278.     /**
  279.      * Return all the occurrences in an array of \DateTime.
  280.      *
  281.      * @param int $limit Limit the resultset to n occurrences (0, null or false = everything)
  282.      * @return array An array of \DateTime objects
  283.      */
  284.     public function getOccurrences($limit null)
  285.     {
  286.         if ( !$limit && $this->isInfinite() ) {
  287.             throw new \LogicException('Cannot get all occurrences of an infinite recurrence set.');
  288.         }
  289.         // cached version already computed
  290.         $iterator $this;
  291.         if ( $this->total !== null ) {
  292.             $iterator $this->cache;
  293.         }
  294.         $res = array();
  295.         $n 0;
  296.         foreach ( $iterator as $occurrence ) {
  297.             $res[] = clone $occurrence// we have to clone because DateTime is not immutable
  298.             $n += 1;
  299.             if ( $limit && $n >= $limit ) {
  300.                 break;
  301.             }
  302.         }
  303.         return $res;
  304.     }
  305.     /**
  306.      * Return all the ocurrences after a date, before a date, or between two dates.
  307.      *
  308.      * @param mixed $begin Can be null to return all occurrences before $end
  309.      * @param mixed $end Can be null to return all occurrences after $begin
  310.      * @param int $limit Limit the resultset to n occurrences (0, null or false = everything)
  311.      * @return array An array of \DateTime objects
  312.      */
  313.     public function getOccurrencesBetween($begin$end$limit null)
  314.     {
  315.         if ( $begin !== null ) {
  316.             $begin RRule::parseDate($begin);
  317.         }
  318.         if ( $end !== null ) {
  319.             $end RRule::parseDate($end);
  320.         }
  321.         elseif ( ! $limit && $this->isInfinite() ) {
  322.             throw new \LogicException('Cannot get all occurrences of an infinite recurrence rule.');
  323.         }
  324.         $iterator $this;
  325.         if ( $this->total !== null ) {
  326.             $iterator $this->cache;
  327.         }
  328.         $res = array();
  329.         $n 0;
  330.         foreach ( $iterator as $occurrence ) {
  331.             if ( $begin !== null && $occurrence $begin ) {
  332.                 continue;
  333.             }
  334.             if ( $end !== null && $occurrence $end ) {
  335.                 break;
  336.             }
  337.             $res[] = clone $occurrence;
  338.             $n += 1;
  339.             if ( $limit && $n >= $limit ) {
  340.                 break;
  341.             }
  342.         }
  343.         return $res;
  344.     }
  345.     /**
  346.      * Return true if $date is an occurrence.
  347.      *
  348.      * @param mixed $date
  349.      * @return bool
  350.      */
  351.     public function occursAt($date)
  352.     {
  353.         $date RRule::parseDate($date);
  354.         if ( in_array($date$this->cache) ) {
  355.             // in the cache (whether cache is complete or not)
  356.             return true;
  357.         }
  358.         elseif ( $this->total !== null ) {
  359.             // cache complete and not in cache
  360.             return false;
  361.         }
  362.         // test if it *should* occur (before exclusion)
  363.         $occurs false;
  364.         foreach ( $this->rdates as $rdate ) {
  365.             if ( $rdate == $date ) {
  366.                 $occurs true;
  367.                 break;
  368.             }
  369.         }
  370.         if ( ! $occurs ) {
  371.             foreach ( $this->rrules as $rrule ) {
  372.                 if ( $rrule->occursAt($date) ) {
  373.                     $occurs true;
  374.                     break;
  375.                 }
  376.             }
  377.         }
  378.         // if it should occur, test if it's excluded
  379.         if ( $occurs ) {
  380.             foreach ( $this->exdates as $exdate ) {
  381.                 if ( $exdate == $date ) {
  382.                     return false;
  383.                 }
  384.             }
  385.             foreach ( $this->exrules as $exrule ) {
  386.                 if ( $exrule->occursAt($date) ) {
  387.                     return false;
  388.                 }
  389.             }
  390.         }
  391.         return $occurs;
  392.     }
  393. ///////////////////////////////////////////////////////////////////////////////
  394. // Iterator interface
  395.     /** @internal */
  396.     protected $current 0;
  397.     /** @internal */
  398.     protected $key 0;
  399.     /**
  400.      * @internal
  401.      */
  402.     public function rewind()
  403.     {
  404.         $this->current $this->iterate(true);
  405.         $this->key 0;
  406.     }
  407.     /**
  408.      * @internal
  409.      */
  410.     public function current()
  411.     {
  412.         return $this->current;
  413.     }
  414.     /**
  415.      * @internal
  416.      */
  417.     public function key()
  418.     {
  419.         return $this->key;
  420.     }
  421.     /**
  422.      * @internal
  423.      */
  424.     public function next()
  425.     {
  426.         $this->current $this->iterate();
  427.         $this->key += 1;
  428.     }
  429.     /**
  430.      * @internal
  431.      */
  432.     public function valid()
  433.     {
  434.         return $this->current !== null;
  435.     }
  436. ///////////////////////////////////////////////////////////////////////////////
  437. // ArrayAccess interface
  438.     /**
  439.      * @internal
  440.      */
  441.     public function offsetExists($offset)
  442.     {
  443.         return is_numeric($offset) && $offset >= && ! is_float($offset) && $offset count($this);
  444.     }
  445.     /**
  446.      * @internal
  447.      */
  448.     public function offsetGet($offset)
  449.     {
  450.         if ( ! is_numeric($offset) || $offset || is_float($offset) ) {
  451.             throw new \InvalidArgumentException('Illegal offset type: '.gettype($offset));
  452.         }
  453.         if ( isset($this->cache[$offset]) ) {
  454.             // found in cache
  455.             return clone $this->cache[$offset];
  456.         }
  457.         elseif ( $this->total !== null ) {
  458.             // cache complete and not found in cache
  459.             return null;
  460.         }
  461.         // not in cache and cache not complete, we have to loop to find it
  462.         $i 0;
  463.         foreach ( $this as $occurrence ) {
  464.             if ( $i == $offset ) {
  465.                 return $occurrence;
  466.             }
  467.             $i++;
  468.             if ( $i $offset ) {
  469.                 break;
  470.             }
  471.         }
  472.         return null;
  473.     }
  474.     /**
  475.      * @internal
  476.      */
  477.     public function offsetSet($offset$value)
  478.     {
  479.         throw new \LogicException('Setting a Date in a RSet is not supported (use addDate)');
  480.     }
  481.     /**
  482.      * @internal
  483.      */
  484.     public function offsetUnset($offset)
  485.     {
  486.         throw new \LogicException('Unsetting a Date in a RSet is not supported (use addDate)');
  487.     }
  488. ///////////////////////////////////////////////////////////////////////////////
  489. // Countable interface
  490.     /**
  491.      * Returns the number of recurrences in this set. It will have go
  492.      * through the whole recurrence, if this hasn't been done before, which
  493.      * introduces a performance penality.
  494.      * @return int
  495.      */
  496.     public function count()
  497.     {
  498.         if ( $this->isInfinite() ) {
  499.             throw new \LogicException('Cannot count an infinite recurrence set.');
  500.         }
  501.         if ( $this->total === null ) {
  502.             foreach ( $this as $occurrence ) {}
  503.         }
  504.         return $this->total;
  505.     }
  506. ///////////////////////////////////////////////////////////////////////////////
  507. // Private methods
  508.     // cache variables
  509.     protected $rlist_heap null;
  510.     protected $rlist_iterator null;
  511.     protected $exlist_heap null;
  512.     protected $exlist_iterator null;
  513.     // local variables for iterate() (see comment in RRule about that)
  514.     /** @internal */
  515.     private $_previous_occurrence null;
  516.     /** @internal */
  517.     private $_total 0;
  518.     /** @internal */
  519.     private $_use_cache 0;
  520.     /**
  521.      * This method will iterate over a bunch of different iterators (rrules and arrays),
  522.      * keeping the results *in order*, while never attempting to merge or sort
  523.      * anything in memory. It can combine both finite and infinite rrule.
  524.      *
  525.      * What we need to do it to build two heaps: rlist and exlist
  526.      * Each heap contains multiple iterators (either RRule or ArrayIterator)
  527.      * At each step of the loop, it calls all of the iterators to generate a new item,
  528.      * and stores them in the heap, that keeps them in order.
  529.      *
  530.      * This is made slightly more complicated because this method is a generator.
  531.      *
  532.      * @param $reset (bool) Whether to restart the iteration, or keep going
  533.      * @return \DateTime|null
  534.      */
  535.     protected function iterate($reset false)
  536.     {
  537.         $previous_occurrence = & $this->_previous_occurrence;
  538.         $total = & $this->_total;
  539.         $use_cache = & $this->_use_cache;
  540.         if ( $reset ) {
  541.             $this->_previous_occurrence null;
  542.             $this->_total 0;
  543.             $this->_use_cache true;
  544.             reset($this->cache);
  545.         }
  546.         // go through the cache first
  547.         if ( $use_cache ) {
  548.             while ( ($occurrence current($this->cache)) !== false ) {
  549.                 next($this->cache);
  550.                 $total += 1;
  551.                 return clone $occurrence;
  552.             }
  553.             reset($this->cache);
  554.             // now set use_cache to false to skip the all thing on next iteration
  555.             // and start filling the cache instead
  556.             $use_cache false;
  557.             // if the cache as been used up completely and we now there is nothing else
  558.             if ( $total === $this->total ) {
  559.                 return null;
  560.             }
  561.         }
  562.         if ( $this->rlist_heap === null ) {
  563.             // rrules + rdate
  564.             $this->rlist_heap = new \SplMinHeap();
  565.             $this->rlist_iterator = new \MultipleIterator(\MultipleIterator::MIT_NEED_ANY);
  566.             $this->rlist_iterator->attachIterator(new \ArrayIterator($this->rdates));
  567.             foreach ( $this->rrules as $rrule ) {
  568.                 $this->rlist_iterator->attachIterator($rrule);
  569.             }
  570.             $this->rlist_iterator->rewind();
  571.             // exrules + exdate
  572.             $this->exlist_heap = new \SplMinHeap();
  573.             $this->exlist_iterator = new \MultipleIterator(\MultipleIterator::MIT_NEED_ANY);
  574.             $this->exlist_iterator->attachIterator(new \ArrayIterator($this->exdates));
  575.             foreach ( $this->exrules as $rrule ) {
  576.                 $this->exlist_iterator->attachIterator($rrule);
  577.             }
  578.             $this->exlist_iterator->rewind();
  579.         }
  580.         while ( true ) {
  581.             foreach ( $this->rlist_iterator->current() as $date ) {
  582.                 if ( $date !== null ) {
  583.                     $this->rlist_heap->insert($date);
  584.                 }
  585.             }
  586.             $this->rlist_iterator->next(); // advance the iterator for the next call
  587.             if ( $this->rlist_heap->isEmpty() ) {
  588.                 break; // exit the loop to stop the iterator
  589.             }
  590.             $occurrence $this->rlist_heap->top();
  591.             $this->rlist_heap->extract(); // remove the occurrence from the heap
  592.             if ( $occurrence == $previous_occurrence ) {
  593.                 continue; // skip, was already considered
  594.             }
  595.             // now we need to check against exlist
  596.             // we need to iterate exlist as long as it contains dates lower than occurrence
  597.             // (they will be discarded), and then check if the date is the same
  598.             // as occurrence (in which case it is discarded)
  599.             $excluded false;
  600.             while ( true ) {
  601.                 foreach ( $this->exlist_iterator->current() as $date ) {
  602.                     if ( $date !== null ) {
  603.                         $this->exlist_heap->insert($date);
  604.                     }
  605.                 }
  606.                 $this->exlist_iterator->next(); // advance the iterator for the next call
  607.                 if ( $this->exlist_heap->isEmpty() ) {
  608.                     break 1// break this loop only
  609.                 }
  610.                 $exdate $this->exlist_heap->top();
  611.                 if ( $exdate $occurrence ) {
  612.                     $this->exlist_heap->extract();
  613.                     continue;
  614.                 }
  615.                 elseif ( $exdate == $occurrence ) {
  616.                     $excluded true;
  617.                     break 1;
  618.                 }
  619.                 else {
  620.                     break 1// exdate is > occurrence, so we'll keep it for later
  621.                 }
  622.             }
  623.             $previous_occurrence $occurrence;
  624.             if ( $excluded ) {
  625.                 continue;
  626.             }
  627.             $total += 1;
  628.             $this->cache[] = $occurrence;
  629.             return clone $occurrence// = yield
  630.         }
  631.         $this->total $total// save total for count cache
  632.         return null// stop the iterator
  633.     }
  634. }