public function __construct($parts)
{
if (is_string($parts)) {
$parts = self::parseRfcString($parts);
} elseif (is_array($parts)) {
$parts = array_change_key_case($parts, CASE_UPPER);
} else {
throw new \InvalidArgumentException(sprintf('The first argument must be a string or an array (%s provided)', gettype($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
if (is_integer($parts['FREQ'])) {
if ($parts['FREQ'] > self::SECONDLY || $parts['FREQ'] < self::YEARLY) {
throw new \InvalidArgumentException('The FREQ rule part must be one of the following: ' . implode(', ', array_keys(self::$frequencies)));
}
$this->freq = $parts['FREQ'];
} else {
// string
$parts['FREQ'] = strtoupper($parts['FREQ']);
if (!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 = 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)');
}
$this->interval = (int) $parts['INTERVAL'];
// DTSTART
if (not_empty($parts['DTSTART'])) {
try {
$this->dtstart = self::parseDate($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 = new \DateTime();
}
// UNTIL (optional)
if (not_empty($parts['UNTIL'])) {
try {
$this->until = self::parseDate($parts['UNTIL']);
} catch (\Exception $e) {
throw new \InvalidArgumentException('Failed to parse UNTIL ; it 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'];
}
if ($this->until && $this->count) {
throw new \InvalidArgumentException('The UNTIL or COUNT rule parts MUST NOT occur in the same rule');
}
// 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 self::YEARLY:
if (!not_empty($parts['BYMONTH'])) {
$parts['BYMONTH'] = array((int) $this->dtstart->format('m'));
}
$parts['BYMONTHDAY'] = array((int) $this->dtstart->format('j'));
break;
case self::MONTHLY:
$parts['BYMONTHDAY'] = array((int) $this->dtstart->format('j'));
break;
case self::WEEKLY:
$parts['BYDAY'] = array(array_search($this->dtstart->format('N'), self::$week_days));
break;
}
}
// BYDAY (translated to byweekday for convenience)
if (not_empty($parts['BYDAY'])) {
if (!is_array($parts['BYDAY'])) {
$parts['BYDAY'] = explode(',', $parts['BYDAY']);
}
$this->byweekday = array();
$this->byweekday_nth = array();
foreach ($parts['BYDAY'] as $value) {
$value = trim(strtoupper($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_nth[] = array(self::$week_days[$matches[2]], (int) $matches[1]);
} else {
$this->byweekday[] = self::$week_days[$matches[2]];
}
}
if (!empty($this->byweekday_nth)) {
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 === 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.');
}
}
}
// 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 === self::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 = array();
$this->bymonthday_negative = array();
foreach ($parts['BYMONTHDAY'] as $value) {
$value = (int) $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[] = $value;
} else {
$this->bymonthday[] = $value;
}
}
}
if (not_empty($parts['BYYEARDAY'])) {
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.');
}
if (!is_array($parts['BYYEARDAY'])) {
$parts['BYYEARDAY'] = explode(',', $parts['BYYEARDAY']);
}
$this->bysetpos = array();
foreach ($parts['BYYEARDAY'] as $value) {
$value = (int) $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[] = $value;
}
}
// BYWEEKNO
if (not_empty($parts['BYWEEKNO'])) {
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.');
}
if (!is_array($parts['BYWEEKNO'])) {
$parts['BYWEEKNO'] = explode(',', $parts['BYWEEKNO']);
}
$this->byweekno = array();
foreach ($parts['BYWEEKNO'] as $value) {
$value = (int) $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[] = $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 = array();
foreach ($parts['BYMONTH'] as $value) {
$value = (int) $value;
if ($value < 1 || $value > 12) {
throw new \InvalidArgumentException('Invalid BYMONTH value: ' . $value);
}
$this->bymonth[] = $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']) || not_empty($parts['BYHOUR']) || not_empty($parts['BYMINUTE']) || not_empty($parts['BYSECOND']))) {
throw new \InvalidArgumentException('The BYSETPOS 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 = array();
foreach ($parts['BYSETPOS'] as $value) {
$value = (int) $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[] = $value;
}
}
if (not_empty($parts['BYHOUR'])) {
if (!is_array($parts['BYHOUR'])) {
$parts['BYHOUR'] = explode(',', $parts['BYHOUR']);
}
$this->byhour = array();
foreach ($parts['BYHOUR'] as $value) {
$value = (int) $value;
if ($value < 0 || $value > 23) {
throw new \InvalidArgumentException('Invalid BYHOUR value: ' . $value);
}
$this->byhour[] = $value;
}
sort($this->byhour);
} elseif ($this->freq < self::HOURLY) {
$this->byhour = array((int) $this->dtstart->format('G'));
}
if (not_empty($parts['BYMINUTE'])) {
if (!is_array($parts['BYMINUTE'])) {
$parts['BYMINUTE'] = explode(',', $parts['BYMINUTE']);
}
$this->byminute = array();
foreach ($parts['BYMINUTE'] as $value) {
$value = (int) $value;
if ($value < 0 || $value > 59) {
throw new \InvalidArgumentException('Invalid BYMINUTE value: ' . $value);
}
$this->byminute[] = $value;
}
sort($this->byminute);
} elseif ($this->freq < self::MINUTELY) {
$this->byminute = array((int) $this->dtstart->format('i'));
}
if (not_empty($parts['BYSECOND'])) {
if (!is_array($parts['BYSECOND'])) {
$parts['BYSECOND'] = explode(',', $parts['BYSECOND']);
}
$this->bysecond = array();
foreach ($parts['BYSECOND'] as $value) {
$value = (int) $value;
// yes, "60" is a valid value, in (very rare) cases on leap seconds
// December 31, 2005 23:59:60 UTC is a valid date...
// so is 2012-06-30T23:59:60UTC
if ($value < 0 || $value > 60) {
throw new \InvalidArgumentException('Invalid BYSECOND value: ' . $value);
}
$this->bysecond[] = $value;
}
sort($this->bysecond);
} elseif ($this->freq < self::SECONDLY) {
$this->bysecond = array((int) $this->dtstart->format('s'));
}
if ($this->freq < self::HOURLY) {
// for frequencies DAILY, WEEKLY, MONTHLY AND YEARLY, we can build
// an array of every time of the day at which there should be an
// occurrence - default, if no BYHOUR/BYMINUTE/BYSECOND are provided
// is only one time, and it's the DTSTART time. This is a cached version
// if you will, since it'll never change at these frequencies
$this->timeset = array();
foreach ($this->byhour as $hour) {
foreach ($this->byminute as $minute) {
foreach ($this->bysecond as $second) {
$this->timeset[] = array($hour, $minute, $second);
}
}
}
}
}