mirror of
https://github.com/rlanvin/php-rrule.git
synced 2024-11-28 05:24:10 +01:00
Initial commit
This commit is contained in:
commit
1535174243
3
.gitignore
vendored
Executable file
3
.gitignore
vendored
Executable file
@ -0,0 +1,3 @@
|
||||
*.sublime*
|
||||
vendor
|
||||
test.php
|
21
LICENSE
Executable file
21
LICENSE
Executable file
@ -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.
|
14
composer.json
Executable file
14
composer.json
Executable file
@ -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/"]
|
||||
}
|
||||
}
|
25
phpunit.xml.dist
Executable file
25
phpunit.xml.dist
Executable file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<phpunit backupGlobals="false"
|
||||
backupStaticAttributes="false"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="false"
|
||||
syntaxCheck="false"
|
||||
bootstrap="./tests/bootstrap.php"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Test Suite">
|
||||
<directory>./tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<filter>
|
||||
<whitelist processUncoveredFilesFromWhitelist="true">
|
||||
<directory>./src/</directory>
|
||||
</whitelist>
|
||||
</filter>
|
||||
</phpunit>
|
906
src/RRule.php
Executable file
906
src/RRule.php
Executable file
@ -0,0 +1,906 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Implementation of RRULE as defined by RFC 5545.
|
||||
*
|
||||
* Heavily based on dateutil/rrule.py
|
||||
*/
|
||||
|
||||
namespace RRule;
|
||||
|
||||
define(__NAMESPACE__.'\MAX_YEAR',date('Y', PHP_INT_MAX));
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
function not_empty($var)
|
||||
{
|
||||
return ! empty($var) || $var === 0 || $var === '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* closure/goog/math/math.js:modulo
|
||||
* Copyright 2006 The Closure Library Authors.
|
||||
*
|
||||
* The % operator in PHP returns the remainder of a / b, but differs from
|
||||
* some other languages in that the result will have the same sign as the
|
||||
* dividend. For example, -1 % 8 == -1, whereas in some other languages
|
||||
* (such as Python) the result would be 7. This function emulates the more
|
||||
* correct modulo behavior, which is useful for certain applications such as
|
||||
* calculating an offset index in a circular list.
|
||||
*
|
||||
* @param int $a The dividend.
|
||||
* @param int $b The divisor.
|
||||
*
|
||||
* @return int $a % $b where the result is between 0 and $b
|
||||
* (either 0 <= x < $b
|
||||
* or $b < x <= 0, depending on the sign of $b).
|
||||
*/
|
||||
function pymod($a, $b)
|
||||
{
|
||||
$x = $a % $b;
|
||||
|
||||
// If $x and $b differ in sign, add $b to wrap the result to the correct sign.
|
||||
return ($x * $b < 0) ? $x + $b : $x;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
function is_leap_year($year)
|
||||
{
|
||||
if ( $year % 4 !== 0 ) {
|
||||
return false;
|
||||
}
|
||||
if ( $year % 100 !== 0 ) {
|
||||
return true;
|
||||
}
|
||||
if ( $year % 400 !== 0 ) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main class.
|
||||
*/
|
||||
class RRule implements \Iterator, \ArrayAccess
|
||||
{
|
||||
// frequencies
|
||||
public static $frequencies = ['SECONDLY','MINUTELY','HOURLY','DAILY','WEEKLY','MONTHLY','YEARLY'];
|
||||
|
||||
const SECONDLY = 0;
|
||||
const MINUTELY = 1;
|
||||
const HOURLY = 2;
|
||||
const DAILY = 3;
|
||||
const WEEKLY = 4;
|
||||
const MONTHLY = 5;
|
||||
const YEARLY = 6;
|
||||
|
||||
// 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];
|
||||
|
||||
// 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
|
||||
);
|
||||
}
|
196
tests/RRuleTest.php
Executable file
196
tests/RRuleTest.php
Executable file
@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
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()
|
||||
{
|
||||
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());
|
||||
}
|
||||
}
|
3
tests/bootstrap.php
Executable file
3
tests/bootstrap.php
Executable file
@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
Loading…
Reference in New Issue
Block a user