1
0
mirror of https://github.com/rlanvin/php-rrule.git synced 2025-02-20 09:54:16 +01:00

Work in progress

This commit is contained in:
rlanvin 2015-06-26 17:02:29 +03:00
parent 1535174243
commit 8a20641693
5 changed files with 727 additions and 338 deletions

37
LICENSE
View File

@ -18,4 +18,39 @@ 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.
SOFTWARE.
-----------------------
Based on Python's dateutil.
dateutil - Extensions to the standard Python datetime module.
Copyright (c) 2003-2011 - Gustavo Niemeyer <gustavo@niemeyer.net>
Copyright (c) 2012-2014 - Tomi Pieviläinen <tomi.pievilainen@iki.fi>
Copyright (c) 2014 - Yaron de Leeuw <me@jarondl.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,3 @@
# RRULE for PHP
work in progress

View File

@ -6,7 +6,7 @@
"homepage": "https://github.com/rlanvin/php-rrule",
"license": "MIT",
"require": {
"php": ">=5.3.0"
"php": ">=5.4.0"
},
"autoload": {
"classmap": ["src/"]

View File

@ -4,12 +4,21 @@
* Implementation of RRULE as defined by RFC 5545.
*
* Heavily based on dateutil/rrule.py
*
* Some useful terms to understand the algorithms and variables naming:
*
* yearday = day of the year, from 0 to 365 (on leap years) - date('z')
* weekday = day of the week (ISO-8601), from 1 (MO) to 7 (SU) - date('N')
* monthday = day of the month, from 1 to 31
* wkst = week start, the weekday (1 to 7) which is the first day of week.
* Default is Monday (1). In some countries it's Sunday (7).
* weekno = number of the week in the year (ISO-8601)
*
* CAREFUL with that bug: https://bugs.php.net/bug.php?id=62476
*/
namespace RRule;
define(__NAMESPACE__.'\MAX_YEAR',date('Y', PHP_INT_MAX));
/**
* @return bool
*/
@ -66,19 +75,35 @@ function is_leap_year($year)
*/
class RRule implements \Iterator, \ArrayAccess
{
// frequencies
public static $frequencies = ['SECONDLY','MINUTELY','HOURLY','DAILY','WEEKLY','MONTHLY','YEARLY'];
const SECONDLY = 7;
const MINUTELY = 6;
const HOURLY = 5;
const DAILY = 4;
const WEEKLY = 3;
const MONTHLY = 2;
const YEARLY = 1;
const SECONDLY = 0;
const MINUTELY = 1;
const HOURLY = 2;
const DAILY = 3;
const WEEKLY = 4;
const MONTHLY = 5;
const YEARLY = 6;
// frequency names
public static $frequencies = [
'SECONDLY' => self::SECONDLY,
'MINUTELY' => self::MINUTELY,
'HOURLY' => self::HOURLY,
'DAILY' => self::DAILY,
'WEEKLY' => self::WEEKLY,
'MONTHLY' => self::MONTHLY,
'YEARLY' => self::YEARLY
];
// weekdays numbered from 1 (ISO-8601 or date('N'))
public static $week_days = ['MO' => 1,'TU' => 2,'WE' => 3,'TH' => 4,'FR' => 5,'SA' => 6,'SU' => 7];
public static $week_days = [
'MO' => 1,
'TU' => 2,
'WE' => 3,
'TH' => 4,
'FR' => 5,
'SA' => 6,
'SU' => 7
];
// original rule
protected $rule = array(
@ -101,7 +126,6 @@ class RRule implements \Iterator, \ArrayAccess
// parsed and validated values
protected $dtstart = null;
protected $dtstart_ts = null;
protected $freq = null;
protected $until = null;
protected $count = null;
@ -118,6 +142,7 @@ class RRule implements \Iterator, \ArrayAccess
protected $bymonth = null;
protected $bysetpos = null;
protected $wkst = null;
protected $timeset = null;
// Public interface
@ -126,10 +151,15 @@ class RRule implements \Iterator, \ArrayAccess
*/
public function __construct(array $parts)
{
$parts = array_change_key_case($parts, CASE_UPPER);
// validate extra parts
$unsupported = array_diff_key($parts, $this->rule);
if ( ! empty($unsupported) ) {
throw new \InvalidArgumentException('Unsupported parameter(s): '.implode(',',array_keys($unsupported)));
throw new \InvalidArgumentException(
'Unsupported parameter(s): '
.implode(',',array_keys($unsupported))
);
}
$parts = array_merge($this->rule, $parts);
@ -138,62 +168,75 @@ class RRule implements \Iterator, \ArrayAccess
// 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)));
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));
if ( (is_int($parts['FREQ']) && ($parts['FREQ'] < self::SECONDLY || $parts['FREQ'] > self::YEARLY))
|| ! array_key_exists($parts['FREQ'], self::$frequencies) ) {
throw new \InvalidArgumentException(
'The FREQ rule part must be one of the following: '
.implode(', ',array_keys(self::$frequencies))
);
}
$this->freq = $parts['FREQ'];
$this->freq = self::$frequencies[$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)');
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']) ) {
if ( $parts['DTSTART'] instanceof \DateTime ) {
$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 {
try {
if ( is_integer($parts['DTSTART']) ) {
$this->dtstart = \DateTime::createFromFormat('U',$parts['DTSTART']);
}
else {
$this->dtstart = new \DateTime($parts['DTSTART']);
}
} catch (\Exception $e) {
throw new \InvalidArgumentException(
'Failed to parse DTSTART ; it must be a valid date, timestamp or \DateTime object'
);
}
}
}
else {
$this->dtstart = date('Y-m-d');
$this->dtstart_ts = strtotime($this->dtstart);
$this->dtstart = new \DateTime();
}
// UNTIL (optional)
if ( not_empty($parts['UNTIL']) ) {
if ( is_string($parts['UNTIL']) ) {
if ( $parts['UNTIL'] instanceof \DateTime ) {
$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');
else {
try {
if ( is_integer($parts['UNTIL']) ) {
$this->until = \DateTime::createFromFormat('U',$parts['UNTIL']);
}
else {
$this->until = new \DateTime($parts['UNTIL']);
}
} catch (\Exception $e) {
throw new \InvalidArgumentException(
'Failed to parse UNTIL ; it must be a valid date, timestamp or \DateTime object'
);
}
}
}
@ -209,64 +252,21 @@ class RRule implements \Iterator, \ArrayAccess
// 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':
case self::YEARLY:
if ( ! not_empty($parts['BYMONTH']) ) {
$parts['BYMONTH'] = [date('m',$this->dtstart_ts)];
$parts['BYMONTH'] = [(int) $this->dtstart->format('m')];
}
$parts['BYMONTHDAY'] = [date('j', $this->dtstart_ts)];
$parts['BYMONTHDAY'] = [(int) $this->dtstart->format('j')];
break;
case 'MONTHLY':
$parts['BYMONTHDAY'] = [date('j',$this->dtstart_ts)];
case self::MONTHLY:
$parts['BYMONTHDAY'] = [(int) $this->dtstart->format('j')];
break;
case 'WEEKLY':
$parts['BYDAY'] = [array_search(date('N', $this->dtstart_ts), self::$week_days)];
case self::WEEKLY:
$parts['BYDAY'] = [array_search($this->dtstart->format('N'), 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']) ) {
@ -275,10 +275,12 @@ class RRule implements \Iterator, \ArrayAccess
$this->byweekday = [];
$this->byweekday_relative = [];
foreach ( $parts['BYDAY'] as $value ) {
$value = trim($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]];
}
@ -287,12 +289,12 @@ class RRule implements \Iterator, \ArrayAccess
}
}
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 ( ! empty($this->byweekday_relative) ) {
if ( ! ($this->freq === self::MONTHLY || $this->freq === self::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.');
if ( $this->freq === self::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.');
}
}
}
@ -303,7 +305,7 @@ class RRule implements \Iterator, \ArrayAccess
// 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' ) {
if ( $this->freq === self::WEEKLY ) {
throw new \InvalidArgumentException('The BYMONTHDAY rule part MUST NOT be specified when the FREQ rule part is set to WEEKLY.');
}
@ -327,7 +329,7 @@ class RRule implements \Iterator, \ArrayAccess
}
if ( not_empty($parts['BYYEARDAY']) ) {
if ( $this->freq == 'DAILY' || $this->freq == 'WEEKLY' || $this->freq == 'MONTHLY' ) {
if ( $this->freq === self::DAILY || $this->freq === self::WEEKLY || $this->freq === self::MONTHLY ) {
throw new \InvalidArgumentException('The BYYEARDAY rule part MUST NOT be specified when the FREQ rule part is set to DAILY, WEEKLY, or MONTHLY.');
}
@ -347,7 +349,7 @@ class RRule implements \Iterator, \ArrayAccess
// BYWEEKNO
if ( not_empty($parts['BYWEEKNO']) ) {
if ( $this->freq !== 'YEARLY' ) {
if ( $this->freq !== self::YEARLY ) {
throw new \InvalidArgumentException('The BYWEEKNO rule part MUST NOT be used when the FREQ rule part is set to anything other than YEARLY.');
}
@ -398,6 +400,89 @@ class RRule implements \Iterator, \ArrayAccess
$this->bysetpos[] = (int) $value;
}
}
// now for the time options
// this gets more complicated
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;
}
if ( $this->freq === self::HOURLY ) {
// do something (__construct_byset) ?
}
}
elseif ( $this->freq < self::HOURLY ) {
$this->byhour = [(int) $this->dtstart->format('G')];
}
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 ( $this->freq == self::MINUTELY ) {
// do something
}
}
elseif ( $this->freq < self::MINUTELY ) {
$this->byminute = [(int) $this->dtstart->format('i')];
}
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 ( $this->freq == self::SECONDLY ) {
// do something
}
}
elseif ( $this->freq < self::SECONDLY ) {
$this->bysecond = [(int) $this->dtstart->format('s')];
}
if ( $this->freq < self::HOURLY ) {
// for frequencies DAILY, WEEKLY, MONTHLY AND YEARLY, we build
// an array of every time of the day at which there should be an
// occurence - default, if no BYHOUR/BYMINUTE/BYSECOND are provided
// is only one time, and it's the DTSTART time.
$this->timeset = array();
foreach ( $this->byhour as $hour ) {
foreach ( $this->byminute as $minute ) {
foreach ( $this->bysecond as $second ) {
// fixme another format?
$this->timeset[] = [$hour,$minute,$second];
}
}
}
sort($this->timeset);
}
}
public function getOccurrences()
@ -498,15 +583,15 @@ class RRule implements \Iterator, \ArrayAccess
protected function getDaySet($year, $month, $day, array $masks)
{
switch ( $this->freq ) {
case 'YEARLY':
case self::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];
case self::MONTHLY:
$start = $masks['last_day_of_month'][$month-1];
$stop = $masks['last_day_of_month'][$month];
return range($start, $stop - 1);
case 'WEEKLY':
case self::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
@ -518,16 +603,16 @@ class RRule implements \Iterator, \ArrayAccess
for ( $j = 0; $j < 7; $j++ ) {
$set[] = $i;
$i += 1;
if ( $masks['doy_to_weekday'][$i] == $this->wkst ) {
if ( $masks['yearday_to_weekday'][$i] == $this->wkst ) {
break;
}
}
return $set;
case 'DAILY':
case 'HOURLY':
case 'MINUTELY':
case 'SECONDLY':
case self::DAILY:
case self::HOURLY:
case self::MINUTELY:
case self::SECONDLY:
$n = (int) date('z', mktime(0,0,0,$month,$day,$year));
return [$n];
}
@ -536,42 +621,43 @@ class RRule implements \Iterator, \ArrayAccess
/**
* Some serious magic is happening here.
*/
protected function buildWeekdayMasks($year, $month, $day, array & $masks)
protected function buildNthWeekdayMask($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();
$masks['yearday_is_in_weekday_relative'] = array();
if ( $this->byweekday_relative ) {
$ranges = array();
if ( $this->freq == 'YEARLY' ) {
if ( $this->freq == self::YEARLY ) {
if ( $this->bymonth ) {
foreach ( $this->bymonth as $bymonth ) {
$ranges[] = [$masks['month_to_last_day'][$bymonth-1], $masks['month_to_last_day'][$bymonth]];
$ranges[] = [$masks['last_day_of_month'][$bymonth-1], $masks['last_day_of_month'][$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]];
elseif ( $this->freq == self::MONTHLY ) {
$ranges[] = [$masks['last_day_of_month'][$month-1], $masks['last_day_of_month'][$month]];
}
if ( $ranges ) {
// Weekly frequency won't get here, so we may not
// care about cross-year weekly periods.
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);
$i = $i - pymod($masks['yearday_to_weekday'][$i] - $weekday, 7);
}
else {
$i = $first + ($nth - 1) * 7;
$i = $i + (7 - $masks['doy_to_weekday'][$i] + $weekday) % 7;
$i = $i + (7 - $masks['yearday_to_weekday'][$i] + $weekday) % 7;
}
if ( $i >= $first && $i <= $last ) {
$masks['doy_to_weekday_relative'][$i] = 1;
$masks['yearday_is_in_weekday_relative'][$i] = true;
}
}
}
@ -579,6 +665,116 @@ class RRule implements \Iterator, \ArrayAccess
}
}
/**
* More magic
*/
protected function buildWeeknoMask($year, $month, $day, & $masks)
{
$masks['yearday_is_in_weekno'] = array();
// calculate the index of the first wkst day of the year
// 0 means the first day of the year is the wkst day (e.g. wkst is Monday and Jan 1st is a Monday)
// n means there is n days before the first wkst day of the year.
// if n >= 4, this is the first day of the year (even though it started the year before)
$first_wkst = (7 - $masks['weekday_of_1st_yearday'] + $this->wkst) % 7;
if( $first_wkst >= 4 ) {
$first_wkst_offset = 0;
// Number of days in the year, plus the days we got from last year.
$nb_days = $masks['year_len'] + $masks['weekday_of_1st_yearday'] - $this->wkst;
// $nb_days = $masks['year_len'] + pymod($masks['weekday_of_1st_yearday'] - $this->wkst,7);
}
else {
$first_wkst_offset = $first_wkst;
// Number of days in the year, minus the days we left in last year.
$nb_days = $masks['year_len'] - $first_wkst;
}
$nb_weeks = (int) ($nb_days / 7) + (int) (($nb_days % 7) / 4);
// alright now we now when the first week starts
// and the number of weeks of the year
// so we can generate a map of every yearday that are in the weeks
// specified in byweekno
foreach ( $this->byweekno as $n ) {
if ( $n < 0 ) {
$n = $n + $nb_weeks + 1;
}
if ( $n <= 0 || $n > $nb_weeks ) {
continue;
}
if ( $n > 1 ) {
$i = $first_wkst_offset + ($n - 1) * 7;
if ( $first_wkst_offset != $first_wkst ) {
// if week #1 started the previous year
// realign the start of the week
$i = $i - (7 - $first_wkst);
}
}
else {
$i = $first_wkst_offset;
}
// now add 7 days into the resultset, stopping either at 7 or
// if we reach wkst before (in the case of short first week of year)
for ( $j = 0; $j < 7; $j++ ) {
$masks['yearday_is_in_weekno'][$i] = true;
$i = $i + 1;
if ( $masks['yearday_to_weekday'][$i] == $this->wkst ) {
break;
}
}
}
// if we asked for week #1, it's possible that the week #1 of next year
// already started this year. Therefore we need to return also the matching
// days of next year.
if ( in_array(1, $this->byweekno) ) {
// Check week number 1 of next year as well
// TODO: Check -numweeks for next year.
$i = $first_wkst_offset + $nb_weeks * 7;
if ( $first_wkst_offset != $first_wkst ) {
$i = $i - (7 - $first_wkst);
}
if ( $i < $masks['year_len'] ) {
// If week starts in next year, we don't care about it.
for ( $j = 0; $j < 7; $j++ ) {
$masks['yearday_is_in_weekno'][$i] = true;
$i += 1;
if ( $masks['yearday_to_weekday'][$i] == $this->wkst ) {
break;
}
}
}
}
if ( $first_wkst_offset ) {
// Check last week number of last year as well.
// If first_wkst_offset is 0, either the year started on week start,
// or week number 1 got days from last year, so there are no
// days from last year's last week number in this year.
if ( ! in_array(-1, $this->byweekno) ) {
$weekday_of_1st_yearday = date('N', mktime(0,0,0,1,1,$year-1));
$first_wkst_offset_last_year = (7 - $weekday_of_1st_yearday + $this->wkst) % 7;
$last_year_len = 365 + is_leap_year($year - 1);
if ( $first_wkst_offset_last_year >= 4) {
$first_wkst_offset_last_year = 0;
$nb_weeks_last_year = 52 + (int) ((($last_year_len + ($weekday_of_1st_yearday - $this->wkst) % 7) % 7) / 4);
}
else {
$nb_weeks_last_year = 52 + (int) ((($masks['year_len'] - $first_wkst_offset) % 7) /4);
}
}
else {
$nb_weeks_last_year = -1;
}
if ( in_array($nb_weeks_last_year, $this->byweekno) ) {
for ( $i = 0; $i < $first_wkst_offset; $i++ ) {
$masks['yearday_is_in_weekno'][$i] = true;
}
}
}
}
/**
* This is the main method, where all of the logic happens.
*
@ -589,32 +785,54 @@ class RRule implements \Iterator, \ArrayAccess
// 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 $hour = null, $minute = null, $second = null;
static $current_set = null, $masks = null, $timeset = null;
static $total = 0;
if ( $reset ) {
$year = $month = $day = null;
$current_set = null;
$hour = $minute = $second = null;
$current_set = $masks = $timeset = 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' ) {
if ( $this->freq === self::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));
// simply loop by adding +7 days. The Python lib does some
// calculation magic at the end of the loop (when incrementing)
// to realign on first pass.
$tmp = $this->dtstart->modify('-'.pymod($this->dtstart->format('N') - $this->wkst,7).'days');
list($year,$month,$day) = explode('-',$tmp->format('Y-n-j'));
unset($tmp);
}
else {
list($year,$month,$day) = explode('-',$this->dtstart);
list($year,$month,$day) = explode('-',$this->dtstart->format('Y-n-j'));
}
}
// todo, not sure when this should be rebuilt
// and not sure what this does anyway
if ( $timeset == null ) {
if ( $this->freq < self::HOURLY ) { // daily, weekly, monthly or yearly
$timeset = $this->timeset;
}
else {
if (
($this->freq >= self::HOURLY && $this->byhour && ! in_array($hour, $this->byhour))
|| ($this->freq >= self::MINUTELY && $this->byminute && ! in_array($minute, $this->byminute))
|| ($this->freq >= self::SECONDLY && $this->bysecond && ! in_array($second, $this->bysecond))
) {
$timeset = array();
}
else {
$timeset = $this->getTimeSet($hour, $minute, $second);
}
}
}
@ -626,83 +844,89 @@ class RRule implements \Iterator, \ArrayAccess
// 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;
if ( empty($masks) || $masks['year'] != $year || $masks['month'] != $month ) {
$masks = array('year' => '','month'=>'');
// only if year has changed
if ( $masks['year'] != $year ) {
$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);
$masks['weekday_of_1st_yearday'] = date('N', mktime(0,0,0,1,1,$year));
$masks['yearday_to_weekday'] = array_slice(self::$WEEKDAY_MASK, $masks['weekday_of_1st_yearday']-1);
if ( $masks['leap_year'] ) {
$masks['yearday_to_month'] = self::$MONTH_MASK_366;
$masks['yearday_to_monthday'] = self::$MONTHDAY_MASK_366;
$masks['yearday_to_monthday_negative'] = self::$NEGATIVE_MONTHDAY_MASK_366;
$masks['last_day_of_month'] = self::$LAST_DAY_OF_MONTH_366;
}
else {
$masks['yearday_to_month'] = self::$MONTH_MASK;
$masks['yearday_to_monthday'] = self::$MONTHDAY_MASK;
$masks['yearday_to_monthday_negative'] = self::$NEGATIVE_MONTHDAY_MASK;
$masks['last_day_of_month'] = self::$LAST_DAY_OF_MONTH;
}
if ( $this->byweekno ) {
$this->buildWeeknoMask($year, $month, $day, $masks);
}
}
// everytime month or year changes
if ( $this->byweekday_relative ) {
$this->buildNthWeekdayMask($year, $month, $day, $masks);
}
$masks['year'] = $year;
$masks['month'] = $month;
}
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);
// calculate the current set
$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";
// echo"\tWorking with $year-$month-$day set=".json_encode($current_set)."\n";
// print_r(json_encode($masks));
// 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.
foreach ( $current_set as $yearday ) {
if ( $this->bymonth && ! in_array($masks['yearday_to_month'][$yearday], $this->bymonth) ) {
continue;
}
// 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]) ) {
if ( $this->byweekno && ! isset($masks['yearday_is_in_weekno'][$yearday]) ) {
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) ) {
if ( $yearday < $masks['year_len'] ) {
if ( ! in_array($yearday + 1, $this->byyearday) && ! in_array(- $masks['year_len'] + $yearday,$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) ) {
else { // if ( ($yearday >= $masks['year_len']
if ( ! in_array($yearday + 1 - $masks['year_len'], $this->byyearday) && ! in_array(- $masks['next_year_len'] + $yearday - $mask['year_len'], $this->byyearday) ) {
continue;
}
}
}
$filtered_set[] = $day_of_year;
if ( ($this->bymonthday || $this->bymonthday_negative)
&& ! in_array($masks['yearday_to_monthday'][$yearday], $this->bymonthday)
&& ! in_array($masks['yearday_to_monthday_negative'][$yearday], $this->bymonthday_negative) ) {
continue;
}
if ( $this->byweekday && ! in_array($masks['yearday_to_weekday'][$yearday], $this->byweekday) ) {
continue;
}
if ( $this->byweekday_relative && ! isset($masks['yearday_is_in_weekday_relative'][$yearday]) ) {
continue;
}
$filtered_set[] = $yearday;
}
// 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
// XXX this needs to be applied after expanding the timeset
if ( $this->bysetpos ) {
$filtered_set = [];
$n = sizeof($current_set);
@ -726,30 +950,37 @@ class RRule implements \Iterator, \ArrayAccess
// 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));
while ( ($yearday = current($current_set)) !== false ) {
// $occurrence = date('Y-m-d', mktime(0, 0, 0, 1, ($yearday + 1), $year));
// echo "\t occurrence (mktime) = ", $occurrence,"\n";
$occurrence = \DateTime::createFromFormat('Y z', "$year $yearday");
// echo "\t occurrence (before time) =", $occurrence->format('r'),"\n";
while ( ($time = current($timeset)) !== false ) {
$occurrence->setTime($time[0], $time[1], $time[2]);
// consider end conditions
if ( $this->until && $occurrence > $this->until ) {
// $this->length = $total (?)
return null;
}
// consider end conditions
if ( $this->until && $occurrence > $this->until ) {
// $this->length = $total (?)
return null;
next($timeset);
if ( $occurrence >= $this->dtstart ) { // ignore occurences before DTSTART
$total += 1;
return $occurrence; // yield
}
}
reset($timeset);
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':
case self::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':
case self::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 ) {
@ -763,22 +994,20 @@ class RRule implements \Iterator, \ArrayAccess
}
}
break;
case 'WEEKLY':
case self::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))));
// list($year,$month,$day) = explode('-',date('Y-m-d',strtotime('+'.($this->interval*7).'day', mktime(0,0,0,$month,$day,$year))));
list($year,$month,$day) = explode('-',(new \DateTime("$year-$month-$day"))->modify('+'.($this->interval*7).'day')->format('Y-n-j'));
break;
case 'DAILY':
case self::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))));
// list($year,$month,$day) = explode('-',date('Y-m-d',strtotime('+'.$this->interval.'day', mktime(0,0,0,$month,$day,$year))));
list($year,$month,$day) = explode('-',(new \DateTime("$year-$month-$day"))->modify('+'.$this->interval.'day')->format('Y-n-j'));
break;
case 'HOURLY':
case 'MINUTELY':
case 'SECONDLY':
throw new LogicException('Unimplemented');
}
// prevent overflow (especially on 32 bits system)
if ( $year >= MAX_YEAR ) {
return null;
case self::HOURLY:
case self::MINUTELY:
case self::SECONDLY:
throw new \InvalidArgumentException('Unimplemented frequency');
}
}
}

View File

@ -4,109 +4,53 @@ use RRule\RRule;
class RRuleTest extends PHPUnit_Framework_TestCase
{
public function testMissingParameter()
{
$this->setExpectedException('InvalidArgumentException');
new RRule([]);
}
public function testUnsupportedParameter()
{
$this->setExpectedException('InvalidArgumentException');
new RRule([
'FREQ' => 'DAILY',
'FOO' => 'BAR'
]);
}
public function validByMonth()
public function invalidRules()
{
return array(
['1'],
['1,2'],
[[1,2]]
);
}
/**
* @dataProvider validByMonth
*/
public function testValidByMonth($bymonth)
{
new RRule([
'FREQ' => 'DAILY',
'BYMONTH' => $bymonth
]);
}
array([]),
array(['FREQ' => 'foobar']),
array(['FREQ' => 'DAILY', 'INTERVAL' => -1]),
array(['FREQ' => 'DAILY', 'UNTIL' => 'foobar']),
array(['FREQ' => 'DAILY', 'COUNT' => -1]),
public function invalidByMonth()
{
return array(
[0],
['-1'],
[-1],
[13]
// The BYDAY rule part MUST NOT be specified with a numeric value
// when the FREQ rule part is not set to MONTHLY or YEARLY.
array(['FREQ' => 'DAILY', 'BYDAY' => ['1MO']]),
array(['FREQ' => 'WEEKLY', 'BYDAY' => ['1MO']]),
// 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.
array(['FREQ' => 'YEARLY', 'BYDAY' => ['1MO'], 'BYWEEKNO' => 20]),
array(['FREQ' => 'DAILY', 'BYMONTHDAY' => 0]),
array(['FREQ' => 'DAILY', 'BYMONTHDAY' => 32]),
array(['FREQ' => 'DAILY', 'BYMONTHDAY' => -32]),
// The BYMONTHDAY rule part MUST NOT be specified when the FREQ rule
// part is set to WEEKLY.
array(['FREQ' => 'WEEKLY', 'BYMONTHDAY' => 1]),
array(['FREQ' => 'YEARLY', 'BYYEARDAY' => 0]),
array(['FREQ' => 'YEARLY', 'BYYEARDAY' => 367]),
// The BYYEARDAY rule part MUST NOT be specified when the FREQ
// rule part is set to DAILY, WEEKLY, or MONTHLY.
array(['FREQ' => 'DAILY', 'BYYEARDAY' => 1]),
array(['FREQ' => 'WEEKLY', 'BYYEARDAY' => 1]),
array(['FREQ' => 'MONTHLY', 'BYYEARDAY' => 1]),
// BYSETPOS rule part MUST only be used in conjunction with another
// BYxxx rule part.
array(['FREQ' => 'DAILY', 'BYSETPOS' => -1]),
);
}
/**
* @dataProvider invalidByMonth
* @dataProvider invalidRules
* @expectedException InvalidArgumentException
* @depends testValidByMonth
*/
public function testInvalidByMonth($bymonth)
public function testInvalidRules($rule)
{
new RRule([
'FREQ' => 'DAILY',
'BYMONTH' => $bymonth
]);
new RRule($rule);
}
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));
@ -115,48 +59,48 @@ class RRuleTest extends PHPUnit_Framework_TestCase
$this->assertTrue(\RRule\is_leap_year(2000));
}
// datetime\(([0-9]+), ([0-9]+), ([0-9]+)[ ,0-9\)]+
// date_create\(([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']),
array([],[date_create('1997-09-02'),date_create('1998-09-02'), date_create('1999-09-02')]),
array(['INTERVAL' => 2], [date_create('1997-09-02'),date_create('1999-09-02'),date_create('2001-09-02')]),
array(['DTSTART' => '2000-02-29'], [date_create('2000-02-29'),date_create('2004-02-29'),date_create('2008-02-29')]),
array(['BYMONTH' => [1,3]], [date_create('1998-01-02'),date_create('1998-03-02'),date_create('1999-01-02')]),
array(['BYMONTHDAY' => [1,3]], [date_create('1997-09-03'),date_create('1997-10-01'),date_create('1997-10-03')]),
array(['BYMONTH' => [1,3], 'BYMONTHDAY' => [5,7]], [date_create('1998-01-05'),date_create('1998-01-07'),date_create('1998-03-05')]),
array(['BYDAY' => ['TU','TH']], [date_create('1997-09-02'),date_create('1997-09-04'),date_create('1997-09-09')]),
array(['BYDAY' => ['SU']], [date_create('1997-09-07'),date_create('1997-09-14'),date_create('1997-09-21')]),
array(['BYDAY' => ['1TU','-1TH']], [date_create('1997-12-25'),date_create('1998-01-06'),date_create('1998-12-31')]),
array(['BYDAY' => ['3TU','-3TH']], [date_create('1997-12-11'),date_create('1998-01-20'),date_create('1998-12-17')]),
array(['BYMONTH' => [1,3], 'BYDAY' => ['TU','TH']], [date_create('1998-01-01'),date_create('1998-01-06'),date_create('1998-01-08')]),
array(['BYMONTH' => [1,3], 'BYDAY' => ['1TU','-1TH']], [date_create('1998-01-06'),date_create('1998-01-29'),date_create('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']),
array(['BYMONTH' => [1,3], 'BYDAY' => ['3TU','-3TH']], [date_create('1998-01-15'),date_create('1998-01-20'),date_create('1998-03-12')]),
array(['BYMONTHDAY' => [1,3], 'BYDAY' => ['TU','TH']], [date_create('1998-01-01'),date_create('1998-02-03'),date_create('1998-03-03')]),
array(['BYMONTHDAY' => [1,3], 'BYDAY' => ['TU','TH'], 'BYMONTH' => [1,3]], [date_create('1998-01-01'),date_create('1998-03-03'),date_create('2001-03-01')]),
array(['BYYEARDAY' => [1,100,200,365], 'COUNT' => 4], [date_create('1997-12-31'),date_create('1998-01-01'),date_create('1998-04-10'), date_create('1998-07-19')]),
array(['BYYEARDAY' => [-365, -266, -166, -1], 'COUNT' => 4], [date_create('1997-12-31'),date_create('1998-01-01'),date_create('1998-04-10'), date_create('1998-07-19')]),
array(['BYYEARDAY' => [1,100,200,365], 'BYMONTH' => [4,7], 'COUNT' => 4], [date_create('1998-04-10'),date_create('1998-07-19'),date_create('1999-04-10'), date_create('1999-07-19')]),
array(['BYYEARDAY' => [-365, -266, -166, -1], 'BYMONTH' => [4,7], 'COUNT' => 4], [date_create('1998-04-10'),date_create('1998-07-19'),date_create('1999-04-10'), date_create('1999-07-19')]),
array(['BYWEEKNO' => 20],[date_create('1998-05-11'),date_create('1998-05-12'),date_create('1998-05-13')]),
// That's a nice one. The first days of week number one may be in the last year.
array(['BYWEEKNO' => 1, 'BYDAY' => 'MO'], [date_create('1997-12-29'), date_create('1999-01-04'), date_create('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'], [date_create('1997-12-28'), date_create('1998-12-27'), date_create('2000-01-02')]),
array(['BYWEEKNO' => -1, 'BYDAY' => 'SU'], [date_create('1997-12-28'), date_create('1999-01-03'), date_create('2000-01-02')]),
array(['BYWEEKNO' => 53, 'BYDAY' => 'MO'], [date_create('1998-12-28'), date_create('2004-12-27'), date_create('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'])
// array(['BYHOUR' => [6, 18]], [date_create('1997-09-02'),date_create('1998-09-02'),date_create('1998-09-02')]),
// array(['BYMINUTE'=> [6, 18]], ['1997-09-02', '1997-09-02', '1998-09-02']),
// array(['BYSECOND' => [6, 18]], ['1997-09-02', '1997-09-02', '1998-09-02']),
// array(['BYHOUR' => [6, 18], 'BYMINUTE' => [6, 18]], ['1997-09-02','1997-09-02','1998-09-02']),
// array(['BYHOUR' => [6, 18], 'BYSECOND' => [6, 18]], ['1997-09-02','1997-09-02','1998-09-02']),
// array(['BYMINUTE' => [6, 18], 'BYSECOND' => [6, 18]], ['1997-09-02','1997-09-02','1997-09-02']),
// array(['BYHOUR'=>[6, 18],'BYMINUTE'=>[6, 18],'BYSECOND'=>[6, 18]],['1997-09-02','1997-09-02','1997-09-02']),
// array(['BYMONTHDAY'=>15,'BYHOUR'=>[6, 18],'BYSETPOS'=>[3, -3],[date_create('1997-11-15'),date_create('1998-02-15'),date_create('1998-11-15')])
);
}
@ -177,7 +121,31 @@ class RRuleTest extends PHPUnit_Framework_TestCase
public function monthlyRules()
{
return array(
array([],[date_create('1997-09-02'),date_create('1997-10-02'),date_create('1997-11-02')]),
array(['INTERVAL'=>2],[date_create('1997-09-02'),date_create('1997-11-02'),date_create('1998-01-02')]),
array(['INTERVAL'=>18],[date_create('1997-09-02'),date_create('1999-03-02'),date_create('2000-09-02')]),
array(['BYMONTH' => [1, 3]],[date_create('1998-01-02'),date_create('1998-03-02'),date_create('1999-01-02')]),
array(['BYMONTHDAY' => [1, 3]],[date_create('1997-09-03'),date_create('1997-10-01'),date_create('1997-10-03')]),
array(['BYMONTHDAY' => [5, 7], 'BYMONTH' => [1, 3]], [date_create('1998-01-05'), date_create('1998-01-07'), date_create('1998-03-05')]),
array(['BYDAY' => ['TU', 'TH']], [date_create('1997-09-02'),date_create('1997-09-04'),date_create('1997-09-09')]),
// Third Monday of the month
array(['BYDAY' => '3MO'],[date_create('1997-09-15'),date_create('1997-10-20'),date_create('1997-11-17')]),
array(['BYDAY' => '1TU,-1TH'],[date_create('1997-09-02'),date_create('1997-09-25'),date_create('1997-10-07')]),
array(['BYDAY' => '3TU,-3TH'],[date_create('1997-09-11'),date_create('1997-09-16'),date_create('1997-10-16')]),
array(['BYDAY' => 'TU,TH', 'BYMONTH' => [1, 3]],[date_create('1998-01-01'),date_create('1998-01-06'),date_create('1998-01-08')]),
array(['BYMONTH' => [1, 3], 'BYDAY' => '1TU, -1TH'],[date_create('1998-01-06'),date_create('1998-01-29'),date_create('1998-03-03')]),
array(['BYMONTH' => [1, 3], 'BYDAY' => '3TU, -3TH'],[date_create('1998-01-15'),date_create('1998-01-20'),date_create('1998-03-12')]),
array(['BYMONTHDAY' => [1, 3], 'BYDAY' => ['TU', 'TH']], [date_create('1998-01-01'),date_create('1998-02-03'),date_create('1998-03-03')]),
array(['BYMONTH' => [1, 3], 'BYMONTHDAY' => [1, 3], 'BYDAY' => ['TU', 'TH']],[date_create('1998-01-01'),date_create('1998-03-03'),date_create('2001-03-01')]),
// array(['BYHOUR'=> [6, 18],['1997-09-02',date_create('1997-10-02'),date_create('1997-10-02')]),
// array(['BYMINUTE'=> [6, 18],['1997-09-02','1997-09-02',date_create('1997-10-02')]),
// array(['BYSECOND' => [6, 18],['1997-09-02','1997-09-02',date_create('1997-10-02')]),
// array(['BYHOUR'=>[6, 18],'BYMINUTE'=>[6, 18]],['1997-09-02','1997-09-02',date_create('1997-10-02')]),
// array(['BYHOUR'=>[6, 18],'BYSECOND'=>[6, 18]],['1997-09-02','1997-09-02',date_create('1997-10-02')]),
// array(['BYMINUTE'=>[6, 18],'BYSECOND'=>[6, 18]],['1997-09-02','1997-09-02','1997-09-02']),
// array(['BYHOUR'=>[6, 18],'BYMINUTE'=>[6, 18],'BYSECOND'=>[6, 18]],['1997-09-02','1997-09-02','1997-09-02']),
// array(['BYMONTHDAY'=>[13, 17],'BYHOUR'=>[6, 18],'BYSETPOS'=>[3, -3]],[date_create('1997-09-13'),date_create('1997-09-17'),date_create('1997-10-13')])
);
}
@ -193,4 +161,158 @@ class RRuleTest extends PHPUnit_Framework_TestCase
], $rule));
$this->assertEquals($occurrences, $rule->getOccurrences());
}
public function weeklyRules()
{
return array(
array([],[date_create('1997-09-02'), date_create('1997-09-09'), date_create('1997-09-16')]),
array(['interval'=>2],[date_create('1997-09-02'),date_create('1997-09-16'),date_create('1997-09-30')]),
array(['interval'=>20],[date_create('1997-09-02'),date_create('1998-01-20'),date_create('1998-06-09')]),
array(['bymonth'=>[1, 3]],[date_create('1998-01-06'),date_create('1998-01-13'),date_create('1998-01-20')]),
array(['byday'=> ['TU', 'TH']],[date_create('1997-09-02'), date_create('1997-09-04'), date_create('1997-09-09')]),
# This test is interesting, because it crosses the year
# boundary in a weekly period to find day '1' as a
# valid recurrence.
array(['bymonth'=>[1, 3],'byday'=>['TU', 'TH']],[date_create('1998-01-01'), date_create('1998-01-06'), date_create('1998-01-08')]),
// array(['byhour'=>[6, 18]],[date_create('1997-09-02'),date_create('1997-09-09'),date_create('1997-09-09')]),
// array(['byminute'=>[6, 18]],[date_create('1997-09-02'),date_create('1997-09-02'),date_create('1997-09-09')]),
// array(['bysecond'=> [6, 18]],[date_create('1997-09-02'),date_create('1997-09-02'),date_create('1997-09-09')]),
// array(['byhour'=> [6, 18],'byminute'=>[6, 18]],[date_create('1997-09-02'),date_create('1997-09-02'),date_create('1997-09-09')]),
// array(['byhour'=>[6, 18],'bysecond'=>[6, 18]],[date_create('1997-09-02'),date_create('1997-09-02'),date_create('1997-09-09')]),
// array(['byminute'=>[6, 18],'bysecond'=>[6, 18]],[date_create('1997-09-02'),date_create('1997-09-02'),date_create('1997-09-02')]),
// array(['byhour'=>[6, 18],'byminute'=>[6, 18],'bysecond'=>[6, 18]],[date_create('1997-09-02'),date_create('1997-09-02'),date_create('1997-09-02')]),
// array(['byday'=>['TU', 'TH'],'byhour'=>[6, 18],'bysetpos'=>[3, -3]],[date_create('1997-09-02'),date_create('1997-09-04'),date_create('1997-09-09')])
);
}
/**
* @dataProvider weeklyRules
*/
public function testWeekly($rule, $occurrences)
{
$rule = new RRule(array_merge([
'FREQ' => 'WEEKLY',
'COUNT' => 3,
'DTSTART' => '1997-09-02'
], $rule));
$this->assertEquals($occurrences, $rule->getOccurrences());
}
public function dailyRules()
{
return array(
array([], [date_create('1997-09-02'),date_create('1997-09-03'),date_create('1997-09-04')]),
array(['interval'=>2],[date_create('1997-09-02'), date_create('1997-09-04'), date_create('1997-09-06')]),
array(['interval'=>92],[date_create('1997-09-02'), date_create('1997-12-03'), date_create('1998-03-05')]),
array(['bymonth'=>[1, 3]],[date_create('1998-01-01'), date_create('1998-01-02'), date_create('1998-01-03')]),
array(['bymonthday'=>[1, 3]],[date_create('1997-09-03'), date_create('1997-10-01'), date_create('1997-10-03')]),
array(['bymonth'=>[1, 3],'bymonthday'=>[5, 7]],[date_create('1998-01-05'), date_create('1998-01-07'), date_create('1998-03-05')]),
array(['byday'=>['TU', 'TH']],[date_create('1997-09-02'), date_create('1997-09-04'), date_create('1997-09-09')]),
array(['bymonth'=> [1, 3], 'byday'=> ['TU', 'TH']],[date_create('1998-01-01'), date_create('1998-01-06'), date_create('1998-01-08')]),
array(['bymonthday'=> [1, 3], 'byday'=>['TU', 'TH']],[date_create('1998-01-01'), date_create('1998-02-03'), date_create('1998-03-03')]),
array(['bymonth'=>[1, 3],'bymonthday'=>[1, 3],'byday'=>['TU', 'TH']],[date_create('1998-01-01'), date_create('1998-03-03'), date_create('2001-03-01')]),
// array(['count'=>4,'byyearday'=>[1, 100, 200, 365]],[date_create('1997-12-31'), date_create('1998-01-01'), date_create('1998-04-10'), date_create('1998-07-19')]),
// array(['count'=>4,'byyearday'=>[-365, -266, -166, -1]],[date_create('1997-12-31'), date_create('1998-01-01'), date_create('1998-04-10'), date_create('1998-07-19')]),
// array(['count'=>4, 'bymonth'=>[1, 7],'byyearday'=>[1, 100, 200, 365]],[date_create('1998-01-01'),date_create('1998-07-19'),date_create('1999-01-01'),date_create('1999-07-19')]),
// array(['count'=>4, 'bymonth' => [1, 7], 'byyearday' => [-365, -266, -166, -1]],[date_create('1998-01-01'), date_create('1998-07-19'), date_create('1999-01-01'), date_create('1999-07-19')]),
// array(['byweekno' => 20], [date_create('1998-05-11'), date_create('1998-05-12'), date_create('1998-05-13')]),
// array(['byweekno' => 1, 'byday' => 'MO'],[date_create('1997-12-29'),date_create('1999-01-04'),date_create('2000-01-03')]),
// array(['byweekno' => 52, 'byday' => 'SU'], [date_create('1997-12-28'), date_create('1998-12-27'), date_create('2000-01-02')]),
// array(['byweekno' => -1, 'byday' => 'SU'],[date_create('1997-12-28'),date_create('1999-01-03'),date_create('2000-01-02')]),
// array(['byweekno'=>53,'byday'=>'MO'],[date_create('1998-12-28'), date_create('2004-12-27'), date_create('2009-12-28')])
);
}
/**
* @dataProvider dailyRules
*/
public function testDaily($rule, $occurrences)
{
$rule = new RRule(array_merge([
'FREQ' => 'DAILY',
'COUNT' => 3,
'DTSTART' => '1997-09-02'
], $rule));
$this->assertEquals($occurrences, $rule->getOccurrences());
}
// def testDailyByHour(self):
// self.assertEqual(list(rrule(DAILY,
// byhour=(6, 18),
// [date_create('1997-09-02'),
// date_create('1997-09-03'),
// date_create('1997-09-03')])
// def testDailyByMinute(self):
// self.assertEqual(list(rrule(DAILY,
// byminute=(6, 18),
// [date_create('1997-09-02'),
// date_create('1997-09-02'),
// date_create('1997-09-03')])
// def testDailyBySecond(self):
// self.assertEqual(list(rrule(DAILY,
// bysecond=(6, 18),
// [date_create('1997-09-02'),
// date_create('1997-09-02'),
// date_create('1997-09-03')])
// def testDailyByHourAndMinute(self):
// self.assertEqual(list(rrule(DAILY,
// byhour=(6, 18),
// byminute=(6, 18),
// [date_create('1997-09-02'),
// date_create('1997-09-02'),
// date_create('1997-09-03')])
// def testDailyByHourAndSecond(self):
// self.assertEqual(list(rrule(DAILY,
// byhour=(6, 18),
// bysecond=(6, 18),
// [date_create('1997-09-02'),
// date_create('1997-09-02'),
// date_create('1997-09-03')])
// def testDailyByMinuteAndSecond(self):
// self.assertEqual(list(rrule(DAILY,
// byminute=(6, 18),
// bysecond=(6, 18),
// [date_create('1997-09-02'),
// date_create('1997-09-02'),
// date_create('1997-09-02')])
// def testDailyByHourAndMinuteAndSecond(self):
// self.assertEqual(list(rrule(DAILY,
// byhour=(6, 18),
// byminute=(6, 18),
// bysecond=(6, 18),
// [date_create('1997-09-02'),
// date_create('1997-09-02'),
// date_create('1997-09-02')])
// def testDailyBySetPos(self):
// self.assertEqual(list(rrule(DAILY,
// byhour=(6, 18),
// byminute=(15, 45),
// bysetpos=(3, -3),
// [date_create('1997-09-02'),
// date_create('1997-09-03'),
// date_create('1997-09-03')])
}