[Pkg-owncloud-commits] [php-sabre-vobject] 49/106: New RRULE parser.

David Prévot taffit at moszumanska.debian.org
Fri Aug 22 15:11:01 UTC 2014


This is an automated email from the git hooks/post-receive script.

taffit pushed a commit to branch master
in repository php-sabre-vobject.

commit bc0cbfb3d14fb2bce40d832bff23db720a4a8f2b
Author: Evert Pot <me at evertpot.com>
Date:   Sat Aug 2 00:40:01 2014 -0400

    New RRULE parser.
    
    This one is more lean as I've stripped out all the stuff related to
    exceptions and overriding events.
    
    This will allow me to refactor RecurrenceIterator to use RRuleParser and
    fix all outstanding recurrenceiterator more elegantly.
---
 lib/Sabre/VObject/Property/ICalendar/Recur.php |  50 +-
 lib/Sabre/VObject/RRuleParser.php              | 898 +++++++++++++++++++++++++
 tests/Sabre/VObject/RRuleParserTest.php        | 698 +++++++++++++++++++
 3 files changed, 1628 insertions(+), 18 deletions(-)

diff --git a/lib/Sabre/VObject/Property/ICalendar/Recur.php b/lib/Sabre/VObject/Property/ICalendar/Recur.php
index 9e5e59c..2df095f 100644
--- a/lib/Sabre/VObject/Property/ICalendar/Recur.php
+++ b/lib/Sabre/VObject/Property/ICalendar/Recur.php
@@ -59,24 +59,7 @@ class Recur extends Property {
             }
             $this->value = $newVal;
         } elseif (is_string($value)) {
-            $value = strtoupper($value);
-            $newValue = array();
-            foreach(explode(';', $value) as $part) {
-
-                // Skipping empty parts.
-                if (empty($part)) {
-                    continue;
-                }
-                list($partName, $partValue) = explode('=', $part);
-
-                // The value itself had multiple values..
-                if (strpos($partValue,',')!==false) {
-                    $partValue=explode(',', $partValue);
-                }
-                $newValue[$partName] = $partValue;
-
-            }
-            $this->value = $newValue;
+            $this->value = self::stringToArray($value);
         } else {
             throw new \InvalidArgumentException('You must either pass a string, or a key=>value array');
         }
@@ -186,4 +169,35 @@ class Recur extends Property {
         return array($values);
 
     }
+
+    /**
+     * Parses an RRULE value string, and turns it into a struct-ish array.
+     *
+     * @param string $value
+     * @return array
+     */
+    static function stringToArray($value) {
+
+        $value = strtoupper($value);
+        $newValue = array();
+        foreach(explode(';', $value) as $part) {
+
+            // Skipping empty parts.
+            if (empty($part)) {
+                continue;
+            }
+            list($partName, $partValue) = explode('=', $part);
+
+            // The value itself had multiple values..
+            if (strpos($partValue,',')!==false) {
+                $partValue=explode(',', $partValue);
+            }
+            $newValue[$partName] = $partValue;
+
+        }
+
+        return $newValue;
+
+    }
+
 }
diff --git a/lib/Sabre/VObject/RRuleParser.php b/lib/Sabre/VObject/RRuleParser.php
new file mode 100644
index 0000000..cd6e145
--- /dev/null
+++ b/lib/Sabre/VObject/RRuleParser.php
@@ -0,0 +1,898 @@
+<?php
+
+namespace Sabre\VObject;
+
+use DateTime;
+use InvalidArgumentException;
+use Iterator;
+
+/**
+ * RRuleParser
+ *
+ * This class receives an RRULE string, and allows you to iterate to get a list
+ * of dates in that recurrence.
+ *
+ * For instance, passing: FREQ=DAILY;LIMIT=5 will cause the iterator to contain
+ * 5 items, one for each day.
+ *
+ * @copyright Copyright (C) 2007-2014 fruux GmbH. All rights reserved.
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class RRuleParser implements Iterator {
+
+    /**
+     * Creates the Iterator
+     *
+     * @param string|array $rrule
+     * @param DateTime $start
+     */
+    public function __construct($rrule, DateTime $start) {
+
+        $this->startDate = $start;
+        $this->parseRRule($rrule);
+        $this->currentDate = clone $this->startDate;
+
+    }
+
+    /* Implementation of the Iterator interface {{{ */
+
+    public function current() {
+
+        if (!$this->valid()) return null;
+        return clone $this->currentDate;
+
+    }
+
+    /**
+     * Returns the current item number
+     *
+     * @return int
+     */
+    public function key() {
+
+        return $this->counter;
+
+    }
+
+    /**
+     * Retursn wheter the current item is a valid item for the recurrence
+     * iterator. This will return false if we've gone beyond the UNTIL or COUNT
+     * statements.
+     *
+     * @return bool
+     */
+    public function valid() {
+
+        if (!is_null($this->count)) {
+            return $this->counter < $this->count;
+        }
+        return is_null($this->until) || $this->currentDate <= $this->until;
+
+    }
+
+    /**
+     * Resets the iterator
+     *
+     * @return void
+     */
+    public function rewind() {
+
+        $this->currentDate = clone $this->startDate;
+        $this->counter = 0;
+
+    }
+
+    /**
+     * Goes on to the next iteration
+     *
+     * @return void
+     */
+    public function next() {
+
+        $previousStamp = $this->currentDate->getTimeStamp();
+
+        // Otherwise, we find the next event in the normal RRULE
+        // sequence.
+        switch($this->frequency) {
+
+            case 'hourly' :
+                $this->nextHourly();
+                break;
+
+            case 'daily' :
+                $this->nextDaily();
+                break;
+
+            case 'weekly' :
+                $this->nextWeekly();
+                break;
+
+            case 'monthly' :
+                $this->nextMonthly();
+                break;
+
+            case 'yearly' :
+                $this->nextYearly();
+                break;
+
+        }
+        $this->counter++;
+
+    }
+
+    /* End of Iterator implementation }}} */
+
+    /**
+     * Returns true if this recurring event never ends.
+     *
+     * @return bool
+     */
+    public function isInfinite() {
+
+        return !$this->count && !$this->until;
+
+    }
+
+    /**
+     * This method allows you to quickly go to the next occurrence after the
+     * specified date.
+     *
+     * Note that this checks the current 'endDate', not the 'stardDate'. This
+     * means that if you forward to January 1st, the iterator will stop at the
+     * first event that ends *after* January 1st.
+     *
+     * @param DateTime $dt
+     * @return void
+     */
+    public function fastForward(\DateTime $dt) {
+
+        while($this->valid() && $this->currentDate < $dt ) {
+            $this->next();
+        }
+
+    }
+
+    /**
+     * The reference start date/time for the rrule.
+     *
+     * All calculations are based on this initial date.
+     *
+     * @var DateTime
+     */
+    protected $startDate;
+
+    /**
+     * The date of the current iteration. You can get this by calling
+     * ->current().
+     *
+     * @var DateTime
+     */
+    protected $currentDate;
+
+    /**
+     * Frequency is one of: secondly, minutely, hourly, daily, weekly, monthly,
+     * yearly.
+     *
+     * @var string
+     */
+    protected $frequency;
+
+    /**
+     * The number of recurrences, or 'null' if infinitely recurring.
+     *
+     * @var int
+     */
+    protected $count;
+
+    /**
+     * The interval.
+     *
+     * If for example frequency is set to daily, interval = 2 would mean every
+     * 2 days.
+     *
+     * @var int
+     */
+    protected $interval = 1;
+
+    /**
+     * The last instance of this recurrence, inclusively
+     *
+     * @var \DateTime|null
+     */
+    protected $until;
+
+    /**
+     * Which seconds to recur.
+     *
+     * This is an array of integers (between 0 and 60)
+     *
+     * @var array
+     */
+    protected $bySecond;
+
+    /**
+     * Which minutes to recur
+     *
+     * This is an array of integers (between 0 and 59)
+     *
+     * @var array
+     */
+    protected $byMinute;
+
+    /**
+     * Which hours to recur
+     *
+     * This is an array of integers (between 0 and 23)
+     *
+     * @var array
+     */
+    protected $byHour;
+
+    /**
+     * The current item in the list.
+     *
+     * You can get this number with the key() method.
+     *
+     * @var int
+     */
+    protected $counter = 0;
+
+    /**
+     * Which weekdays to recur.
+     *
+     * This is an array of weekdays
+     *
+     * This may also be preceeded by a positive or negative integer. If present,
+     * this indicates the nth occurrence of a specific day within the monthly or
+     * yearly rrule. For instance, -2TU indicates the second-last tuesday of
+     * the month, or year.
+     *
+     * @var array
+     */
+    protected $byDay;
+
+    /**
+     * Which days of the month to recur
+     *
+     * This is an array of days of the months (1-31). The value can also be
+     * negative. -5 for instance means the 5th last day of the month.
+     *
+     * @var array
+     */
+    protected $byMonthDay;
+
+    /**
+     * Which days of the year to recur.
+     *
+     * This is an array with days of the year (1 to 366). The values can also
+     * be negative. For instance, -1 will always represent the last day of the
+     * year. (December 31st).
+     *
+     * @var array
+     */
+    protected $byYearDay;
+
+    /**
+     * Which week numbers to recur.
+     *
+     * This is an array of integers from 1 to 53. The values can also be
+     * negative. -1 will always refer to the last week of the year.
+     *
+     * @var array
+     */
+    protected $byWeekNo;
+
+    /**
+     * Which months to recur
+     *
+     * This is an array of integers from 1 to 12.
+     *
+     * @var array
+     */
+    protected $byMonth;
+
+    /**
+     * Which items in an existing st to recur.
+     *
+     * These numbers work together with an existing by* rule. It specifies
+     * exactly which items of the existing by-rule to filter.
+     *
+     * Valid values are 1 to 366 and -1 to -366. As an example, this can be
+     * used to recur the last workday of the month.
+     *
+     * This would be done by setting frequency to 'monthly', byDay to
+     * 'MO,TU,WE,TH,FR' and bySetPos to -1.
+     *
+     * @var array
+     */
+    protected $bySetPos;
+
+    /**
+     * When a week starts
+     *
+     * @var string
+     */
+    protected $weekStart = 'MO';
+
+    /* Functions that advance the iterator {{{ */
+
+    /**
+     * Does the processing for advancing the iterator for hourly frequency.
+     *
+     * @return void
+     */
+    protected function nextHourly() {
+
+        $this->currentDate->modify('+' . $this->interval . ' hours');
+
+    }
+
+    /**
+     * Does the processing for advancing the iterator for daily frequency.
+     *
+     * @return void
+     */
+    protected function nextDaily() {
+
+        if (!$this->byHour && !$this->byDay) {
+            $this->currentDate->modify('+' . $this->interval . ' days');
+            return;
+        }
+
+        if (isset($this->byHour)) {
+            $recurrenceHours = $this->getHours();
+        }
+
+        if (isset($this->byDay)) {
+            $recurrenceDays = $this->getDays();
+        }
+
+        if (isset($this->byMonth)) {
+            $recurrenceMonths = $this->getMonths();
+        }
+
+        do {
+            if ($this->byHour) {
+                if ($this->currentDate->format('G') == '23') {
+                    // to obey the interval rule
+                    $this->currentDate->modify('+' . $this->interval-1 . ' days');
+                }
+
+                $this->currentDate->modify('+1 hours');
+
+            } else {
+                $this->currentDate->modify('+' . $this->interval . ' days');
+
+            }
+
+            // Current month of the year
+            $currentMonth = $this->currentDate->format('n');
+
+            // Current day of the week
+            $currentDay = $this->currentDate->format('w');
+
+            // Current hour of the day
+            $currentHour = $this->currentDate->format('G');
+
+        } while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours)) || ($this->byMonth && !in_array($currentMonth, $recurrenceMonths)));
+
+    }
+
+    /**
+     * Does the processing for advancing the iterator for weekly frequency.
+     *
+     * @return void
+     */
+    protected function nextWeekly() {
+
+        if (!$this->byHour && !$this->byDay) {
+            $this->currentDate->modify('+' . $this->interval . ' weeks');
+            return;
+        }
+
+        if ($this->byHour) {
+            $recurrenceHours = $this->getHours();
+        }
+
+        if ($this->byDay) {
+            $recurrenceDays = $this->getDays();
+        }
+
+        // First day of the week:
+        $firstDay = $this->dayMap[$this->weekStart];
+
+        do {
+
+            if ($this->byHour) {
+                $this->currentDate->modify('+1 hours');
+            } else {
+                $this->currentDate->modify('+1 days');
+            }
+
+            // Current day of the week
+            $currentDay = (int) $this->currentDate->format('w');
+
+            // Current hour of the day
+            $currentHour = (int) $this->currentDate->format('G');
+
+            // We need to roll over to the next week
+            if ($currentDay === $firstDay && (!$this->byHour || $currentHour == '0')) {
+                $this->currentDate->modify('+' . $this->interval-1 . ' weeks');
+
+                // We need to go to the first day of this week, but only if we
+                // are not already on this first day of this week.
+                if($this->currentDate->format('w') != $firstDay) {
+                    $this->currentDate->modify('last ' . $this->dayNames[$this->dayMap[$this->weekStart]]);
+                }
+            }
+
+            // We have a match
+        } while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours)));
+    }
+
+    /**
+     * Does the processing for advancing the iterator for monthly frequency.
+     *
+     * @return void
+     */
+    protected function nextMonthly() {
+
+        $currentDayOfMonth = $this->currentDate->format('j');
+        if (!$this->byMonthDay && !$this->byDay) {
+
+            // If the current day is higher than the 28th, rollover can
+            // occur to the next month. We Must skip these invalid
+            // entries.
+            if ($currentDayOfMonth < 29) {
+                $this->currentDate->modify('+' . $this->interval . ' months');
+            } else {
+                $increase = 0;
+                do {
+                    $increase++;
+                    $tempDate = clone $this->currentDate;
+                    $tempDate->modify('+ ' . ($this->interval*$increase) . ' months');
+                } while ($tempDate->format('j') != $currentDayOfMonth);
+                $this->currentDate = $tempDate;
+            }
+            return;
+        }
+
+        while(true) {
+
+            $occurrences = $this->getMonthlyOccurrences();
+
+            foreach($occurrences as $occurrence) {
+
+                // The first occurrence thats higher than the current
+                // day of the month wins.
+                if ($occurrence > $currentDayOfMonth) {
+                    break 2;
+                }
+
+            }
+
+            // If we made it all the way here, it means there were no
+            // valid occurrences, and we need to advance to the next
+            // month.
+            //
+            // This line does not currently work in hhvm. Temporary workaround
+            // follows:
+            // $this->currentDate->modify('first day of this month');
+            $this->currentDate = new \DateTime($this->currentDate->format('Y-m-1 H:i:s'), $this->currentDate->getTimezone());
+            // end of workaround
+            $this->currentDate->modify('+ ' . $this->interval . ' months');
+
+            // This goes to 0 because we need to start counting at hte
+            // beginning.
+            $currentDayOfMonth = 0;
+
+        }
+
+        $this->currentDate->setDate($this->currentDate->format('Y'), $this->currentDate->format('n'), $occurrence);
+
+    }
+
+    /**
+     * Does the processing for advancing the iterator for yearly frequency.
+     *
+     * @return void
+     */
+    protected function nextYearly() {
+
+        $currentMonth = $this->currentDate->format('n');
+        $currentYear = $this->currentDate->format('Y');
+        $currentDayOfMonth = $this->currentDate->format('j');
+
+        // No sub-rules, so we just advance by year
+        if (!$this->byMonth) {
+
+            // Unless it was a leap day!
+            if ($currentMonth==2 && $currentDayOfMonth==29) {
+
+                $counter = 0;
+                do {
+                    $counter++;
+                    // Here we increase the year count by the interval, until
+                    // we hit a date that's also in a leap year.
+                    //
+                    // We could just find the next interval that's dividable by
+                    // 4, but that would ignore the rule that there's no leap
+                    // year every year that's dividable by a 100, but not by
+                    // 400. (1800, 1900, 2100). So we just rely on the datetime
+                    // functions instead.
+                    $nextDate = clone $this->currentDate;
+                    $nextDate->modify('+ ' . ($this->interval*$counter) . ' years');
+                } while ($nextDate->format('n')!=2);
+                $this->currentDate = $nextDate;
+
+                return;
+
+            }
+
+            // The easiest form
+            $this->currentDate->modify('+' . $this->interval . ' years');
+            return;
+
+        }
+
+        $currentMonth = $this->currentDate->format('n');
+        $currentYear = $this->currentDate->format('Y');
+        $currentDayOfMonth = $this->currentDate->format('j');
+
+        $advancedToNewMonth = false;
+
+        // If we got a byDay or getMonthDay filter, we must first expand
+        // further.
+        if ($this->byDay || $this->byMonthDay) {
+
+            while(true) {
+
+                $occurrences = $this->getMonthlyOccurrences();
+
+                foreach($occurrences as $occurrence) {
+
+                    // The first occurrence that's higher than the current
+                    // day of the month wins.
+                    // If we advanced to the next month or year, the first
+                    // occurrence is always correct.
+                    if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) {
+                        break 2;
+                    }
+
+                }
+
+                // If we made it here, it means we need to advance to
+                // the next month or year.
+                $currentDayOfMonth = 1;
+                $advancedToNewMonth = true;
+                do {
+
+                    $currentMonth++;
+                    if ($currentMonth>12) {
+                        $currentYear+=$this->interval;
+                        $currentMonth = 1;
+                    }
+                } while (!in_array($currentMonth, $this->byMonth));
+
+                $this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth);
+
+            }
+
+            // If we made it here, it means we got a valid occurrence
+            $this->currentDate->setDate($currentYear, $currentMonth, $occurrence);
+            return;
+
+        } else {
+
+            // These are the 'byMonth' rules, if there are no byDay or
+            // byMonthDay sub-rules.
+            do {
+
+                $currentMonth++;
+                if ($currentMonth>12) {
+                    $currentYear+=$this->interval;
+                    $currentMonth = 1;
+                }
+            } while (!in_array($currentMonth, $this->byMonth));
+            $this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth);
+
+            return;
+
+        }
+
+    }
+
+    /* }}} */
+
+    /**
+     * This method receives a string from an RRULE property, and populates this
+     * class with all the values.
+     *
+     * @param string|array $rule
+     * @return void
+     */
+    protected function parseRRule($rrule) {
+
+        if (is_string($rrule)) {
+            $rrule = Property\ICalendar\Recur::stringToArray($rrule);
+        }
+
+        foreach($rrule as $key=>$value) {
+
+            $key = strtoupper($key);
+            switch($key) {
+
+                case 'FREQ' :
+                    $value = strtolower($value);
+                    if (!in_array(
+                        $value,
+                        array('secondly','minutely','hourly','daily','weekly','monthly','yearly')
+                    )) {
+                        throw new InvalidArgumentException('Unknown value for FREQ=' . strtoupper($value));
+                    }
+                    $this->frequency = $value;
+                    break;
+
+                case 'UNTIL' :
+                    $this->until = DateTimeParser::parse($value, $this->startDate->getTimezone());
+
+                    // In some cases events are generated with an UNTIL=
+                    // parameter before the actual start of the event.
+                    //
+                    // Not sure why this is happening. We assume that the
+                    // intention was that the event only recurs once.
+                    //
+                    // So we are modifying the parameter so our code doesn't
+                    // break.
+                    if($this->until < $this->startDate) {
+                        $this->until = $this->startDate;
+                    }
+                    break;
+
+                case 'INTERVAL' :
+                    // No break
+
+                case 'COUNT' :
+                    $val = (int)$value;
+                    if ($val < 1) {
+                        throw new \InvalidArgumentException(strtoupper($key) . ' in RRULE must be a positive integer!');
+                    }
+                    $key = strtolower($key);
+                    $this->$key = $val;
+                    break;
+
+                case 'BYSECOND' :
+                    $this->bySecond = (array)$value;
+                    break;
+
+                case 'BYMINUTE' :
+                    $this->byMinute = (array)$value;
+                    break;
+
+                case 'BYHOUR' :
+                    $this->byHour = (array)$value;
+                    break;
+
+                case 'BYDAY' :
+                    $value = (array)$value;
+                    foreach($value as $part) {
+                        if (!preg_match('#^  (-|\+)? ([1-5])? (MO|TU|WE|TH|FR|SA|SU) $# xi', $part)) {
+                            throw new \InvalidArgumentException('Invalid part in BYDAY clause: ' . $part);
+                        } 
+                    }
+                    $this->byDay = $value;
+                    break;
+
+                case 'BYMONTHDAY' :
+                    $this->byMonthDay = (array)$value;
+                    break;
+
+                case 'BYYEARDAY' :
+                    $this->byYearDay = (array)$value;
+                    break;
+
+                case 'BYWEEKNO' :
+                    $this->byWeekNo = (array)$value;
+                    break;
+
+                case 'BYMONTH' :
+                    $this->byMonth = (array)$value;
+                    break;
+
+                case 'BYSETPOS' :
+                    $this->bySetPos = (array)$value;
+                    break;
+
+                case 'WKST' :
+                    $this->weekStart = strtoupper($value);
+                    break;
+
+                default:
+                    throw new \InvalidArgumentException('Not supported: ' . strtoupper($key));
+
+            }
+
+        }
+
+    }
+
+    /**
+     * Mappings between the day number and english day name.
+     *
+     * @var array
+     */
+    protected $dayNames = array(
+        0 => 'Sunday',
+        1 => 'Monday',
+        2 => 'Tuesday',
+        3 => 'Wednesday',
+        4 => 'Thursday',
+        5 => 'Friday',
+        6 => 'Saturday',
+    );
+
+    /**
+     * Returns all the occurrences for a monthly frequency with a 'byDay' or
+     * 'byMonthDay' expansion for the current month.
+     *
+     * The returned list is an array of integers with the day of month (1-31).
+     *
+     * @return array
+     */
+    protected function getMonthlyOccurrences() {
+
+        $startDate = clone $this->currentDate;
+
+        $byDayResults = array();
+
+        // Our strategy is to simply go through the byDays, advance the date to
+        // that point and add it to the results.
+        if ($this->byDay) foreach($this->byDay as $day) {
+
+            $dayName = $this->dayNames[$this->dayMap[substr($day,-2)]];
+
+
+            // Dayname will be something like 'wednesday'. Now we need to find
+            // all wednesdays in this month.
+            $dayHits = array();
+
+            // workaround for missing 'first day of the month' support in hhvm
+            $checkDate = new \DateTime($startDate->format('Y-m-1'));
+            // workaround modify always advancing the date even if the current day is a $dayName in hhvm
+            if ($checkDate->format('l') !== $dayName) {
+                $checkDate->modify($dayName);
+            }
+
+            do {
+                $dayHits[] = $checkDate->format('j');
+                $checkDate->modify('next ' . $dayName);
+            } while ($checkDate->format('n') === $startDate->format('n'));
+
+            // So now we have 'all wednesdays' for month. It is however
+            // possible that the user only really wanted the 1st, 2nd or last
+            // wednesday.
+            if (strlen($day)>2) {
+                $offset = (int)substr($day,0,-2);
+
+                if ($offset>0) {
+                    // It is possible that the day does not exist, such as a
+                    // 5th or 6th wednesday of the month.
+                    if (isset($dayHits[$offset-1])) {
+                        $byDayResults[] = $dayHits[$offset-1];
+                    }
+                } else {
+
+                    // if it was negative we count from the end of the array
+                    $byDayResults[] = $dayHits[count($dayHits) + $offset];
+                }
+            } else {
+                // There was no counter (first, second, last wednesdays), so we
+                // just need to add the all to the list).
+                $byDayResults = array_merge($byDayResults, $dayHits);
+
+            }
+
+        }
+
+        $byMonthDayResults = array();
+        if ($this->byMonthDay) foreach($this->byMonthDay as $monthDay) {
+
+            // Removing values that are out of range for this month
+            if ($monthDay > $startDate->format('t') ||
+                $monthDay < 0-$startDate->format('t')) {
+                    continue;
+            }
+            if ($monthDay>0) {
+                $byMonthDayResults[] = $monthDay;
+            } else {
+                // Negative values
+                $byMonthDayResults[] = $startDate->format('t') + 1 + $monthDay;
+            }
+        }
+
+        // If there was just byDay or just byMonthDay, they just specify our
+        // (almost) final list. If both were provided, then byDay limits the
+        // list.
+        if ($this->byMonthDay && $this->byDay) {
+            $result = array_intersect($byMonthDayResults, $byDayResults);
+        } elseif ($this->byMonthDay) {
+            $result = $byMonthDayResults;
+        } else {
+            $result = $byDayResults;
+        }
+        $result = array_unique($result);
+        sort($result, SORT_NUMERIC);
+
+        // The last thing that needs checking is the BYSETPOS. If it's set, it
+        // means only certain items in the set survive the filter.
+        if (!$this->bySetPos) {
+            return $result;
+        }
+
+        $filteredResult = array();
+        foreach($this->bySetPos as $setPos) {
+
+            if ($setPos<0) {
+                $setPos = count($result)-($setPos+1);
+            }
+            if (isset($result[$setPos-1])) {
+                $filteredResult[] = $result[$setPos-1];
+            }
+        }
+
+        sort($filteredResult, SORT_NUMERIC);
+        return $filteredResult;
+
+    }
+
+    /**
+     * Simple mapping from iCalendar day names to day numbers
+     *
+     * @var array
+     */
+    protected $dayMap = array(
+        'SU' => 0,
+        'MO' => 1,
+        'TU' => 2,
+        'WE' => 3,
+        'TH' => 4,
+        'FR' => 5,
+        'SA' => 6,
+    );
+
+    protected function getHours()
+    {
+        $recurrenceHours = array();
+        foreach($this->byHour as $byHour) {
+            $recurrenceHours[] = $byHour;
+        }
+
+        return $recurrenceHours;
+    }
+
+    protected function getDays() {
+
+        $recurrenceDays = array();
+        foreach($this->byDay as $byDay) {
+
+            // The day may be preceeded with a positive (+n) or
+            // negative (-n) integer. However, this does not make
+            // sense in 'weekly' so we ignore it here.
+            $recurrenceDays[] = $this->dayMap[substr($byDay,-2)];
+
+        }
+
+        return $recurrenceDays;
+    }
+
+    protected function getMonths() {
+
+        $recurrenceMonths = array();
+        foreach($this->byMonth as $byMonth) {
+            $recurrenceMonths[] = $byMonth;
+        }
+
+        return $recurrenceMonths;
+    }
+}
diff --git a/tests/Sabre/VObject/RRuleParserTest.php b/tests/Sabre/VObject/RRuleParserTest.php
new file mode 100644
index 0000000..7d39998
--- /dev/null
+++ b/tests/Sabre/VObject/RRuleParserTest.php
@@ -0,0 +1,698 @@
+<?php
+
+namespace Sabre\VObject;
+
+use DateTime;
+use DateTimeZone;
+
+class RRuleParserTest extends \PHPUnit_Framework_TestCase {
+
+    function testHourly() {
+
+        $this->parse(
+            'FREQ=HOURLY;INTERVAL=3;COUNT=12',
+            '2011-10-07 12:00:00',
+            array(
+                '2011-10-07 12:00:00',
+                '2011-10-07 15:00:00',
+                '2011-10-07 18:00:00',
+                '2011-10-07 21:00:00',
+                '2011-10-08 00:00:00',
+                '2011-10-08 03:00:00',
+                '2011-10-08 06:00:00',
+                '2011-10-08 09:00:00',
+                '2011-10-08 12:00:00',
+                '2011-10-08 15:00:00',
+                '2011-10-08 18:00:00',
+                '2011-10-08 21:00:00',
+            )
+        );
+
+    }
+
+    function testDaily() {
+
+        $this->parse(
+            'FREQ=DAILY;INTERVAL=3;UNTIL=20111025T000000Z',
+            '2011-10-07',
+            array(
+                '2011-10-07 00:00:00',
+                '2011-10-10 00:00:00',
+                '2011-10-13 00:00:00',
+                '2011-10-16 00:00:00',
+                '2011-10-19 00:00:00',
+                '2011-10-22 00:00:00',
+                '2011-10-25 00:00:00',
+            )
+        );
+
+    }
+
+    function testDailyByDayByHour() {
+
+        $this->parse(
+            'FREQ=DAILY;BYDAY=SA,SU;BYHOUR=6,7',
+            '2011-10-08 06:00:00',
+            array(
+                '2011-10-08 06:00:00',
+                '2011-10-08 07:00:00',
+                '2011-10-09 06:00:00',
+                '2011-10-09 07:00:00',
+                '2011-10-15 06:00:00',
+                '2011-10-15 07:00:00',
+                '2011-10-16 06:00:00',
+                '2011-10-16 07:00:00',
+                '2011-10-22 06:00:00',
+                '2011-10-22 07:00:00',
+                '2011-10-23 06:00:00',
+                '2011-10-23 07:00:00',
+            )
+        );
+
+    }
+
+    function testDailyByHour() {
+
+        $this->parse(
+            'FREQ=DAILY;INTERVAL=2;BYHOUR=10,11,12,13,14,15',
+            '2012-10-11 12:00:00',
+            array(
+                '2012-10-11 12:00:00',
+                '2012-10-11 13:00:00',
+                '2012-10-11 14:00:00',
+                '2012-10-11 15:00:00',
+                '2012-10-13 10:00:00',
+                '2012-10-13 11:00:00',
+                '2012-10-13 12:00:00',
+                '2012-10-13 13:00:00',
+                '2012-10-13 14:00:00',
+                '2012-10-13 15:00:00',
+                '2012-10-15 10:00:00',
+                '2012-10-15 11:00:00',
+            )
+        );
+
+    }
+
+    function testDailyByDay() {
+
+        $this->parse(
+            'FREQ=DAILY;INTERVAL=2;BYDAY=TU,WE,FR',
+            '2011-10-07 12:00:00',
+            array(
+                '2011-10-07 12:00:00',
+                '2011-10-11 12:00:00',
+                '2011-10-19 12:00:00',
+                '2011-10-21 12:00:00',
+                '2011-10-25 12:00:00',
+                '2011-11-02 12:00:00',
+                '2011-11-04 12:00:00',
+                '2011-11-08 12:00:00',
+                '2011-11-16 12:00:00',
+                '2011-11-18 12:00:00',
+                '2011-11-22 12:00:00',
+                '2011-11-30 12:00:00',
+            )
+        );
+
+    }
+
+    function testDailyCount() {
+
+        $this->parse(
+            'FREQ=DAILY;COUNT=5',
+            '2014-08-01 18:03:00',
+            array(
+                '2014-08-01 18:03:00',
+                '2014-08-02 18:03:00',
+                '2014-08-03 18:03:00',
+                '2014-08-04 18:03:00',
+                '2014-08-05 18:03:00',
+            )
+        );
+
+    }
+
+    function testDailyByMonth() {
+
+        $this->parse(
+            'FREQ=DAILY;BYMONTH=9,10;BYDAY=SU',
+            '2007-10-04 16:00:00',
+            array(
+                "2013-09-29 16:00:00",
+                "2013-10-06 16:00:00",
+                "2013-10-13 16:00:00",
+                "2013-10-20 16:00:00",
+                "2013-10-27 16:00:00",
+                "2014-09-07 16:00:00"
+            ),
+            '2013-09-28'
+        );
+
+    }
+
+    function testWeekly() {
+
+        $this->parse(
+            'FREQ=WEEKLY;INTERVAL=2;COUNT=10',
+            '2011-10-07 00:00:00',
+            array(
+                '2011-10-07 00:00:00',
+                '2011-10-21 00:00:00',
+                '2011-11-04 00:00:00',
+                '2011-11-18 00:00:00',
+                '2011-12-02 00:00:00',
+                '2011-12-16 00:00:00',
+                '2011-12-30 00:00:00',
+                '2012-01-13 00:00:00',
+                '2012-01-27 00:00:00',
+                '2012-02-10 00:00:00',
+            )
+        );
+
+    }
+
+    function testWeeklyByDay() {
+
+        $this->parse(
+            'FREQ=WEEKLY;INTERVAL=1;COUNT=4;BYDAY=MO;WKST=SA',
+            '2014-08-01 00:00:00',
+            array(
+                '2014-08-01 00:00:00',
+                '2014-08-04 00:00:00',
+                '2014-08-11 00:00:00',
+                '2014-08-18 00:00:00',
+            )
+        );
+
+    }
+
+    function testWeeklyByDay2() {
+
+        $this->parse(
+            'FREQ=WEEKLY;INTERVAL=2;BYDAY=TU,WE,FR;WKST=SU',
+            '2011-10-07 00:00:00',
+            array(
+                '2011-10-07 00:00:00',
+                '2011-10-18 00:00:00',
+                '2011-10-19 00:00:00',
+                '2011-10-21 00:00:00',
+                '2011-11-01 00:00:00',
+                '2011-11-02 00:00:00',
+                '2011-11-04 00:00:00',
+                '2011-11-15 00:00:00',
+                '2011-11-16 00:00:00',
+                '2011-11-18 00:00:00',
+                '2011-11-29 00:00:00',
+                '2011-11-30 00:00:00',
+            )
+        );
+
+    }
+
+    function testWeeklyByDayByHour() {
+
+        $this->parse(
+            'FREQ=WEEKLY;INTERVAL=2;BYDAY=TU,WE,FR;WKST=MO;BYHOUR=8,9,10',
+            '2011-10-07 08:00:00',
+            array(
+                '2011-10-07 08:00:00',
+                '2011-10-07 09:00:00',
+                '2011-10-07 10:00:00',
+                '2011-10-18 08:00:00',
+                '2011-10-18 09:00:00',
+                '2011-10-18 10:00:00',
+                '2011-10-19 08:00:00',
+                '2011-10-19 09:00:00',
+                '2011-10-19 10:00:00',
+                '2011-10-21 08:00:00',
+                '2011-10-21 09:00:00',
+                '2011-10-21 10:00:00',
+                '2011-11-01 08:00:00',
+                '2011-11-01 09:00:00',
+                '2011-11-01 10:00:00',
+            )
+        );
+
+    }
+
+    function testWeeklyByDaySpecificHour() {
+
+        $this->parse(
+            'FREQ=WEEKLY;INTERVAL=2;BYDAY=TU,WE,FR;WKST=SU',
+            '2011-10-07 18:00:00',
+            array(
+                '2011-10-07 18:00:00',
+                '2011-10-18 18:00:00',
+                '2011-10-19 18:00:00',
+                '2011-10-21 18:00:00',
+                '2011-11-01 18:00:00',
+                '2011-11-02 18:00:00',
+                '2011-11-04 18:00:00',
+                '2011-11-15 18:00:00',
+                '2011-11-16 18:00:00',
+                '2011-11-18 18:00:00',
+                '2011-11-29 18:00:00',
+                '2011-11-30 18:00:00',
+            )
+        );
+
+    }
+
+    function testMonthly() {
+
+        $this->parse(
+            'FREQ=MONTHLY;INTERVAL=3;COUNT=5',
+            '2011-12-05 00:00:00',
+            array(
+                 '2011-12-05 00:00:00',
+                 '2012-03-05 00:00:00',
+                 '2012-06-05 00:00:00',
+                 '2012-09-05 00:00:00',
+                 '2012-12-05 00:00:00',
+            )
+        );
+
+    }
+
+    function testMonlthyEndOfMonth() {
+
+        $this->parse(
+            'FREQ=MONTHLY;INTERVAL=2;COUNT=12',
+            '2011-12-31 00:00:00',
+            array(
+                '2011-12-31 00:00:00',
+                '2012-08-31 00:00:00',
+                '2012-10-31 00:00:00',
+                '2012-12-31 00:00:00',
+                '2013-08-31 00:00:00',
+                '2013-10-31 00:00:00',
+                '2013-12-31 00:00:00',
+                '2014-08-31 00:00:00',
+                '2014-10-31 00:00:00',
+                '2014-12-31 00:00:00',
+                '2015-08-31 00:00:00',
+                '2015-10-31 00:00:00',
+            )
+        );
+
+    }
+
+    function testMonthlyByMonthDay() {
+
+        $this->parse(
+            'FREQ=MONTHLY;INTERVAL=5;COUNT=9;BYMONTHDAY=1,31,-7',
+            '2011-01-01 00:00:00',
+            array(
+                '2011-01-01 00:00:00',
+                '2011-01-25 00:00:00',
+                '2011-01-31 00:00:00',
+                '2011-06-01 00:00:00',
+                '2011-06-24 00:00:00',
+                '2011-11-01 00:00:00',
+                '2011-11-24 00:00:00',
+                '2012-04-01 00:00:00',
+                '2012-04-24 00:00:00',
+            )
+        );
+
+    }
+
+    function testMonthlyByDay() {
+
+        $this->parse(
+            'FREQ=MONTHLY;INTERVAL=2;COUNT=16;BYDAY=MO,-2TU,+1WE,3TH',
+            '2011-01-03 00:00:00',
+            array(
+                '2011-01-03 00:00:00',
+                '2011-01-05 00:00:00',
+                '2011-01-10 00:00:00',
+                '2011-01-17 00:00:00',
+                '2011-01-18 00:00:00',
+                '2011-01-20 00:00:00',
+                '2011-01-24 00:00:00',
+                '2011-01-31 00:00:00',
+                '2011-03-02 00:00:00',
+                '2011-03-07 00:00:00',
+                '2011-03-14 00:00:00',
+                '2011-03-17 00:00:00',
+                '2011-03-21 00:00:00',
+                '2011-03-22 00:00:00',
+                '2011-03-28 00:00:00',
+                '2011-05-02 00:00:00',
+            )
+        );
+
+    }
+
+    function testMonthlyByDayByMonthDay() {
+
+        $this->parse(
+            'FREQ=MONTHLY;COUNT=10;BYDAY=MO;BYMONTHDAY=1',
+            '2011-08-01 00:00:00',
+            array(
+                '2011-08-01 00:00:00',
+                '2012-10-01 00:00:00',
+                '2013-04-01 00:00:00',
+                '2013-07-01 00:00:00',
+                '2014-09-01 00:00:00',
+                '2014-12-01 00:00:00',
+                '2015-06-01 00:00:00',
+                '2016-02-01 00:00:00',
+                '2016-08-01 00:00:00',
+                '2017-05-01 00:00:00',
+            )
+        );
+
+    }
+
+    function testMonthlyByDayBySetPos() {
+
+        $this->parse(
+            'FREQ=MONTHLY;COUNT=10;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=1,-1',
+            '2011-01-03 00:00:00',
+            array(
+                '2011-01-03 00:00:00',
+                '2011-01-31 00:00:00',
+                '2011-02-01 00:00:00',
+                '2011-02-28 00:00:00',
+                '2011-03-01 00:00:00',
+                '2011-03-31 00:00:00',
+                '2011-04-01 00:00:00',
+                '2011-04-29 00:00:00',
+                '2011-05-02 00:00:00',
+                '2011-05-31 00:00:00',
+            )
+        );
+
+    }
+
+    function testYearly() {
+
+        $this->parse(
+            'FREQ=YEARLY;COUNT=10;INTERVAL=3',
+            '2011-01-01 00:00:00',
+            array(
+                '2011-01-01 00:00:00',
+                '2014-01-01 00:00:00',
+                '2017-01-01 00:00:00',
+                '2020-01-01 00:00:00',
+                '2023-01-01 00:00:00',
+                '2026-01-01 00:00:00',
+                '2029-01-01 00:00:00',
+                '2032-01-01 00:00:00',
+                '2035-01-01 00:00:00',
+                '2038-01-01 00:00:00',
+            )
+        );
+    }
+
+    function testYearlyLeapYear() {
+
+        $this->parse(
+            'FREQ=YEARLY;COUNT=3',
+            '2012-02-29 00:00:00',
+            array(
+                '2012-02-29 00:00:00',
+                '2016-02-29 00:00:00',
+                '2020-02-29 00:00:00',
+            )
+        );
+    }
+
+    function testYearlyByMonth() {
+
+        $this->parse(
+            'FREQ=YEARLY;COUNT=8;INTERVAL=4;BYMONTH=4,10',
+            '2011-04-07 00:00:00',
+            array(
+                '2011-04-07 00:00:00',
+                '2011-10-07 00:00:00',
+                '2015-04-07 00:00:00',
+                '2015-10-07 00:00:00',
+                '2019-04-07 00:00:00',
+                '2019-10-07 00:00:00',
+                '2023-04-07 00:00:00',
+                '2023-10-07 00:00:00',
+            )
+        );
+
+    }
+
+    function testYearlyByMonthByDay() {
+
+        $this->parse(
+            'FREQ=YEARLY;COUNT=8;INTERVAL=5;BYMONTH=4,10;BYDAY=1MO,-1SU',
+            '2011-04-04 00:00:00',
+            array(
+                '2011-04-04 00:00:00',
+                '2011-04-24 00:00:00',
+                '2011-10-03 00:00:00',
+                '2011-10-30 00:00:00',
+                '2016-04-04 00:00:00',
+                '2016-04-24 00:00:00',
+                '2016-10-03 00:00:00',
+                '2016-10-30 00:00:00',
+            )
+        );
+
+    }
+
+    function testFastForward() {
+
+        // The idea is that we're fast-forwarding too far in the future, so
+        // there will be no results left.
+        $this->parse(
+            'FREQ=YEARLY;COUNT=8;INTERVAL=5;BYMONTH=4,10;BYDAY=1MO,-1SU',
+            '2011-04-04 00:00:00',
+            array(),
+            '2020-05-05 00:00:00'
+        );
+
+    } 
+
+    /**
+     * The bug that was in the
+     * system before would fail on the 5th tuesday of the month, if the 5th
+     * tuesday did not exist.
+     *
+     * A pretty slow test. Had to be marked as 'medium' for phpunit to not die
+     * after 1 second. Would be good to optimize later.
+     *
+     * @medium
+     */
+    function testFifthTuesdayProblem() {
+
+        $this->parse(
+            'FREQ=MONTHLY;INTERVAL=1;UNTIL=20071030T035959Z;BYDAY=5TU',
+            '2007-10-04 14:46:42',
+            array(
+                "2007-10-04 14:46:42",
+            )
+        );
+
+    }
+
+    /**
+     * This bug came from a Fruux customer. This would result in a never-ending
+     * request.
+     */
+    function testFastFowardTooFar() {
+
+        $this->parse(
+            'FREQ=WEEKLY;BYDAY=MO;UNTIL=20090704T205959Z;INTERVAL=1',
+            '2009-04-20 18:00:00',
+            array(
+                '2009-04-20 18:00:00',
+                '2009-04-27 18:00:00',
+                '2009-05-04 18:00:00',
+                '2009-05-11 18:00:00',
+                '2009-05-18 18:00:00',
+                '2009-05-25 18:00:00',
+                '2009-06-01 18:00:00',
+                '2009-06-08 18:00:00',
+                '2009-06-15 18:00:00',
+                '2009-06-22 18:00:00',
+                '2009-06-29 18:00:00',
+            )
+        );
+
+    }
+
+    /**
+     * This also at one point caused an infinite loop. We're keeping the test.
+     */
+    function testYearlyByMonthLoop() {
+
+        $this->parse(
+            'FREQ=YEARLY;INTERVAL=1;UNTIL=20120203T225959Z;BYMONTH=2;BYSETPOS=1;BYDAY=SU,MO,TU,WE,TH,FR,SA',
+            '2012-01-01 15:45:00',
+            array(
+                '2012-02-01 15:45:00',
+            ),
+            '2012-01-29 23:00:00'
+        );
+
+
+    }
+
+    /**
+     * Something, somewhere produced an ics with an interval set to 0. Because
+     * this means we increase the current day (or week, month) by 0, this also
+     * results in an infinite loop.
+     *
+     * @expectedException InvalidArgumentException
+     */
+    function testZeroInterval() {
+
+        $this->parse(
+            'FREQ=YEARLY;INTERVAL=0',
+            '2012-08-24 14:57:00',
+            array(),
+            '2013-01-01 23:00:00'
+        );
+
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    function testInvalidFreq() {
+
+        $this->parse(
+            'FREQ=SMONTHLY;INTERVAL=3;UNTIL=20111025T000000Z',
+            '2011-10-07',
+            array()
+        );
+
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    function testByDayBadOffset() {
+
+        $this->parse(
+            'FREQ=WEEKLY;INTERVAL=1;COUNT=4;BYDAY=0MO;WKST=SA',
+            '2014-08-01 00:00:00',
+            array()
+        );
+
+    }
+
+    function testUntilBeginHAsTimezone() {
+
+        $this->parse(
+            'FREQ=WEEKLY;UNTIL=20131118T183000',
+            '2013-09-23 18:30:00',
+            array(
+                '2013-09-23 18:30:00',
+                '2013-09-30 18:30:00',
+                '2013-10-07 18:30:00',
+                '2013-10-14 18:30:00',
+                '2013-10-21 18:30:00',
+                '2013-10-28 18:30:00',
+                '2013-11-04 18:30:00',
+                '2013-11-11 18:30:00',
+                '2013-11-18 18:30:00',
+            ),
+            null,
+            'America/New_York'
+        );
+
+    }
+
+    function testUntilBeforeDtStart() {
+
+        $this->parse(
+            'FREQ=DAILY;UNTIL=20140101T000000Z',
+            '2014-08-02 00:15:00',
+            array(
+                '2014-08-02 00:15:00',
+            )
+        );
+
+    }
+
+    function testIgnoredStuff() {
+
+        $this->parse(
+            'FREQ=DAILY;BYSECOND=1;BYMINUTE=1;BYYEARDAY=1;BYWEEKNO=1;COUNT=2',
+            '2014-08-02 00:15:00',
+            array(
+                '2014-08-02 00:15:00',
+                '2014-08-03 00:15:00',
+            )
+        );
+
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    function testUnsupportedPart() {
+
+        $this->parse(
+            'FREQ=DAILY;BYWODAN=1',
+            '2014-08-02 00:15:00',
+            array()
+        );
+
+    }
+
+    function testIteratorFunctions() {
+
+        $parser = new RRuleParser('FREQ=DAILY', new DateTime('2014-08-02 00:00:13'));
+        $parser->next();
+        $this->assertEquals(
+            new DateTime('2014-08-03 00:00:13'),
+            $parser->current()
+        );
+        $this->assertEquals(
+            1,
+            $parser->key()
+        );
+
+        $parser->rewind();
+
+        $this->assertEquals(
+            new DateTime('2014-08-02 00:00:13'),
+            $parser->current()
+        );
+        $this->assertEquals(
+            0,
+            $parser->key()
+        );
+
+    }
+
+    function parse($rule, $start, $expected, $fastForward = null, $tz = 'UTC') {
+
+        $dt = new DateTime($start, new DateTimeZone($tz));
+        $parser = new RRuleParser($rule, $dt);
+
+        if ($fastForward) {
+            $parser->fastForward(new DateTime($fastForward));
+        }
+
+        $result = array();
+        while($parser->valid()) {
+
+            $item = $parser->current();
+            $result[] = $item->format('Y-m-d H:i:s');
+
+            if ($parser->isInfinite() && count($result) >= count($expected)) {
+                break;
+            }
+            $parser->next();
+
+        }
+
+        $this->assertEquals(
+            $expected,
+            $result
+        );
+
+    }
+
+}

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-owncloud/php-sabre-vobject.git



More information about the Pkg-owncloud-commits mailing list