From 1535174243860b5611e8c38d63dc5e3afbe8febf Mon Sep 17 00:00:00 2001 From: rlanvin Date: Tue, 23 Jun 2015 11:31:18 +0300 Subject: [PATCH] Initial commit --- .gitignore | 3 + LICENSE | 21 + README.md | 0 composer.json | 14 + phpunit.xml.dist | 25 ++ src/RRule.php | 906 ++++++++++++++++++++++++++++++++++++++++++++ tests/RRuleTest.php | 196 ++++++++++ tests/bootstrap.php | 3 + 8 files changed, 1168 insertions(+) create mode 100755 .gitignore create mode 100755 LICENSE create mode 100755 README.md create mode 100755 composer.json create mode 100755 phpunit.xml.dist create mode 100755 src/RRule.php create mode 100755 tests/RRuleTest.php create mode 100755 tests/bootstrap.php diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..684a066 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.sublime* +vendor +test.php \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..8aec5e9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 RĂ©mi Lanvin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100755 index 0000000..e69de29 diff --git a/composer.json b/composer.json new file mode 100755 index 0000000..4cc9c30 --- /dev/null +++ b/composer.json @@ -0,0 +1,14 @@ +{ + "name": "rlanvin/php-rrule", + "type": "library", + "description": "RRule implementation (RFC 5545)", + "keywords": ["rrule","ical"], + "homepage": "https://github.com/rlanvin/php-rrule", + "license": "MIT", + "require": { + "php": ">=5.3.0" + }, + "autoload": { + "classmap": ["src/"] + } +} \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100755 index 0000000..595f769 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,25 @@ + + + + + + ./tests/ + + + + + + ./src/ + + + \ No newline at end of file diff --git a/src/RRule.php b/src/RRule.php new file mode 100755 index 0000000..c6a5987 --- /dev/null +++ b/src/RRule.php @@ -0,0 +1,906 @@ + 1,'TU' => 2,'WE' => 3,'TH' => 4,'FR' => 5,'SA' => 6,'SU' => 7]; + + // original rule + protected $rule = array( + 'DTSTART' => null, + 'FREQ' => null, + 'UNTIL' => null, + 'COUNT' => null, + 'INTERVAL' => 1, + 'BYSECOND' => null, + 'BYMINUTE' => null, + 'BYHOUR' => null, + 'BYDAY' => null, + 'BYMONTHDAY' => null, + 'BYYEARDAY' => null, + 'BYWEEKNO' => null, + 'BYMONTH' => null, + 'BYSETPOS' => null, + 'WKST' => 'MO' + ); + + // parsed and validated values + protected $dtstart = null; + protected $dtstart_ts = null; + protected $freq = null; + protected $until = null; + protected $count = null; + protected $interval = null; + protected $bysecond = null; + protected $byminute = null; + protected $byhour = null; + protected $byweekday = null; + protected $byweekday_relative = null; + protected $bymonthday = null; + protected $bymonthday_negative = null; + protected $byyearday = null; + protected $byweekno = null; + protected $bymonth = null; + protected $bysetpos = null; + protected $wkst = null; + +// Public interface + + /** + * Constructor + */ + public function __construct(array $parts) + { + // validate extra parts + $unsupported = array_diff_key($parts, $this->rule); + if ( ! empty($unsupported) ) { + throw new \InvalidArgumentException('Unsupported parameter(s): '.implode(',',array_keys($unsupported))); + } + + $parts = array_merge($this->rule, $parts); + $this->rule = $parts; // save original rule + + // WKST + $parts['WKST'] = strtoupper($parts['WKST']); + if ( ! array_key_exists($parts['WKST'], self::$week_days) ) { + throw new \InvalidArgumentException('The WKST rule part must be one of the following: '.implode(', ',array_keys(self::$week_days))); + } + $this->wkst = self::$week_days[$parts['WKST']]; + + // FREQ + $parts['FREQ'] = strtoupper($parts['FREQ']); + if ( ! in_array($parts['FREQ'], self::$frequencies) ) { + throw new \InvalidArgumentException('The FREQ rule part must be one of the following: '.implode(', ',self::$frequencies)); + } + $this->freq = $parts['FREQ']; + + // INTERVAL + $parts['INTERVAL'] = (int) $parts['INTERVAL']; + if ( $parts['INTERVAL'] < 1 ) { + throw new \InvalidArgumentException('The INTERVAL rule part must be a positive integer (> 0)'); + } + $this->interval = (int) $parts['INTERVAL']; + + // DTSTART + if ( not_empty($parts['DTSTART']) ) { + if ( is_string($parts['DTSTART']) ) { + $this->dtstart = $parts['DTSTART']; + $this->dtstart_ts = strtotime($parts['DTSTART']); + } + elseif ( $parts['DTSTART'] instanceof DateTime ) { + $this->dtstart = $parts['DTSTART']->format('Y-m-d'); + $this->dtstart_ts = $parts['DTSTART']->getTimestamp(); + } + elseif ( is_integer($parts['DTSTART']) ) { + $this->dtstart = date('Y-m-d',$parts['DTSTART']); + $this->dtstart_ts = $parts['DTSTART']; + } + + if ( ! $this->dtstart_ts ) { + throw new \InvalidArgumentException('Cannot parse DTSTART - must be a valid date, timestamp or DateTime object'); + } + } + else { + $this->dtstart = date('Y-m-d'); + $this->dtstart_ts = strtotime($this->dtstart); + } + + // UNTIL (optional) + if ( not_empty($parts['UNTIL']) ) { + if ( is_string($parts['UNTIL']) ) { + $this->until = $parts['UNTIL']; + } + elseif ( $parts['UNTIL'] instanceof DateTime ) { + $this->until = $parts['UNTIL']->format('Y-m-d'); + } + elseif ( is_integer($parts['UNTIL']) ) { + $this->until = date('Y-m-d',$parts['UNTIL']); + } + + if ( ! strtotime($this->until) ) { + throw new \InvalidArgumentException('Cannot parse UNTIL - must be a valid date, timestamp or DateTime object'); + } + } + + // COUNT (optional) + if ( not_empty($parts['COUNT']) ) { + $parts['COUNT'] = (int) $parts['COUNT']; + if ( $parts['COUNT'] < 1 ) { + throw new \InvalidArgumentException('COUNT must be a positive integer (> 0)'); + } + $this->count = (int) $parts['COUNT']; + } + + // infer necessary BYXXX rules from DTSTART, if not provided + if ( ! (not_empty($parts['BYWEEKNO']) || not_empty($parts['BYYEARDAY']) || not_empty($parts['BYMONTHDAY']) || not_empty($parts['BYDAY'])) ) { + switch ( $this->freq ) { + case 'YEARLY': + if ( ! not_empty($parts['BYMONTH']) ) { + $parts['BYMONTH'] = [date('m',$this->dtstart_ts)]; + } + $parts['BYMONTHDAY'] = [date('j', $this->dtstart_ts)]; + break; + case 'MONTHLY': + $parts['BYMONTHDAY'] = [date('j',$this->dtstart_ts)]; + break; + case 'WEEKLY': + $parts['BYDAY'] = [array_search(date('N', $this->dtstart_ts), self::$week_days)]; + break; + } + } + + // BYSECOND + if ( not_empty($parts['BYSECOND']) ) { + if ( ! is_array($parts['BYSECOND']) ) { + $parts['BYSECOND'] = explode(',',$parts['BYSECOND']); + } + + $this->bysecond = []; + foreach ( $parts['BYSECOND'] as $value ) { + if ( $value < 0 || $value > 60 ) { + throw new \InvalidArgumentException('Invalid BYSECOND value: '.$value); + } + $this->bysecond[] = (int) $value; + } + } + + if ( not_empty($parts['BYMINUTE']) ) { + if ( ! is_array($parts['BYMINUTE']) ) { + $parts['BYMINUTE'] = explode(',',$parts['BYMINUTE']); + } + + $this->byminute = []; + foreach ( $parts['BYMINUTE'] as $value ) { + if ( $value < 0 || $value > 59 ) { + throw new \InvalidArgumentException('Invalid BYMINUTE value: '.$value); + } + $this->byminute[] = (int) $value; + } + } + + if ( not_empty($parts['BYHOUR']) ) { + if ( ! is_array($parts['BYHOUR']) ) { + $parts['BYHOUR'] = explode(',',$parts['BYHOUR']); + } + + $this->byhour = []; + foreach ( $parts['BYHOUR'] as $value ) { + if ( $value < 0 || $value > 23 ) { + throw new \InvalidArgumentException('Invalid BYHOUR value: '.$value); + } + $this->byhour[] = (int) $value; + } + } + + // BYDAY (translated to byweekday for convenience) + if ( not_empty($parts['BYDAY']) ) { + if ( ! is_array($parts['BYDAY']) ) { + $parts['BYDAY'] = explode(',',$parts['BYDAY']); + } + $this->byweekday = []; + $this->byweekday_relative = []; + foreach ( $parts['BYDAY'] as $value ) { + $valid = preg_match('/^([+-]?[0-9]+)?([A-Z]{2})$/', $value, $matches); + if ( ! $valid || (not_empty($matches[1]) && ($matches[1] == 0 || $matches[1] > 53 || $matches[1] < -53)) || ! array_key_exists($matches[2], self::$week_days) ) { + throw new \InvalidArgumentException('Invalid BYDAY value: '.$value); + } + if ( $matches[1] ) { + $this->byweekday_relative[] = [self::$week_days[$matches[2]], (int)$matches[1]]; + } + else { + $this->byweekday[] = self::$week_days[$matches[2]]; + } + } + + if ( ! empty($this->weekday_relative) ) { + if ( $this->freq !== 'MONTHLY' && $this->freq !== 'YEARLY' ) { + throw new InvalidArgumentException('The BYDAY rule part MUST NOT be specified with a numeric value when the FREQ rule part is not set to MONTHLY or YEARLY.'); + } + if ( $this->freq == 'YEARLY' && not_empty($parts['BYWEEKNO']) ) { + throw new InvalidArgumentException('The BYDAY rule part MUST NOT be specified with a numeric value with the FREQ rule part set to YEARLY when the BYWEEKNO rule part is specified.'); + } + } + } + + // The BYMONTHDAY rule part specifies a COMMA-separated list of days + // of the month. Valid values are 1 to 31 or -31 to -1. For + // example, -10 represents the tenth to the last day of the month. + // The BYMONTHDAY rule part MUST NOT be specified when the FREQ rule + // part is set to WEEKLY. + if ( not_empty($parts['BYMONTHDAY']) ) { + if ( $this->freq == 'WEEKLY' ) { + throw new \InvalidArgumentException('The BYMONTHDAY rule part MUST NOT be specified when the FREQ rule part is set to WEEKLY.'); + } + + if ( ! is_array($parts['BYMONTHDAY']) ) { + $parts['BYMONTHDAY'] = explode(',',$parts['BYMONTHDAY']); + } + + $this->bymonthday = []; + $this->bymonthday_negative = []; + foreach ( $parts['BYMONTHDAY'] as $value ) { + if ( ! $value || $value < -31 || $value > 31 ) { + throw new \InvalidArgumentException('Invalid BYMONTHDAY value: '.$value.' (valid values are 1 to 31 or -31 to -1)'); + } + if ( $value < 0 ) { + $this->bymonthday_negative[] = (int) $value; + } + else { + $this->bymonthday[] = (int) $value; + } + } + } + + if ( not_empty($parts['BYYEARDAY']) ) { + if ( $this->freq == 'DAILY' || $this->freq == 'WEEKLY' || $this->freq == 'MONTHLY' ) { + throw new \InvalidArgumentException('The BYYEARDAY rule part MUST NOT be specified when the FREQ rule part is set to DAILY, WEEKLY, or MONTHLY.'); + } + + if ( ! is_array($parts['BYYEARDAY']) ) { + $parts['BYYEARDAY'] = explode(',',$parts['BYYEARDAY']); + } + + $this->bysetpos = []; + foreach ( $parts['BYYEARDAY'] as $value ) { + if ( ! $value || $value < -366 || $value > 366 ) { + throw new \InvalidArgumentException('Invalid BYSETPOS value: '.$value.' (valid values are 1 to 366 or -366 to -1)'); + } + + $this->byyearday[] = (int) $value; + } + } + + // BYWEEKNO + if ( not_empty($parts['BYWEEKNO']) ) { + if ( $this->freq !== 'YEARLY' ) { + throw new \InvalidArgumentException('The BYWEEKNO rule part MUST NOT be used when the FREQ rule part is set to anything other than YEARLY.'); + } + + if ( ! is_array($parts['BYWEEKNO']) ) { + $parts['BYWEEKNO'] = explode(',',$parts['BYWEEKNO']); + } + + $this->byweekno = []; + foreach ( $parts['BYWEEKNO'] as $value ) { + if ( ! $value || $value < -53 || $value > 53 ) { + throw new \InvalidArgumentException('Invalid BYWEEKNO value: '.$value.' (valid values are 1 to 53 or -53 to -1)'); + } + $this->byweekno[] = (int) $value; + } + } + + // The BYMONTH rule part specifies a COMMA-separated list of months + // of the year. Valid values are 1 to 12. + if ( not_empty($parts['BYMONTH']) ) { + if ( ! is_array($parts['BYMONTH']) ) { + $parts['BYMONTH'] = explode(',',$parts['BYMONTH']); + } + + $this->bymonth = []; + foreach ( $parts['BYMONTH'] as $value ) { + if ( $value < 1 || $value > 12 ) { + throw new \InvalidArgumentException('Invalid BYMONTH value: '.$value); + } + $this->bymonth[] = (int) $value; + } + } + + if ( not_empty($parts['BYSETPOS']) ) { + if ( ! (not_empty($parts['BYWEEKNO']) || not_empty($parts['BYYEARDAY']) || not_empty($parts['BYMONTHDAY']) || not_empty($parts['BYDAY']) || not_empty($parts['BYMONTH'])) ) { + throw new \InvalidArgumentException('The BYSETPOST rule part MUST only be used in conjunction with another BYxxx rule part.'); + } + + if ( ! is_array($parts['BYSETPOS']) ) { + $parts['BYSETPOS'] = explode(',',$parts['BYSETPOS']); + } + + $this->bysetpos = []; + foreach ( $parts['BYSETPOS'] as $value ) { + if ( ! $value || $value < -366 || $value > 366 ) { + throw new \InvalidArgumentException('Invalid BYSETPOS value: '.$value.' (valid values are 1 to 366 or -366 to -1)'); + } + + $this->bysetpos[] = (int) $value; + } + } + } + + public function getOccurrences() + { + if ( ! $this->count && ! $this->until ) { + throw new \LogicException('Cannot get all occurences of an infinite recurrence rule.'); + } + $res = []; + foreach ( $this as $occurence ) { + $res[] = $occurence; + } + return $res; + } + + /** + * @return array + */ + public function getOccurrencesBetween($begin, $end) + { + $res = []; + foreach ( $this as $occurence ) { + if ( $occurence < $begin ) { + continue; + } + if ( $occurence > $end ) { + break; + } + $res[] = $occurence; + } + return $res; + } + + /** + * @return bool + */ + public function occursOn($date) + { + + } + +// Iterator interface + + protected $position = 0; + + public function rewind() + { + $this->position = $this->iterate(true); + } + + public function current() + { + return $this->position; + } + + public function key() + { + // void + } + + public function next() + { + $this->position = $this->iterate(); + } + + public function valid() + { + return $this->position !== null; + } + +// ArrayAccess interface + + public function offsetExists($offset) + { + + } + + public function offsetGet($offset) + { + + } + + public function offsetSet($offset, $value) + { + throw new LogicException('Setting a Date in a RRule is not supported'); + } + + public function offsetUnset($offset) + { + throw new LogicException('Unsetting a Date in a RRule is not supported'); + } + +// private methods + + /** + * This method returns an array of days of the year (numbered from 0 to 365) + * of the current timeframe (year, month, week, day) containing the current date + */ + protected function getDaySet($year, $month, $day, array $masks) + { + switch ( $this->freq ) { + case 'YEARLY': + return range(0,$masks['year_len']-1); + + case 'MONTHLY': + $start = $masks['month_to_last_day'][$month-1]; + $stop = $masks['month_to_last_day'][$month]; + return range($start, $stop - 1); + + case 'WEEKLY': + // on first iteration, the first week will not be complete + // we don't backtrack to the first day of the week, to avoid + // crossing year boundary in reverse (i.e. if the week started + // during the previous year), because that would generate + // negative indexes (which would not work with the masks) + $set = []; + $i = (int) date('z', mktime(0,0,0,$month,$day,$year)); + $start = $i; + for ( $j = 0; $j < 7; $j++ ) { + $set[] = $i; + $i += 1; + if ( $masks['doy_to_weekday'][$i] == $this->wkst ) { + break; + } + } + return $set; + + case 'DAILY': + case 'HOURLY': + case 'MINUTELY': + case 'SECONDLY': + $n = (int) date('z', mktime(0,0,0,$month,$day,$year)); + return [$n]; + } + } + + /** + * Some serious magic is happening here. + */ + protected function buildWeekdayMasks($year, $month, $day, array & $masks) + { + $masks['doy_to_weekday'] = array_slice(self::$WEEKDAY_MASK, date('N', mktime(0,0,0,1,1,$year))-1); + $masks['doy_to_weekday_relative'] = array(); + + if ( $this->byweekday_relative ) { + $ranges = array(); + if ( $this->freq == 'YEARLY' ) { + if ( $this->bymonth ) { + foreach ( $this->bymonth as $bymonth ) { + $ranges[] = [$masks['month_to_last_day'][$bymonth-1], $masks['month_to_last_day'][$bymonth]]; + } + } + else { + $ranges = [[0,$masks['year_len']-1]]; + } + } + elseif ( $this->freq == 'MONTHLY') { + $ranges[] = [$masks['month_to_last_day'][$month-1], $masks['month_to_last_day'][$month]]; + } + + if ( $ranges ) { + foreach ( $ranges as $tmp ) { + list($first, $last) = $tmp; + foreach ( $this->byweekday_relative as $tmp ) { + list($weekday, $nth) = $tmp; + if ( $nth < 0 ) { + $i = $last + ($nth + 1) * 7; + $i = $i - pymod($masks['doy_to_weekday'][$i] - $weekday, 7); + } + else { + $i = $first + ($nth - 1) * 7; + $i = $i + (7 - $masks['doy_to_weekday'][$i] + $weekday) % 7; + } + if ( $i >= $first && $i <= $last ) { + $masks['doy_to_weekday_relative'][$i] = 1; + } + } + } + } + } + } + + /** + * This is the main method, where all of the logic happens. + * + * This method is a generator that works for PHP 5.3/5.4 (using static variables) + */ + protected function iterate($reset = false) + { + // these are the static variables, i.e. the variables that persists + // at every call of the method (to emulate a generator) + static $year = null, $month = null, $day = null; + static $current_set = null; + static $total = 0; + + if ( $reset ) { + $year = $month = $day = null; + $current_set = null; + $total = 0; + } + + // stop once $total has reached COUNT + if ( $this->count && $total >= $this->count ) { +// echo "\tTotal = $total ; COUNT = ".$this->count." stopping iteration\n"; + return null; + } + + if ( $year == null ) { + // difference from python here + if ( $this->freq == 'WEEKLY' ) { + // we align the start date to the WKST, so we can then + // simply loop by adding +7 days + $tmp = strtotime($this->dtstart); + $tmp = strtotime('-'.pymod(date('N', $tmp) - $this->wkst,7).'days', $tmp); + list($year,$month,$day) = explode('-',date('Y-m-d',$tmp)); + } + else { + list($year,$month,$day) = explode('-',$this->dtstart); + } + } + + while (true) { + // 1. get an array of all days in the next interval (day, month, week, etc.) + // we filter out from this array all days that do not match the BYXXX conditions + // to speed things up, we use days of the year (day numbers) instead of date + if ( $current_set === null ) { + // rebuild the various masks and converters + // these arrays will allow fast date operations + // without relying on date() methods + $masks = []; + $masks['leap_year'] = is_leap_year($year); + $masks['year_len'] = 365 + (int) $masks['leap_year']; + $masks['next_year_len'] = 365 + is_leap_year($year + 1); + if ( $masks['leap_year'] ) { + $masks['doy_to_month'] = self::$MONTH_MASK_366; + $masks['doy_to_monthday'] = self::$MONTHDAY_MASK_366; + $masks['doy_to_monthday_negative'] = self::$NEGATIVE_MONTHDAY_MASK_366; + $masks['month_to_last_day'] = self::$LAST_DAY_OF_MONTH_366; + } + else { + $masks['doy_to_month'] = self::$MONTH_MASK; + $masks['doy_to_monthday'] = self::$MONTHDAY_MASK; + $masks['doy_to_monthday_negative'] = self::$NEGATIVE_MONTHDAY_MASK; + $masks['month_to_last_day'] = self::$LAST_DAY_OF_MONTH; + } + $this->buildWeekdayMasks($year, $month, $day, $masks); + + $current_set = $this->getDaySet($year, $month, $day, $masks); +// echo"\tWorking with set=".json_encode($current_set)."\n"; + +// echo "\tdoy_to_weekday = ".json_encode($masks['doy_to_weekday'])."\n"; +// echo "\tdoy_to_weekday_relative = ".json_encode($masks['doy_to_weekday_relative'])."\n"; +// fgets(STDIN); + + $filtered_set = array(); + + // If multiple BYxxx rule parts are specified, then after evaluating the + // specified FREQ and INTERVAL rule parts, the BYxxx rule parts are + // applied to the current set of evaluated occurrences in the following + // order: BYMONTH, BYWEEKNO, BYYEARDAY, BYMONTHDAY, BYDAY, BYHOUR, + // BYMINUTE, BYSECOND and BYSETPOS; then COUNT and UNTIL are evaluated. + + // filter out (if needed) + foreach ( $current_set as $day_of_year ) { +// echo "\t DAY OF YEAR ",$day_of_year,"\n"; +// echo "\t month=",$masks['doy_to_month'][$day_of_year],"\n"; +// echo "\t monthday=",$doy_to_monthday[$day_of_year],"\n"; +// echo "\t -monthday=",$doy_to_monthday_negative[$day_of_year],"\n"; +// echo "\t weekday=",$doy_to_weekday[$day_of_year],"\n"; +// fgets(STDIN); + if ( $this->bymonth && ! in_array($masks['doy_to_month'][$day_of_year], $this->bymonth) ) { + continue; + } + if ( ($this->bymonthday || $this->bymonthday_negative) + && ! in_array($masks['doy_to_monthday'][$day_of_year], $this->bymonthday) + && ! in_array($masks['doy_to_monthday_negative'][$day_of_year], $this->bymonthday_negative) ) { + continue; + } + if ( $this->byweekday && ! in_array($masks['doy_to_weekday'][$day_of_year], $this->byweekday) ) { + continue; + } + if ( $this->byweekday_relative && ! isset($masks['doy_to_weekday_relative'][$day_of_year]) ) { + continue; + } + + if ( $this->byyearday ) { + if ( $day_of_year < $masks['year_len'] ) { + if ( ! in_array($day_of_year + 1, $this->byyearday) && ! in_array(- $masks['year_len'] + $day_of_year,$this->byyearday) ) { + continue; + } + } + else { // if ( ($day_of_year >= $masks['year_len'] + if ( ! in_array($day_of_year + 1 - $masks['year_len'], $this->byyearday) && ! in_array(- $masks['next_year_len'] + $day_of_year - $mask['year_len'], $this->byyearday) ) { + continue; + } + } + } + + $filtered_set[] = $day_of_year; + } +// echo "\tFiltered set (before BYSETPOS)=".json_encode($filtered_set)."\n"; + + $current_set = $filtered_set; + + // Note: if one day we decide to support time this will have to be + // moved/rewritten to expand time *before* applying BYSETPOS + if ( $this->bysetpos ) { + $filtered_set = []; + $n = sizeof($current_set); + foreach ( $this->bysetpos as $pos ) { + if ( $pos < 0 ) { + $pos = $n + $pos; + } + else { + $pos = $pos - 1; + } + if ( isset($current_set[$pos]) ) { + $filtered_set[] = $current_set[$pos]; + } + } + $current_set = array_unique($filtered_set); + } + +// echo "\tFiltered set (after BYSETPOS)=".json_encode($filtered_set)."\n"; + } + + // 2. loop, generate a valid date, and return the result (fake "yield") + // at the same time, we check the end condition and return null if + // we need to stop + while ( ($day_of_year = current($current_set)) !== false ) { + $occurrence = date('Y-m-d', mktime(0, 0, 0, 1, ($day_of_year + 1), $year)); + + // consider end conditions + if ( $this->until && $occurrence > $this->until ) { + // $this->length = $total (?) + return null; + } + + next($current_set); + if ( $occurrence >= $this->dtstart ) { // ignore occurences before DTSTART + $total += 1; + return $occurrence; // yield + } + } + + // 3. we reset the loop to the next interval + $current_set = null; // reset the loop + switch ( $this->freq ) { + case 'YEARLY': + // we do not care about $month or $day not existing, they are not used in yearly frequency + $year = $year + $this->interval; + break; + case 'MONTHLY': + // we do not care about the day of the month not existing, it is not used in monthly frequency + $month = $month + $this->interval; + if ( $month > 12 ) { + $delta = (int) ($month / 12); + $mod = $month % 12; + $month = $mod; + $year = $year + $delta; + if ( $month == 0 ) { + $month = 12; + $year = $year - 1; + } + } + break; + case 'WEEKLY': + // here we take a little shortcut from the Python version, by using date/time methods + list($year,$month,$day) = explode('-',date('Y-m-d',strtotime('+'.($this->interval*7).'day', mktime(0,0,0,$month,$day,$year)))); + break; + case 'DAILY': + // here we take a little shortcut from the Python version, by using date/time methods + list($year,$month,$day) = explode('-',date('Y-m-d',strtotime('+'.$this->interval.'day', mktime(0,0,0,$month,$day,$year)))); + break; + case 'HOURLY': + case 'MINUTELY': + case 'SECONDLY': + throw new LogicException('Unimplemented'); + } + // prevent overflow (especially on 32 bits system) + if ( $year >= MAX_YEAR ) { + return null; + } + } + } + +// constants +// Every mask is 7 days longer to handle cross-year weekly periods. + + public static $MONTH_MASK = array( + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, + 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, + 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4, + 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, + 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10, + 11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11, + 12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12, + 1,1,1,1,1,1,1 + ); + + public static $MONTH_MASK_366 = array( + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, + 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, + 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4, + 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, + 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10, + 11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11, + 12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12, + 1,1,1,1,1,1,1 + ); + + public static $MONTHDAY_MASK = array( + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + 1,2,3,4,5,6,7 + ); + + public static $MONTHDAY_MASK_366 = array( + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + 1,2,3,4,5,6,7 + ); + + public static $NEGATIVE_MONTHDAY_MASK = array( + -31,-30,-29,-28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + -28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + -31,-30,-29,-28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + -30,-29,-28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + -31,-30,-29,-28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + -30,-29,-28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + -31,-30,-29,-28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + -31,-30,-29,-28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + -30,-29,-28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + -31,-30,-29,-28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + -30,-29,-28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + -31,-30,-29,-28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + -31,-30,-29,-28,-27,-26,-25 + ); + + public static $NEGATIVE_MONTHDAY_MASK_366 = array( + -31,-30,-29,-28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + -29,-28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + -31,-30,-29,-28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + -30,-29,-28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + -31,-30,-29,-28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + -30,-29,-28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + -31,-30,-29,-28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + -31,-30,-29,-28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + -30,-29,-28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + -31,-30,-29,-28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + -30,-29,-28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + -31,-30,-29,-28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + -31,-30,-29,-28,-27,-26,-25 + ); + + public static $WEEKDAY_MASK = array( + 1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7, + 1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7, + 1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7, + 1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7, + 1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7, + 1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7, + 1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7, + 1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7, + 1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7, + 1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7, + 1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,4,5,6,7 + ); + + public static $LAST_DAY_OF_MONTH_366 = array( + 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 + ); + + public static $LAST_DAY_OF_MONTH = array( + 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 + ); +} \ No newline at end of file diff --git a/tests/RRuleTest.php b/tests/RRuleTest.php new file mode 100755 index 0000000..66227dc --- /dev/null +++ b/tests/RRuleTest.php @@ -0,0 +1,196 @@ +setExpectedException('InvalidArgumentException'); + new RRule([]); + } + + public function testUnsupportedParameter() + { + $this->setExpectedException('InvalidArgumentException'); + new RRule([ + 'FREQ' => 'DAILY', + 'FOO' => 'BAR' + ]); + } + + public function validByMonth() + { + return array( + ['1'], + ['1,2'], + [[1,2]] + ); + } + /** + * @dataProvider validByMonth + */ + public function testValidByMonth($bymonth) + { + new RRule([ + 'FREQ' => 'DAILY', + 'BYMONTH' => $bymonth + ]); + } + + public function invalidByMonth() + { + return array( + [0], + ['-1'], + [-1], + [13] + ); + } + + /** + * @dataProvider invalidByMonth + * @expectedException InvalidArgumentException + * @depends testValidByMonth + */ + public function testInvalidByMonth($bymonth) + { + new RRule([ + 'FREQ' => 'DAILY', + 'BYMONTH' => $bymonth + ]); + } + + + public function validByDay() + { + return array( + ['MO'], + ['1MO'], + ['+1MO'], + ['-1MO'], + ['53MO'], + ['53MO'] + ); + } + /** + * @dataProvider validByDay + */ + public function testValidByDay($byday) + { + new RRule([ + 'FREQ' => 'DAILY', + 'BYDAY' => $byday + ]); + } + + public function invalidByDay() + { + return array( + [0], + ['54MO'], + ['-54MO'] + ); + } + + /** + * @dataProvider invalidByDay + * @expectedException InvalidArgumentException + * @depends testValidByDay + */ + public function testInvalidByDay($byday) + { + new RRule([ + 'FREQ' => 'DAILY', + 'BYDAY' => $byday + ]); + } + + + public function testIsLeapYear() + { + $this->assertFalse(\RRule\is_leap_year(1700)); + $this->assertFalse(\RRule\is_leap_year(1800)); + $this->assertFalse(\RRule\is_leap_year(1900)); + $this->assertTrue(\RRule\is_leap_year(2000)); + } + +// datetime\(([0-9]+), ([0-9]+), ([0-9]+)[ ,0-9\)]+ + + public function yearlyRules() + { + return array( + array([],['1997-09-02','1998-09-02','1999-09-02']), + array(['INTERVAL' => 2], ['1997-09-02','1999-09-02','2001-09-02']), + array(['BYMONTH' => [1,3]], ['1998-01-02','1998-03-02','1999-01-02']), + array(['BYMONTHDAY' => [1,3]], ['1997-09-03','1997-10-01','1997-10-03']), + array(['BYMONTH' => [1,3], 'BYMONTHDAY' => [5,7]], ['1998-01-05','1998-01-07','1998-03-05']), + array(['BYDAY' => ['TU','TH']], ['1997-09-02','1997-09-04','1997-09-09']), + array(['BYDAY' => ['SU']], ['1997-09-07','1997-09-14','1997-09-21']), + array(['BYDAY' => ['1TU','-1TH']], ['1997-12-25','1998-01-06','1998-12-31']), + array(['BYDAY' => ['3TU','-3TH']], ['1997-12-11','1998-01-20','1998-12-17']), + array(['BYMONTH' => [1,3], 'BYDAY' => ['TU','TH']], ['1998-01-01','1998-01-06','1998-01-08']), + array(['BYMONTH' => [1,3], 'BYDAY' => ['1TU','-1TH']], ['1998-01-06','1998-01-29','1998-03-03']), + // This is interesting because the TH(-3) ends up before the TU(3). + array(['BYMONTH' => [1,3], 'BYDAY' => ['3TU','-3TH']], ['1998-01-15','1998-01-20','1998-03-12']), + array(['BYMONTHDAY' => [1,3], 'BYDAY' => ['TU','TH']], ['1998-01-01','1998-02-03','1998-03-03']), + array(['BYMONTHDAY' => [1,3], 'BYDAY' => ['TU','TH'], 'BYMONTH' => [1,3]], ['1998-01-01','1998-03-03','2001-03-01']), + array(['BYYEARDAY' => [1,100,200,365], 'COUNT' => 4], ['1997-12-31','1998-01-01','1998-04-10', '1998-07-19']), + array(['BYYEARDAY' => [-365, -266, -166, -1], 'COUNT' => 4], ['1997-12-31','1998-01-01','1998-04-10', '1998-07-19']), + array(['BYYEARDAY' => [1,100,200,365], 'BYMONTH' => [4,7], 'COUNT' => 4], ['1998-04-10','1998-07-19','1999-04-10', '1999-07-19']), + array(['BYYEARDAY' => [-365, -266, -166, -1], 'BYMONTH' => [4,7], 'COUNT' => 4], ['1998-04-10','1998-07-19','1999-04-10', '1999-07-19']), + // array(['BYWEEKNO' => 20],['1998-5-11','1998-5-12','1998-5-13']), + // // That's a nice one. The first days of week number one may be in the last year. + // array(['BYWEEKNO' => 1, 'BYDAY' => 'MO'], ['1997-12-29', '1999-01-04', '2000-01-03']), + // // Another nice test. The last days of week number 52/53 may be in the next year. + // array(['BYWEEKNO' => 52, 'BYDAY' => 'SU'], ['1997-12-28', '1998-12-27', '2000-01-02']), + // array(['BYWEEKNO' => -1, 'BYDAY' => 'SU'], ['1997-12-28', '1999-01-03', '2000-01-02']), + // array(['BYWEEKNO' => 53, 'BYDAY' => 'MO'], ['1998-12-28', '2004-12-27', '2009-12-28']), + + // FIXME (time part missing) + // array(['BYHOUR' => [6, 18]], ['1997-09-02','1998-09-02','1998-09-02']), + // array(['BYMINUTE'=> [6, 18]], ['1997-9-2', '1997-9-2', '1998-9-2']), + // array(['BYSECOND' => [6, 18]], ['1997-9-2', '1997-9-2', '1998-9-2']), + // array(['BYHOUR' => [6, 18], 'BYMINUTE' => [6, 18]], ['1997-9-2','1997-9-2','1998-9-2']), + // array(['BYHOUR' => [6, 18], 'BYSECOND' => [6, 18]], ['1997-9-2','1997-9-2','1998-9-2']), + // array(['BYMINUTE' => [6, 18], 'BYSECOND' => [6, 18]], ['1997-9-2','1997-9-2','1997-9-2']), + // array(['BYHOUR'=>[6, 18],'BYMINUTE'=>[6, 18],'BYSECOND'=>[6, 18]],['1997-9-2','1997-9-2','1997-9-2']), + // array(['BYMONTHDAY'=>15,'BYHOUR'=>[6, 18],'BYSETPOS'=>[3, -3],['1997-11-15','1998-2-15','1998-11-15']) + + ); + } + + /** + * @dataProvider yearlyRules + */ + public function testYearly($rule, $occurrences) + { + $rule = new RRule(array_merge([ + 'FREQ' => 'YEARLY', + 'COUNT' => 3, + 'DTSTART' => '1997-09-02' + ], $rule)); + $this->assertEquals($occurrences, $rule->getOccurrences()); + } + + + public function monthlyRules() + { + return array( + + ); + } + + /** + * @dataProvider monthlyRules + */ + public function testMonthly($rule, $occurrences) + { + $rule = new RRule(array_merge([ + 'FREQ' => 'MONTHLY', + 'COUNT' => 3, + 'DTSTART' => '1997-09-02' + ], $rule)); + $this->assertEquals($occurrences, $rule->getOccurrences()); + } +} \ No newline at end of file diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100755 index 0000000..f091c14 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,3 @@ +