1316 lines
36 KiB
PHP
1316 lines
36 KiB
PHP
<?php
|
|
|
|
/*
|
|
* Copyright 2013 Shaun Simmons
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*
|
|
* Based on rrule.js
|
|
* Copyright 2010, Jakub Roztocil and Lars Schoning
|
|
* https://github.com/jkbr/rrule/blob/master/LICENCE
|
|
*
|
|
* Based on python-dateutil - Extensions to the standard Python datetime module.
|
|
* Copyright (c) 2003-2011 - Gustavo Niemeyer <gustavo@niemeyer.net>
|
|
* Copyright (c) 2012 - Tomi Pieviläinen <tomi.pievilainen@iki.fi>
|
|
*/
|
|
|
|
namespace Recurr;
|
|
|
|
use Recurr\Exception\InvalidArgument;
|
|
use Recurr\Exception\InvalidRRule;
|
|
use Recurr\Exception\InvalidWeekday;
|
|
|
|
/**
|
|
* This class is responsible for providing a programmatic way of building,
|
|
* parsing, and handling RRULEs.
|
|
*
|
|
* http://www.ietf.org/rfc/rfc2445.txt
|
|
*
|
|
* Information, not contained in the built/parsed RRULE, necessary to determine
|
|
* the various recurrence instance start time and dates are derived from the
|
|
* DTSTART property (default: \DateTime()).
|
|
*
|
|
* For example, "FREQ=YEARLY;BYMONTH=1" doesn't specify a specific day within
|
|
* the month or a time. This information would be the same as what is specified
|
|
* for DTSTART.
|
|
*
|
|
*
|
|
* BYxxx rule parts modify the recurrence in some manner. BYxxx rule parts for
|
|
* a period of time which is the same or greater than the frequency generally
|
|
* reduce or limit the number of occurrences of the recurrence generated.
|
|
*
|
|
* For example, "FREQ=DAILY;BYMONTH=1" reduces the number of recurrence
|
|
* instances from all days (if BYMONTH tag is not present) to all days in
|
|
* January.
|
|
*
|
|
* BYxxx rule parts for a period of time less than the frequency generally
|
|
* increase or expand the number of occurrences of the recurrence.
|
|
*
|
|
* For example, "FREQ=YEARLY;BYMONTH=1,2" increases the number of days within
|
|
* the yearly recurrence set from 1 (if BYMONTH tag is not present) to 2.
|
|
*
|
|
* 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.
|
|
*
|
|
* Here is an example of evaluating multiple BYxxx rule parts.
|
|
*
|
|
* FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;BYMINUTE=30
|
|
*
|
|
* First, the "INTERVAL=2" would be applied to "FREQ=YEARLY" to arrive at
|
|
* "every other year".
|
|
* Then, "BYMONTH=1" would be applied to arrive at "every January, every
|
|
* other year".
|
|
* Then, "BYDAY=SU" would be applied to arrive at "every Sunday in January,
|
|
* every other year".
|
|
* Then, "BYHOUR=8,9" would be applied to arrive at "every Sunday in January
|
|
* at 8 AM and 9 AM, every other year".
|
|
* Then, "BYMINUTE=30" would be applied to arrive at "every Sunday in January
|
|
* at 8:30 AM and 9:30 AM, every other year".
|
|
* Then, lacking information from RRULE, the second is derived from DTSTART, to
|
|
* end up in "every Sunday in January at 8:30:00 AM and 9:30:00 AM, every
|
|
* other year". Similarly, if the BYMINUTE, BYHOUR, BYDAY, BYMONTHDAY or
|
|
* BYMONTH rule part were missing, the appropriate minute, hour, day or month
|
|
* would have been retrieved from the "DTSTART" property.
|
|
*
|
|
* Example: The following is a rule which specifies 10 meetings which occur
|
|
* every other day:
|
|
*
|
|
* FREQ=DAILY;COUNT=10;INTERVAL=2
|
|
*
|
|
* @package Recurr
|
|
* @author Shaun Simmons <shaun@envysphere.com>
|
|
*/
|
|
class Rule
|
|
{
|
|
const TZ_FIXED = 'fixed';
|
|
const TZ_FLOAT = 'floating';
|
|
|
|
public static $freqs = array(
|
|
'YEARLY' => 0,
|
|
'MONTHLY' => 1,
|
|
'WEEKLY' => 2,
|
|
'DAILY' => 3,
|
|
'HOURLY' => 4,
|
|
'MINUTELY' => 5,
|
|
'SECONDLY' => 6,
|
|
);
|
|
|
|
/** @var string */
|
|
protected $timezone;
|
|
|
|
/** @var \DateTimeInterface|null */
|
|
protected $startDate;
|
|
|
|
/** @var \DateTimeInterface|null */
|
|
protected $endDate;
|
|
|
|
/** @var bool */
|
|
protected $isStartDateFromDtstart = false;
|
|
|
|
/** @var string */
|
|
protected $freq;
|
|
|
|
/** @var int */
|
|
protected $interval = 1;
|
|
|
|
/** @var bool */
|
|
protected $isExplicitInterval = false;
|
|
|
|
/** @var \DateTimeInterface|null */
|
|
protected $until;
|
|
|
|
/** @var int|null */
|
|
protected $count;
|
|
|
|
/** @var array */
|
|
protected $bySecond;
|
|
|
|
/** @var array */
|
|
protected $byMinute;
|
|
|
|
/** @var array */
|
|
protected $byHour;
|
|
|
|
/** @var array */
|
|
protected $byDay;
|
|
|
|
/** @var array */
|
|
protected $byMonthDay;
|
|
|
|
/** @var array */
|
|
protected $byYearDay;
|
|
|
|
/** @var array */
|
|
protected $byWeekNumber;
|
|
|
|
/** @var array */
|
|
protected $byMonth;
|
|
|
|
/** @var string */
|
|
protected $weekStart = 'MO';
|
|
protected $weekStartDefined = false;
|
|
|
|
/** @var array */
|
|
protected $days = array(
|
|
'MO' => 0,
|
|
'TU' => 1,
|
|
'WE' => 2,
|
|
'TH' => 3,
|
|
'FR' => 4,
|
|
'SA' => 5,
|
|
'SU' => 6
|
|
);
|
|
|
|
/** @var int[] */
|
|
protected $bySetPosition;
|
|
|
|
/** @var array */
|
|
protected $rDates = array();
|
|
|
|
/** @var array */
|
|
protected $exDates = array();
|
|
|
|
/**
|
|
* Construct a new Rule.
|
|
*
|
|
* @param string $rrule RRULE string
|
|
* @param string|\DateTimeInterface|null $startDate
|
|
* @param string|\DateTimeInterface|null $endDate
|
|
* @param string $timezone
|
|
*
|
|
* @throws InvalidRRule
|
|
*/
|
|
public function __construct($rrule = null, $startDate = null, $endDate = null, $timezone = null)
|
|
{
|
|
if (empty($timezone)) {
|
|
if ($startDate instanceof \DateTimeInterface) {
|
|
$timezone = $startDate->getTimezone()->getName();
|
|
} else {
|
|
$timezone = date_default_timezone_get();
|
|
}
|
|
}
|
|
$this->setTimezone($timezone);
|
|
|
|
if ($startDate !== null && !$startDate instanceof \DateTimeInterface) {
|
|
$startDate = new \DateTime($startDate, new \DateTimeZone($timezone));
|
|
}
|
|
|
|
$this->setStartDate($startDate);
|
|
|
|
if ($endDate !== null && !$endDate instanceof \DateTimeInterface) {
|
|
$endDate = new \DateTime($endDate, new \DateTimeZone($timezone));
|
|
}
|
|
|
|
$this->setEndDate($endDate);
|
|
|
|
if (is_array($rrule)) {
|
|
$this->loadFromArray($rrule);
|
|
} else if (!empty($rrule)) {
|
|
$this->loadFromString($rrule);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a Rule object based on a RRULE string.
|
|
*
|
|
* @param string $rrule RRULE string
|
|
* @param string|\DateTimeInterface $startDate
|
|
* @param \DateTimeInterface|null $endDate
|
|
* @param string $timezone
|
|
*
|
|
* @return Rule
|
|
* @throws InvalidRRule
|
|
*/
|
|
public static function createFromString($rrule, $startDate = null, $endDate = null, $timezone = null)
|
|
{
|
|
$rule = new static($rrule, $startDate, $endDate, $timezone);
|
|
|
|
return $rule;
|
|
}
|
|
|
|
/**
|
|
* Create a Rule object based on a RRULE array.
|
|
*
|
|
* @param array $rrule RRULE array
|
|
* @param string|\DateTimeInterface $startDate
|
|
* @param \DateTimeInterface|null $endDate
|
|
* @param string $timezone
|
|
*
|
|
* @return Rule
|
|
* @throws InvalidRRule
|
|
*/
|
|
public static function createFromArray($rrule, $startDate = null, $endDate = null, $timezone = null)
|
|
{
|
|
$rule = new static($rrule, $startDate, $endDate, $timezone);
|
|
|
|
return $rule;
|
|
}
|
|
|
|
/**
|
|
* Populate the object based on a RRULE string.
|
|
*
|
|
* @param string $rrule RRULE string
|
|
*
|
|
* @return Rule
|
|
* @throws InvalidRRule
|
|
*/
|
|
public function loadFromString($rrule)
|
|
{
|
|
$rrule = strtoupper($rrule);
|
|
$rrule = trim($rrule, ';');
|
|
$rrule = trim($rrule, "\n");
|
|
$rows = explode("\n", $rrule);
|
|
|
|
$parts = array();
|
|
|
|
foreach ($rows as $rruleForRow) {
|
|
$parts = array_merge($parts, $this->parseString($rruleForRow));
|
|
}
|
|
|
|
return $this->loadFromArray($parts);
|
|
}
|
|
|
|
/**
|
|
* Parse string for parts
|
|
*
|
|
* @param string $rrule
|
|
*
|
|
* @return array
|
|
*
|
|
* @throws InvalidRRule
|
|
*/
|
|
public function parseString($rrule)
|
|
{
|
|
if (strpos($rrule, 'DTSTART:') === 0) {
|
|
$pieces = explode(':', $rrule);
|
|
|
|
if (count($pieces) !== 2) {
|
|
throw new InvalidRRule('DSTART is not valid');
|
|
}
|
|
|
|
return array('DTSTART' => $pieces[1]);
|
|
}
|
|
|
|
if (strpos($rrule, 'RRULE:') === 0) {
|
|
$rrule = str_replace('RRULE:', '', $rrule);
|
|
}
|
|
|
|
$pieces = explode(';', $rrule);
|
|
$parts = array();
|
|
|
|
if (!count($pieces)) {
|
|
throw new InvalidRRule('RRULE is empty');
|
|
}
|
|
|
|
// Split each piece of the RRULE in to KEY=>VAL
|
|
foreach ($pieces as $piece) {
|
|
if (false === strpos($piece, '=')) {
|
|
continue;
|
|
}
|
|
|
|
list($key, $val) = explode('=', $piece);
|
|
$parts[$key] = $val;
|
|
}
|
|
|
|
return $parts;
|
|
}
|
|
|
|
/**
|
|
* Populate the object based on a RRULE array.
|
|
*
|
|
* @param array
|
|
*
|
|
* @return Rule
|
|
* @throws InvalidRRule
|
|
*/
|
|
public function loadFromArray($parts)
|
|
{
|
|
// FREQ is required
|
|
if (!isset($parts['FREQ'])) {
|
|
throw new InvalidRRule('FREQ is required');
|
|
} else {
|
|
if (!in_array($parts['FREQ'], array_keys(self::$freqs))) {
|
|
throw new InvalidRRule('FREQ is invalid');
|
|
}
|
|
|
|
$this->setFreq(self::$freqs[$parts['FREQ']]);
|
|
}
|
|
|
|
// DTSTART
|
|
if (isset($parts['DTSTART'])) {
|
|
$this->isStartDateFromDtstart = true;
|
|
$date = new \DateTime($parts['DTSTART']);
|
|
$date = $date->setTimezone(new \DateTimeZone($this->getTimezone()));
|
|
$this->setStartDate($date);
|
|
}
|
|
|
|
// DTEND
|
|
if (isset($parts['DTEND'])) {
|
|
$date = new \DateTime($parts['DTEND']);
|
|
$date = $date->setTimezone(new \DateTimeZone($this->getTimezone()));
|
|
$this->setEndDate($date);
|
|
}
|
|
|
|
// UNTIL or COUNT
|
|
if (isset($parts['UNTIL']) && isset($parts['COUNT'])) {
|
|
throw new InvalidRRule('UNTIL and COUNT must not exist together in the same RRULE');
|
|
} elseif (isset($parts['UNTIL'])) {
|
|
$date = new \DateTime($parts['UNTIL']);
|
|
$date = $date->setTimezone(new \DateTimeZone($this->getTimezone()));
|
|
$this->setUntil($date);
|
|
} elseif (isset($parts['COUNT'])) {
|
|
$this->setCount($parts['COUNT']);
|
|
}
|
|
|
|
// INTERVAL
|
|
if (isset($parts['INTERVAL'])) {
|
|
$this->setInterval($parts['INTERVAL']);
|
|
}
|
|
|
|
// BYSECOND
|
|
if (isset($parts['BYSECOND'])) {
|
|
$this->setBySecond(explode(',', $parts['BYSECOND']));
|
|
}
|
|
|
|
// BYMINUTE
|
|
if (isset($parts['BYMINUTE'])) {
|
|
$this->setByMinute(explode(',', $parts['BYMINUTE']));
|
|
}
|
|
|
|
// BYHOUR
|
|
if (isset($parts['BYHOUR'])) {
|
|
$this->setByHour(explode(',', $parts['BYHOUR']));
|
|
}
|
|
|
|
// BYDAY
|
|
if (isset($parts['BYDAY'])) {
|
|
$this->setByDay(explode(',', $parts['BYDAY']));
|
|
}
|
|
|
|
// BYMONTHDAY
|
|
if (isset($parts['BYMONTHDAY'])) {
|
|
$this->setByMonthDay(explode(',', $parts['BYMONTHDAY']));
|
|
}
|
|
|
|
// BYYEARDAY
|
|
if (isset($parts['BYYEARDAY'])) {
|
|
$this->setByYearDay(explode(',', $parts['BYYEARDAY']));
|
|
}
|
|
|
|
// BYWEEKNO
|
|
if (isset($parts['BYWEEKNO'])) {
|
|
$this->setByWeekNumber(explode(',', $parts['BYWEEKNO']));
|
|
}
|
|
|
|
// BYMONTH
|
|
if (isset($parts['BYMONTH'])) {
|
|
$this->setByMonth(explode(',', $parts['BYMONTH']));
|
|
}
|
|
|
|
// BYSETPOS
|
|
if (isset($parts['BYSETPOS'])) {
|
|
$this->setBySetPosition(explode(',', $parts['BYSETPOS']));
|
|
}
|
|
|
|
// WKST
|
|
if (isset($parts['WKST'])) {
|
|
$this->setWeekStart($parts['WKST']);
|
|
}
|
|
|
|
// RDATE
|
|
if (isset($parts['RDATE'])) {
|
|
$this->setRDates(explode(',', $parts['RDATE']));
|
|
}
|
|
|
|
// EXDATE
|
|
if (isset($parts['EXDATE'])) {
|
|
$this->setExDates(explode(',', $parts['EXDATE']));
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get the RRULE as a string
|
|
*
|
|
* @param string $timezoneType
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getString($timezoneType=self::TZ_FLOAT)
|
|
{
|
|
$format = 'Ymd\THis';
|
|
|
|
$parts = array();
|
|
|
|
// FREQ
|
|
$parts[] = 'FREQ='.$this->getFreqAsText();
|
|
|
|
// UNTIL or COUNT
|
|
$until = $this->getUntil();
|
|
$count = $this->getCount();
|
|
if (!empty($until)) {
|
|
if ($timezoneType === self::TZ_FIXED) {
|
|
$u = clone $until;
|
|
$u = $u->setTimezone(new \DateTimeZone('UTC'));
|
|
$parts[] = 'UNTIL='.$u->format($format.'\Z');
|
|
} else {
|
|
$parts[] = 'UNTIL='.$until->format($format);
|
|
}
|
|
} elseif (!empty($count)) {
|
|
$parts[] = 'COUNT='.$count;
|
|
}
|
|
|
|
// DTSTART
|
|
if ($this->isStartDateFromDtstart) {
|
|
if ($timezoneType === self::TZ_FIXED) {
|
|
$d = $this->getStartDate();
|
|
$tzid = $d->getTimezone()->getName();
|
|
$date = $d->format($format);
|
|
$parts[] = "DTSTART;TZID=$tzid:$date";
|
|
} else {
|
|
$parts[] = 'DTSTART='.$this->getStartDate()->format($format);
|
|
}
|
|
}
|
|
|
|
// DTEND
|
|
if ($this->endDate instanceof \DateTime) {
|
|
if ($timezoneType === self::TZ_FIXED) {
|
|
$d = $this->getEndDate();
|
|
$tzid = $d->getTimezone()->getName();
|
|
$date = $d->format($format);
|
|
|
|
$parts[] = "DTEND;TZID=$tzid:$date";
|
|
} else {
|
|
$parts[] = 'DTEND='.$this->getEndDate()->format($format);
|
|
}
|
|
}
|
|
|
|
// INTERVAL
|
|
$interval = $this->getInterval();
|
|
if ($this->isExplicitInterval && !empty($interval)) {
|
|
$parts[] = 'INTERVAL='.$interval;
|
|
}
|
|
|
|
// BYSECOND
|
|
$bySecond = $this->getBySecond();
|
|
if (!empty($bySecond)) {
|
|
$parts[] = 'BYSECOND='.implode(',', $bySecond);
|
|
}
|
|
|
|
// BYMINUTE
|
|
$byMinute = $this->getByMinute();
|
|
if (!empty($byMinute)) {
|
|
$parts[] = 'BYMINUTE='.implode(',', $byMinute);
|
|
}
|
|
|
|
// BYHOUR
|
|
$byHour = $this->getByHour();
|
|
if (!empty($byHour)) {
|
|
$parts[] = 'BYHOUR='.implode(',', $byHour);
|
|
}
|
|
|
|
// BYDAY
|
|
$byDay = $this->getByDay();
|
|
if (!empty($byDay)) {
|
|
$parts[] = 'BYDAY='.implode(',', $byDay);
|
|
}
|
|
|
|
// BYMONTHDAY
|
|
$byMonthDay = $this->getByMonthDay();
|
|
if (!empty($byMonthDay)) {
|
|
$parts[] = 'BYMONTHDAY='.implode(',', $byMonthDay);
|
|
}
|
|
|
|
// BYYEARDAY
|
|
$byYearDay = $this->getByYearDay();
|
|
if (!empty($byYearDay)) {
|
|
$parts[] = 'BYYEARDAY='.implode(',', $byYearDay);
|
|
}
|
|
|
|
// BYWEEKNO
|
|
$byWeekNumber = $this->getByWeekNumber();
|
|
if (!empty($byWeekNumber)) {
|
|
$parts[] = 'BYWEEKNO='.implode(',', $byWeekNumber);
|
|
}
|
|
|
|
// BYMONTH
|
|
$byMonth = $this->getByMonth();
|
|
if (!empty($byMonth)) {
|
|
$parts[] = 'BYMONTH='.implode(',', $byMonth);
|
|
}
|
|
|
|
// BYSETPOS
|
|
$bySetPosition = $this->getBySetPosition();
|
|
if (!empty($bySetPosition)) {
|
|
$parts[] = 'BYSETPOS='.implode(',', $bySetPosition);
|
|
}
|
|
|
|
// WKST
|
|
$weekStart = $this->getWeekStart();
|
|
if ($this->weekStartDefined && !empty($weekStart)) {
|
|
$parts[] = 'WKST='.$weekStart;
|
|
}
|
|
|
|
// RDATE
|
|
$rDates = $this->getRDates();
|
|
if (!empty($rDates)) {
|
|
foreach ($rDates as $key => $inclusion) {
|
|
$format = 'Ymd';
|
|
if ($inclusion->hasTime) {
|
|
$format .= '\THis';
|
|
if ($inclusion->isUtcExplicit) {
|
|
$format .= '\Z';
|
|
}
|
|
}
|
|
$rDates[$key] = $inclusion->date->format($format);
|
|
}
|
|
$parts[] = 'RDATE='.implode(',', $rDates);
|
|
}
|
|
|
|
// EXDATE
|
|
$exDates = $this->getExDates();
|
|
if (!empty($exDates)) {
|
|
foreach ($exDates as $key => $exclusion) {
|
|
$format = 'Ymd';
|
|
if ($exclusion->hasTime) {
|
|
$format .= '\THis';
|
|
if ($exclusion->isUtcExplicit) {
|
|
$format .= '\Z';
|
|
}
|
|
}
|
|
$exDates[$key] = $exclusion->date->format($format);
|
|
}
|
|
$parts[] = 'EXDATE='.implode(',', $exDates);
|
|
}
|
|
|
|
return implode(';', $parts);
|
|
}
|
|
|
|
/**
|
|
* @param string $timezone
|
|
*
|
|
* @see http://www.php.net/manual/en/timezones.php
|
|
* @return $this
|
|
*/
|
|
public function setTimezone($timezone)
|
|
{
|
|
$this->timezone = $timezone;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get timezone to use for \DateTimeInterface objects that are UTC.
|
|
*
|
|
* @return null|string
|
|
*/
|
|
public function getTimezone()
|
|
{
|
|
return $this->timezone;
|
|
}
|
|
|
|
/**
|
|
* This date specifies the first instance in the recurrence set.
|
|
*
|
|
* @param \DateTimeInterface|null $startDate Date of the first instance in the recurrence
|
|
* @param bool|null $includeInString If true, include as DTSTART when calling getString()
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setStartDate($startDate, $includeInString = null)
|
|
{
|
|
$this->startDate = $startDate;
|
|
|
|
if ($includeInString !== null) {
|
|
$this->isStartDateFromDtstart = (bool) $includeInString;
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @return \DateTimeInterface
|
|
*/
|
|
public function getStartDate()
|
|
{
|
|
return $this->startDate;
|
|
}
|
|
|
|
/**
|
|
* This date specifies the last possible instance in the recurrence set.
|
|
*
|
|
* @param \DateTimeInterface|null $endDate Date of the last possible instance in the recurrence
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setEndDate($endDate)
|
|
{
|
|
$this->endDate = $endDate;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @return \DateTimeInterface|null
|
|
*/
|
|
public function getEndDate()
|
|
{
|
|
return $this->endDate;
|
|
}
|
|
|
|
/**
|
|
* Identifies the type of recurrence rule.
|
|
*
|
|
* May be one of:
|
|
* - Frequency::SECONDLY to specify repeating events based on an
|
|
* interval of a second or more.
|
|
* - Frequency::MINUTELY to specify repeating events based on an
|
|
* interval of a minute or more.
|
|
* - Frequency::HOURLY to specify repeating events based on an
|
|
* interval of an hour or more.
|
|
* - Frequency::DAILY to specify repeating events based on an
|
|
* interval of a day or more.
|
|
* - Frequency::WEEKLY to specify repeating events based on an
|
|
* interval of a week or more.
|
|
* - Frequency::MONTHLY to specify repeating events based on an
|
|
* interval of a month or more.
|
|
* - Frequency::YEAR to specify repeating events based on an
|
|
* interval of a year or more.
|
|
*
|
|
* @param string|int $freq Frequency of recurrence.
|
|
*
|
|
* @return $this
|
|
* @throws Exception\InvalidArgument
|
|
*/
|
|
public function setFreq($freq)
|
|
{
|
|
if (is_string($freq)) {
|
|
if (!array_key_exists($freq, self::$freqs)) {
|
|
throw new InvalidArgument('Frequency must comply with RFC 2445.');
|
|
} else {
|
|
$freq = self::$freqs[$freq];
|
|
}
|
|
}
|
|
|
|
if (is_int($freq) && ($freq < 0 || $freq > 6)) {
|
|
throw new InvalidArgument('Frequency integer must be between 0 and 6 Use the class constants.');
|
|
}
|
|
|
|
$this->freq = $freq;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get the type of recurrence rule (as integer).
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getFreq()
|
|
{
|
|
return $this->freq;
|
|
}
|
|
|
|
/**
|
|
* Get the type of recurrence rule (as text).
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getFreqAsText()
|
|
{
|
|
return array_search($this->getFreq(), self::$freqs);
|
|
}
|
|
|
|
/**
|
|
* The interval represents how often the recurrence rule repeats.
|
|
*
|
|
* The default value is "1", meaning every second for a SECONDLY rule,
|
|
* or every minute for a MINUTELY rule, every hour for an HOURLY rule,
|
|
* every day for a DAILY rule, every week for a WEEKLY rule, every month
|
|
* for a MONTHLY rule and every year for a YEARLY rule.
|
|
*
|
|
* @param int $interval Positive integer that represents how often the
|
|
* recurrence rule repeats.
|
|
*
|
|
* @return $this
|
|
* @throws Exception\InvalidArgument
|
|
*/
|
|
public function setInterval($interval)
|
|
{
|
|
$interval = (int) $interval;
|
|
|
|
if ($interval < 1) {
|
|
throw new InvalidArgument('Interval must be a positive integer');
|
|
}
|
|
|
|
$this->interval = $interval;
|
|
$this->isExplicitInterval = true;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get the interval that represents how often the recurrence rule repeats.
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getInterval()
|
|
{
|
|
return $this->interval;
|
|
}
|
|
|
|
/**
|
|
* Define a \DateTimeInterface value which bounds the recurrence rule in an
|
|
* inclusive manner. If the value specified is synchronized with the
|
|
* specified recurrence, this DateTime becomes the last instance of the
|
|
* recurrence. If not present, and a COUNT is also not present, the RRULE
|
|
* is considered to repeat forever.
|
|
*
|
|
* Either UNTIL or COUNT may be specified, but UNTIL and COUNT MUST NOT
|
|
* both be specified.
|
|
*
|
|
* @param \DateTimeInterface $until The upper bound of the recurrence.
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setUntil(\DateTimeInterface $until)
|
|
{
|
|
$this->until = $until;
|
|
$this->count = null;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get the \DateTimeInterface that the recurrence lasts until.
|
|
*
|
|
* @return \DateTimeInterface|null
|
|
*/
|
|
public function getUntil()
|
|
{
|
|
$date = $this->until;
|
|
|
|
if ($date instanceof \DateTime
|
|
&& $date->getTimezone()->getName() == 'UTC'
|
|
&& $this->getTimezone() != 'UTC'
|
|
) {
|
|
$timestamp = $date->getTimestamp();
|
|
$date = $date->setTimezone(new \DateTimeZone($this->getTimezone()));
|
|
$date = $date->setTimestamp($timestamp);
|
|
}
|
|
|
|
return $date;
|
|
}
|
|
|
|
/**
|
|
* The count defines the number of occurrences at which to range-bound the
|
|
* recurrence. The DTSTART counts as the first occurrence.
|
|
*
|
|
* Either COUNT or UNTIL may be specified, but COUNT and UNTIL MUST NOT
|
|
* both be specified.
|
|
*
|
|
* @param int $count Number of occurrences
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setCount($count)
|
|
{
|
|
$this->count = (int) $count;
|
|
$this->until = null;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get the number of occurrences at which the recurrence is range-bound.
|
|
*
|
|
* @return int|null
|
|
*/
|
|
public function getCount()
|
|
{
|
|
return $this->count;
|
|
}
|
|
|
|
/**
|
|
* This rule specifies an array of seconds within a minute.
|
|
*
|
|
* Valid values are 0 to 59.
|
|
*
|
|
* @param array $bySecond Array of seconds within a minute
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setBySecond(array $bySecond)
|
|
{
|
|
$this->bySecond = $bySecond;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get an array of seconds within a minute.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getBySecond()
|
|
{
|
|
return $this->bySecond;
|
|
}
|
|
|
|
/**
|
|
* This rule specifies an array of minutes within an hour.
|
|
*
|
|
* Valid values are 0 to 59.
|
|
*
|
|
* @param array $byMinute Array of minutes within an hour
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setByMinute(array $byMinute)
|
|
{
|
|
$this->byMinute = $byMinute;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get an array of minutes within an hour.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getByMinute()
|
|
{
|
|
return $this->byMinute;
|
|
}
|
|
|
|
/**
|
|
* This rule specifies an array of hours of the day.
|
|
*
|
|
* Valid values are 0 to 23.
|
|
*
|
|
* @param array $byHour Array of hours of the day
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setByHour(array $byHour)
|
|
{
|
|
$this->byHour = $byHour;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get an array of hours of the day.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getByHour()
|
|
{
|
|
return $this->byHour;
|
|
}
|
|
|
|
/**
|
|
* This rule specifies an array of days of the week;
|
|
*
|
|
* MO indicates Monday; TU indicates Tuesday; WE indicates Wednesday;
|
|
* TH indicates Thursday; FR indicates Friday; SA indicates Saturday;
|
|
* SU indicates Sunday.
|
|
*
|
|
* Each BYDAY value can also be preceded by a positive (+n) or negative
|
|
* (-n) integer. If present, this indicates the nth occurrence of the
|
|
* specific day within the MONTHLY or YEARLY RRULE. For example, within
|
|
* a MONTHLY rule, +1MO (or simply 1MO) represents the first Monday
|
|
* within the month, whereas -1MO represents the last Monday of the
|
|
* month. If an integer modifier is not present, it means all days of
|
|
* this type within the specified frequency. For example, within a
|
|
* MONTHLY rule, MO represents all Mondays within the month.
|
|
*
|
|
* -------------------------------------------
|
|
* DO NOT MIX DAYS AND DAYS WITH MODIFIERS.
|
|
* This is not supported.
|
|
* -------------------------------------------
|
|
*
|
|
* @param array $byDay Array of days of the week
|
|
*
|
|
* @return $this
|
|
* @throws InvalidRRule
|
|
*/
|
|
public function setByDay(array $byDay)
|
|
{
|
|
if ($this->getFreq() > static::$freqs['MONTHLY'] && preg_match('/\d/', implode(',', $byDay))) {
|
|
throw new InvalidRRule('BYDAY only supports MONTHLY and YEARLY frequencies');
|
|
}
|
|
if (count($byDay) === 0 || $byDay === array('')) {
|
|
throw new InvalidRRule('BYDAY must be set to at least one day');
|
|
}
|
|
|
|
$this->byDay = $byDay;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get an array of days of the week (SU, MO, TU, ..)
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getByDay()
|
|
{
|
|
return $this->byDay;
|
|
}
|
|
|
|
/**
|
|
* Get an array of Weekdays
|
|
*
|
|
* @return array of Weekdays
|
|
* @throws InvalidWeekday
|
|
*/
|
|
public function getByDayTransformedToWeekdays()
|
|
{
|
|
$byDay = $this->getByDay();
|
|
|
|
if (null === $byDay || !count($byDay)) {
|
|
return array();
|
|
}
|
|
|
|
foreach ($byDay as $idx => $day) {
|
|
if (strlen($day) === 2) {
|
|
$byDay[$idx] = new Weekday($day, null);
|
|
} else {
|
|
preg_match('/^([+-]?[0-9]+)([A-Z]{2})$/', $day, $dayParts);
|
|
$byDay[$idx] = new Weekday($dayParts[2], $dayParts[1]);
|
|
}
|
|
}
|
|
|
|
return $byDay;
|
|
}
|
|
|
|
/**
|
|
* This rule specifies an array 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.
|
|
*
|
|
* @param array $byMonthDay Array of days of the month from -31 to 31
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setByMonthDay(array $byMonthDay)
|
|
{
|
|
$this->byMonthDay = $byMonthDay;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get an array of days of the month.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getByMonthDay()
|
|
{
|
|
return $this->byMonthDay;
|
|
}
|
|
|
|
/**
|
|
* This rule specifies an array of days of the year.
|
|
* Valid values are 1 to 366 or -366 to -1.
|
|
*
|
|
* For example, -1 represents the last day of the year (December 31st) and
|
|
* -306 represents the 306th to the last day of the year (March 1st).
|
|
*
|
|
* @param array $byYearDay Array of days of the year from -1 to 306
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setByYearDay(array $byYearDay)
|
|
{
|
|
$this->byYearDay = $byYearDay;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get an array of days of the year.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getByYearDay()
|
|
{
|
|
return $this->byYearDay;
|
|
}
|
|
|
|
/**
|
|
* This rule specifies an array of ordinals specifying weeks of the year.
|
|
* Valid values are 1 to 53 or -53 to -1.
|
|
*
|
|
* This corresponds to weeks according to week numbering as defined in
|
|
* [ISO 8601]. A week is defined as a seven day period, starting on the day
|
|
* of the week defined to be the week start (see setWeekStart). Week number
|
|
* one of the calendar year is the first week which contains at least four
|
|
* days in that calendar year. This rule is only valid for YEARLY rules.
|
|
*
|
|
* For example, 3 represents the third week of the year.
|
|
*
|
|
* Note: Assuming a Monday week start, week 53 can only occur when
|
|
* Thursday is January 1 or if it is a leap year and Wednesday is January 1.
|
|
*
|
|
* @param array $byWeekNumber Array of ordinals specifying weeks of the year.
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setByWeekNumber(array $byWeekNumber)
|
|
{
|
|
$this->byWeekNumber = $byWeekNumber;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get an array of ordinals specifying weeks of the year.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getByWeekNumber()
|
|
{
|
|
return $this->byWeekNumber;
|
|
}
|
|
|
|
/**
|
|
* This rule specifies an array of months of the year.
|
|
*
|
|
* Valid values are 1 to 12.
|
|
*
|
|
* @param array $byMonth Array of months of the year from 1 to 12
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setByMonth(array $byMonth)
|
|
{
|
|
$this->byMonth = $byMonth;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get an array of months of the year.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getByMonth()
|
|
{
|
|
return $this->byMonth;
|
|
}
|
|
|
|
public function hasByMonth()
|
|
{
|
|
$val = $this->getByMonth();
|
|
|
|
return ! empty($val);
|
|
}
|
|
|
|
/**
|
|
* This rule specifies the day on which the workweek starts.
|
|
*
|
|
* Valid values are MO, TU, WE, TH, FR, SA and SU.
|
|
*
|
|
* This is significant when a WEEKLY RRULE has an interval greater than 1,
|
|
* and a BYDAY rule part is specified.
|
|
*
|
|
* This is also significant when in a YEARLY RRULE when a BYWEEKNO rule
|
|
* is specified. The default value is MO.
|
|
*
|
|
* @param string $weekStart The day on which the workweek starts.
|
|
*
|
|
* @return $this
|
|
* @throws Exception\InvalidArgument
|
|
*/
|
|
public function setWeekStart($weekStart)
|
|
{
|
|
$weekStart = strtoupper($weekStart);
|
|
|
|
if (!in_array($weekStart, array('MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'))) {
|
|
throw new InvalidArgument('Week Start must be one of MO, TU, WE, TH, FR, SA, SU');
|
|
}
|
|
|
|
$this->weekStart = $weekStart;
|
|
$this->weekStartDefined = true;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get the day on which the workweek starts.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getWeekStart()
|
|
{
|
|
return $this->weekStart;
|
|
}
|
|
|
|
/**
|
|
* Get the day on which the workweek starts, as an integer from 0-6,
|
|
* 0 being Monday and 6 being Sunday.
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getWeekStartAsNum()
|
|
{
|
|
$weekStart = $this->getWeekStart();
|
|
|
|
return $this->days[$weekStart];
|
|
}
|
|
|
|
/**
|
|
* This rule specifies an array of values which corresponds to the nth
|
|
* occurrence within the set of events specified by the rule. Valid values
|
|
* are 1 to 366 or -366 to -1. It MUST only be used in conjunction with
|
|
* another BYxxx rule part.
|
|
*
|
|
* For example "the last work day of the month" could be represented as:
|
|
* RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1
|
|
*
|
|
* Each BYSETPOS value can include a positive or negative integer.
|
|
* If present, this indicates the nth occurrence of the specific occurrence
|
|
* within the set of events specified by the rule.
|
|
*
|
|
* @param array $bySetPosition Array of values which corresponds to the nth
|
|
* occurrence within the set of events specified by the rule.
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setBySetPosition($bySetPosition)
|
|
{
|
|
$this->bySetPosition = $bySetPosition;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get the array of values which corresponds to the nth occurrence within
|
|
* the set of events specified by the rule.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getBySetPosition()
|
|
{
|
|
return $this->bySetPosition;
|
|
}
|
|
|
|
/**
|
|
* This rule specifies an array of dates that will be
|
|
* included in a recurrence set.
|
|
*
|
|
* @param string[]|DateInclusion[] $rDates Array of dates that will be
|
|
* included in the recurrence set.
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setRDates(array $rDates)
|
|
{
|
|
$timezone = new \DateTimeZone($this->getTimezone());
|
|
|
|
foreach ($rDates as $key => $val) {
|
|
if ($val instanceof DateInclusion) {
|
|
$val->date = $this->convertZtoUtc($val->date);
|
|
} else {
|
|
$date = new \DateTime($val, $timezone);
|
|
$rDates[$key] = new DateInclusion(
|
|
$this->convertZtoUtc($date),
|
|
strpos($val, 'T') !== false,
|
|
strpos($val, 'Z') !== false
|
|
);
|
|
}
|
|
}
|
|
|
|
$this->rDates = $rDates;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get the array of dates that will be included in a recurrence set.
|
|
*
|
|
* @return DateInclusion[]
|
|
*/
|
|
public function getRDates()
|
|
{
|
|
return $this->rDates;
|
|
}
|
|
|
|
/**
|
|
* This rule specifies an array of exception dates that will not be
|
|
* included in a recurrence set.
|
|
*
|
|
* @param string[]|DateExclusion[] $exDates Array of dates that will not be
|
|
* included in the recurrence set.
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setExDates(array $exDates)
|
|
{
|
|
$timezone = new \DateTimeZone($this->getTimezone());
|
|
|
|
foreach ($exDates as $key => $val) {
|
|
if ($val instanceof DateExclusion) {
|
|
$val->date = $this->convertZtoUtc($val->date);
|
|
} else {
|
|
$date = new \DateTime($val, $timezone);
|
|
$exDates[$key] = new DateExclusion(
|
|
$this->convertZtoUtc($date),
|
|
strpos($val, 'T') !== false,
|
|
strpos($val, 'Z') !== false
|
|
);
|
|
}
|
|
}
|
|
|
|
$this->exDates = $exDates;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* DateTime::setTimezone fails if the timezone does not have an ID.
|
|
* "Z" is the same as "UTC", but "Z" does not have an ID.
|
|
*
|
|
* This is necessary for exclusion dates to be handled properly.
|
|
*
|
|
* @param \DateTimeInterface $date
|
|
*
|
|
* @return \DateTimeInterface
|
|
*/
|
|
private function convertZtoUtc(\DateTimeInterface $date)
|
|
{
|
|
if ($date->getTimezone()->getName() !== 'Z') {
|
|
return $date;
|
|
}
|
|
|
|
return $date->setTimezone(new \DateTimeZone('UTC'));
|
|
}
|
|
|
|
/**
|
|
* Get the array of dates that will not be included in a recurrence set.
|
|
*
|
|
* @return DateExclusion[]
|
|
*/
|
|
public function getExDates()
|
|
{
|
|
return $this->exDates;
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
public function repeatsIndefinitely()
|
|
{
|
|
return !$this->getCount() && !$this->getUntil() && !$this->getEndDate();
|
|
}
|
|
}
|