nextcloud/3rdparty/when/When.php

732 lines
15 KiB
PHP

<?php
/**
* Name: When
* Author: Thomas Planer <tplaner@gmail.com>
* Location: http://github.com/tplaner/When
* Created: September 2010
* Description: Determines the next date of recursion given an iCalendar "rrule" like pattern.
* Requirements: PHP 5.3+ - makes extensive use of the Date and Time library (http://us2.php.net/manual/en/book.datetime.php)
*/
class When
{
protected $frequency;
protected $start_date;
protected $try_date;
protected $end_date;
protected $gobymonth;
protected $bymonth;
protected $gobyweekno;
protected $byweekno;
protected $gobyyearday;
protected $byyearday;
protected $gobymonthday;
protected $bymonthday;
protected $gobyday;
protected $byday;
protected $gobysetpos;
protected $bysetpos;
protected $suggestions;
protected $count;
protected $counter;
protected $goenddate;
protected $interval;
protected $wkst;
protected $valid_week_days;
protected $valid_frequency;
/**
* __construct
*/
public function __construct()
{
$this->frequency = null;
$this->gobymonth = false;
$this->bymonth = range(1,12);
$this->gobymonthday = false;
$this->bymonthday = range(1,31);
$this->gobyday = false;
// setup the valid week days (0 = sunday)
$this->byday = range(0,6);
$this->gobyyearday = false;
$this->byyearday = range(0,366);
$this->gobysetpos = false;
$this->bysetpos = range(1,366);
$this->gobyweekno = false;
// setup the range for valid weeks
$this->byweekno = range(0,54);
$this->suggestions = array();
// this will be set if a count() is specified
$this->count = 0;
// how many *valid* results we returned
$this->counter = 0;
// max date we'll return
$this->end_date = new DateTime('9999-12-31');
// the interval to increase the pattern by
$this->interval = 1;
// what day does the week start on? (0 = sunday)
$this->wkst = 0;
$this->valid_week_days = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
$this->valid_frequency = array('SECONDLY', 'MINUTELY', 'HOURLY', 'DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY');
}
/**
* @param DateTime|string $start_date of the recursion - also is the first return value.
* @param string $frequency of the recrusion, valid frequencies: secondly, minutely, hourly, daily, weekly, monthly, yearly
*/
public function recur($start_date, $frequency = "daily")
{
try
{
if(is_object($start_date))
{
$this->start_date = clone $start_date;
}
else
{
// timestamps within the RFC have a 'Z' at the end of them, remove this.
$start_date = trim($start_date, 'Z');
$this->start_date = new DateTime($start_date);
}
$this->try_date = clone $this->start_date;
}
catch(Exception $e)
{
throw new InvalidArgumentException('Invalid start date DateTime: ' . $e);
}
$this->freq($frequency);
return $this;
}
public function freq($frequency)
{
if(in_array(strtoupper($frequency), $this->valid_frequency))
{
$this->frequency = strtoupper($frequency);
}
else
{
throw new InvalidArgumentException('Invalid frequency type.');
}
return $this;
}
// accepts an rrule directly
public function rrule($rrule)
{
// strip off a trailing semi-colon
$rrule = trim($rrule, ";");
$parts = explode(";", $rrule);
foreach($parts as $part)
{
list($rule, $param) = explode("=", $part);
$rule = strtoupper($rule);
$param = strtoupper($param);
switch($rule)
{
case "FREQ":
$this->frequency = $param;
break;
case "UNTIL":
$this->until($param);
break;
case "COUNT":
$this->count($param);
break;
case "INTERVAL":
$this->interval($param);
break;
case "BYDAY":
$params = explode(",", $param);
$this->byday($params);
break;
case "BYMONTHDAY":
$params = explode(",", $param);
$this->bymonthday($params);
break;
case "BYYEARDAY":
$params = explode(",", $param);
$this->byyearday($params);
break;
case "BYWEEKNO":
$params = explode(",", $param);
$this->byweekno($params);
break;
case "BYMONTH":
$params = explode(",", $param);
$this->bymonth($params);
break;
case "BYSETPOS":
$params = explode(",", $param);
$this->bysetpos($params);
break;
case "WKST":
$this->wkst($param);
break;
}
}
return $this;
}
//max number of items to return based on the pattern
public function count($count)
{
$this->count = (int)$count;
return $this;
}
// how often the recurrence rule repeats
public function interval($interval)
{
$this->interval = (int)$interval;
return $this;
}
// starting day of the week
public function wkst($day)
{
switch($day)
{
case 'SU':
$this->wkst = 0;
break;
case 'MO':
$this->wkst = 1;
break;
case 'TU':
$this->wkst = 2;
break;
case 'WE':
$this->wkst = 3;
break;
case 'TH':
$this->wkst = 4;
break;
case 'FR':
$this->wkst = 5;
break;
case 'SA':
$this->wkst = 6;
break;
}
return $this;
}
// max date
public function until($end_date)
{
try
{
if(is_object($end_date))
{
$this->end_date = clone $end_date;
}
else
{
// timestamps within the RFC have a 'Z' at the end of them, remove this.
$end_date = trim($end_date, 'Z');
$this->end_date = new DateTime($end_date);
}
}
catch(Exception $e)
{
throw new InvalidArgumentException('Invalid end date DateTime: ' . $e);
}
return $this;
}
public function bymonth($months)
{
if(is_array($months))
{
$this->gobymonth = true;
$this->bymonth = $months;
}
return $this;
}
public function bymonthday($days)
{
if(is_array($days))
{
$this->gobymonthday = true;
$this->bymonthday = $days;
}
return $this;
}
public function byweekno($weeks)
{
$this->gobyweekno = true;
if(is_array($weeks))
{
$this->byweekno = $weeks;
}
return $this;
}
public function bysetpos($days)
{
$this->gobysetpos = true;
if(is_array($days))
{
$this->bysetpos = $days;
}
return $this;
}
public function byday($days)
{
$this->gobyday = true;
if(is_array($days))
{
$this->byday = array();
foreach($days as $day)
{
$len = strlen($day);
$as = '+';
// 0 mean no occurence is set
$occ = 0;
if($len == 3)
{
$occ = substr($day, 0, 1);
}
if($len == 4)
{
$as = substr($day, 0, 1);
$occ = substr($day, 1, 1);
}
if($as == '-')
{
$occ = '-' . $occ;
}
else
{
$occ = '+' . $occ;
}
$day = substr($day, -2, 2);
switch($day)
{
case 'SU':
$this->byday[] = $occ . 'SU';
break;
case 'MO':
$this->byday[] = $occ . 'MO';
break;
case 'TU':
$this->byday[] = $occ . 'TU';
break;
case 'WE':
$this->byday[] = $occ . 'WE';
break;
case 'TH':
$this->byday[] = $occ . 'TH';
break;
case 'FR':
$this->byday[] = $occ . 'FR';
break;
case 'SA':
$this->byday[] = $occ . 'SA';
break;
}
}
}
return $this;
}
public function byyearday($days)
{
$this->gobyyearday = true;
if(is_array($days))
{
$this->byyearday = $days;
}
return $this;
}
// this creates a basic list of dates to "try"
protected function create_suggestions()
{
switch($this->frequency)
{
case "YEARLY":
$interval = 'year';
break;
case "MONTHLY":
$interval = 'month';
break;
case "WEEKLY":
$interval = 'week';
break;
case "DAILY":
$interval = 'day';
break;
case "HOURLY":
$interval = 'hour';
break;
case "MINUTELY":
$interval = 'minute';
break;
case "SECONDLY":
$interval = 'second';
break;
}
$month_day = $this->try_date->format('j');
$month = $this->try_date->format('n');
$year = $this->try_date->format('Y');
$timestamp = $this->try_date->format('H:i:s');
if($this->gobysetpos)
{
if($this->try_date == $this->start_date)
{
$this->suggestions[] = clone $this->try_date;
}
else
{
if($this->gobyday)
{
foreach($this->bysetpos as $_pos)
{
$tmp_array = array();
$_mdays = range(1, date('t',mktime(0,0,0,$month,1,$year)));
foreach($_mdays as $_mday)
{
$date_time = new DateTime($year . '-' . $month . '-' . $_mday . ' ' . $timestamp);
$occur = ceil($_mday / 7);
$day_of_week = $date_time->format('l');
$dow_abr = strtoupper(substr($day_of_week, 0, 2));
// set the day of the month + (positive)
$occur = '+' . $occur . $dow_abr;
$occur_zero = '+0' . $dow_abr;
// set the day of the month - (negative)
$total_days = $date_time->format('t') - $date_time->format('j');
$occur_neg = '-' . ceil(($total_days + 1)/7) . $dow_abr;
$day_from_end_of_month = $date_time->format('t') + 1 - $_mday;
if(in_array($occur, $this->byday) || in_array($occur_zero, $this->byday) || in_array($occur_neg, $this->byday))
{
$tmp_array[] = clone $date_time;
}
}
if($_pos > 0)
{
$this->suggestions[] = clone $tmp_array[$_pos - 1];
}
else
{
$this->suggestions[] = clone $tmp_array[count($tmp_array) + $_pos];
}
}
}
}
}
elseif($this->gobyyearday)
{
foreach($this->byyearday as $_day)
{
if($_day >= 0)
{
$_day--;
$_time = strtotime('+' . $_day . ' days', mktime(0, 0, 0, 1, 1, $year));
$this->suggestions[] = new Datetime(date('Y-m-d', $_time) . ' ' . $timestamp);
}
else
{
$year_day_neg = 365 + $_day;
$leap_year = $this->try_date->format('L');
if($leap_year == 1)
{
$year_day_neg = 366 + $_day;
}
$_time = strtotime('+' . $year_day_neg . ' days', mktime(0, 0, 0, 1, 1, $year));
$this->suggestions[] = new Datetime(date('Y-m-d', $_time) . ' ' . $timestamp);
}
}
}
// special case because for years you need to loop through the months too
elseif($this->gobyday && $interval == "year")
{
foreach($this->bymonth as $_month)
{
// this creates an array of days of the month
$_mdays = range(1, date('t',mktime(0,0,0,$_month,1,$year)));
foreach($_mdays as $_mday)
{
$date_time = new DateTime($year . '-' . $_month . '-' . $_mday . ' ' . $timestamp);
// get the week of the month (1, 2, 3, 4, 5, etc)
$week = $date_time->format('W');
if($date_time >= $this->start_date && in_array($week, $this->byweekno))
{
$this->suggestions[] = clone $date_time;
}
}
}
}
elseif($interval == "day")
{
$this->suggestions[] = clone $this->try_date;
}
elseif($interval == "week")
{
$this->suggestions[] = clone $this->try_date;
if($this->gobyday)
{
$week_day = $this->try_date->format('w');
$days_in_month = $this->try_date->format('t');
$overflow_count = 1;
$_day = $month_day;
$run = true;
while($run)
{
$_day++;
if($_day <= $days_in_month)
{
$tmp_date = new DateTime($year . '-' . $month . '-' . $_day . ' ' . $timestamp);
}
else
{
//$tmp_month = $month+1;
$tmp_date = new DateTime($year . '-' . $month . '-' . $overflow_count . ' ' . $timestamp);
$tmp_date->modify('+1 month');
$overflow_count++;
}
$week_day = $tmp_date->format('w');
if($this->try_date == $this->start_date)
{
if($week_day == $this->wkst)
{
$this->try_date = clone $tmp_date;
$this->try_date->modify('-7 days');
$run = false;
}
}
if($week_day != $this->wkst)
{
$this->suggestions[] = clone $tmp_date;
}
else
{
$run = false;
}
}
}
}
elseif($this->gobyday && $interval == "month")
{
$_mdays = range(1, date('t',mktime(0,0,0,$month,1,$year)));
foreach($_mdays as $_mday)
{
$date_time = new DateTime($year . '-' . $month . '-' . $_mday . ' ' . $timestamp);
// get the week of the month (1, 2, 3, 4, 5, etc)
$week = $date_time->format('W');
if($date_time >= $this->start_date && in_array($week, $this->byweekno))
{
$this->suggestions[] = clone $date_time;
}
}
}
elseif($this->gobymonth)
{
foreach($this->bymonth as $_month)
{
$date_time = new DateTime($year . '-' . $_month . '-' . $month_day . ' ' . $timestamp);
if($date_time >= $this->start_date)
{
$this->suggestions[] = clone $date_time;
}
}
}
else
{
$this->suggestions[] = clone $this->try_date;
}
if($interval == "month")
{
$this->try_date->modify('first day of next month');
if((int) date('t', $this->try_date->format('U')) > (int) $this->start_date->format('j')){
$this->try_date->modify('+' . (int) $this->start_date->format('j') - 1 . ' day');
}else{
$this->try_date->modify('+' . (int) date('t', $this->try_date->format('U')) - 1 . ' day');
}
}
else
{
$this->try_date->modify($this->interval . ' ' . $interval);
}
}
protected function valid_date($date)
{
$year = $date->format('Y');
$month = $date->format('n');
$day = $date->format('j');
$year_day = $date->format('z') + 1;
$year_day_neg = -366 + $year_day;
$leap_year = $date->format('L');
if($leap_year == 1)
{
$year_day_neg = -367 + $year_day;
}
// this is the nth occurence of the date
$occur = ceil($day / 7);
$week = $date->format('W');
$day_of_week = $date->format('l');
$dow_abr = strtoupper(substr($day_of_week, 0, 2));
// set the day of the month + (positive)
$occur = '+' . $occur . $dow_abr;
$occur_zero = '+0' . $dow_abr;
// set the day of the month - (negative)
$total_days = $date->format('t') - $date->format('j');
$occur_neg = '-' . ceil(($total_days + 1)/7) . $dow_abr;
$day_from_end_of_month = $date->format('t') + 1 - $day;
if(in_array($month, $this->bymonth) &&
(in_array($occur, $this->byday) || in_array($occur_zero, $this->byday) || in_array($occur_neg, $this->byday)) &&
in_array($week, $this->byweekno) &&
(in_array($day, $this->bymonthday) || in_array(-$day_from_end_of_month, $this->bymonthday)) &&
(in_array($year_day, $this->byyearday) || in_array($year_day_neg, $this->byyearday)))
{
return true;
}
else
{
return false;
}
}
// return the next valid DateTime object which matches the pattern and follows the rules
public function next()
{
// check the counter is set
if($this->count !== 0)
{
if($this->counter >= $this->count)
{
return false;
}
}
// create initial set of suggested dates
if(count($this->suggestions) === 0)
{
$this->create_suggestions();
}
// loop through the suggested dates
while(count($this->suggestions) > 0)
{
// get the first one on the array
$try_date = array_shift($this->suggestions);
// make sure the date doesn't exceed the max date
if($try_date > $this->end_date)
{
return false;
}
// make sure it falls within the allowed days
if($this->valid_date($try_date) === true)
{
$this->counter++;
return $try_date;
}
else
{
// we might be out of suggested days, so load some more
if(count($this->suggestions) === 0)
{
$this->create_suggestions();
}
}
}
}
}