First commit

This commit is contained in:
Theodotos Andreou 2018-01-14 13:10:16 +00:00
commit c6e2478c40
13918 changed files with 2303184 additions and 0 deletions

View file

@ -0,0 +1,180 @@
##When
**If you are considering using When, please use the [develop branch](https://github.com/tplaner/When/tree/develop) it will replace this branch when the documentation is complete, functionally it offers everything this version does, it supports PHP 5.3+.**
Date/Calendar recursion library for PHP 5.2+
Author: Thomas Planer
---
###About
After a comprehensive search I couldn't find a PHP library which could handle recursive dates.
There is: [http://phpicalendar.org/][6] however it would have been extremely difficult to extract the recursion
portion of the script from the application.
Oddly, there are extremely good date recursion libraries for both Ruby and Python:
Ruby: [http://github.com/seejohnrun/ice_cube][1]
Python: [http://labix.org/python-dateutil][2]
Since I couldn't find an equivalent for PHP I created [When][3].
---
###Unit Tests
Tests were written in PHPUnit ([http://www.phpunit.de/][4])
Initial set of tests were created from the examples found within RFC5545 ([http://tools.ietf.org/html/rfc5545][5]).
-----------------------------------
###Documentation
Initializing the class
$when = new When();
Once you have initialized the class you can create a recurring event by calling on the recur method
$when->recur(<DateTime object|valid Date string>, <yearly|monthly|weekly|daily>);
You can limit the number of dates to find by specifying a limit():
$when->limit(<int>);
Alternatively you can specify an end date:
$when->until(<DateTime object|valid Date String>);
Note: the end date does not have to match the recurring pattern.
Note: the script will stop returning results when either the limit or the end date is met.
More documentation to come, please take a look at the unit tests for an understanding of what the class is capable of.
---
###Examples (take a look at the unit tests for more examples)
The next 5 occurrences of Friday the 13th:
$r = new When();
$r->recur(new DateTime(), 'monthly')
->count(5)
->byday(array('FR'))
->bymonthday(array(13));
while($result = $r->next())
{
echo $result->format('c') . '<br />';
}
Every four years, the first Tuesday after a Monday in November, for the next 20 years (U.S. Presidential Election day):
// this is the next election date
$start = new DateTime('2012-09-06');
$r = new When();
$r->recur($start, 'yearly')
->until($start->modify('+20 years'))
->interval(4)
->bymonth(array(11))
->byday(array('TU'))
->bymonthday(array(2,3,4,5,6,7,8));
while($result = $r->next())
{
echo $result->format('c') . '<br />';
}
You can now pass raw RRULE's to the class:
$r = new When();
$r->recur('19970922T090000')->rrule('FREQ=MONTHLY;COUNT=6;BYDAY=-2MO');
while($result = $r->next())
{
echo $result->format('c') . '<br />';
}
**Warnings:**
* If you submit a pattern which has no results the script will loop infinitely.
* If you do not specify an end date (until) or a count for your pattern you must limit the number of results within your script to avoid an infinite loop.
---
###Contributing
If you would like to contribute please create a fork and upon making changes submit a pull request.
Please ensure 100% pass of unit tests before submitting a pull request.
There are 78 tests, 1410 assertions currently.
>>>phpunit --verbose tests
PHPUnit 3.4.15 by Sebastian Bergmann.
tests
When_Core_Tests
..
When_Daily_Rrule_Test
.....
When_Daily_Test
.....
When_Iterator_Tests
..
When_Monthly_Rrule_Test
..............
When_Monthly_Test
..............
When_Weekly_Rrule_Test
........
When_Weekly_Test
........
When_Rrule_Test
..........
When_Yearly_Test
..........
Time: 2 seconds, Memory: 6.00Mb
OK (78 tests, 1410 assertions)
---
###License
Copyright (c) 2010 Thomas Planer
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.
[1]: http://github.com/seejohnrun/ice_cube
[2]: http://labix.org/python-dateutil
[3]: http://github.com/tplaner/When
[4]: http://www.phpunit.de/
[5]: http://tools.ietf.org/html/rfc5545
[6]: http://phpicalendar.org/

View file

@ -0,0 +1,755 @@
<?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;
protected $keep_first_month_day;
/**
* __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);
$this->counter = 0;
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 || ($this->gobymonthday && $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;
}
}
}
elseif($interval == "month")
{
// Keep track of the original day of the month that was used
if ($this->keep_first_month_day === null) {
$this->keep_first_month_day = $month_day;
}
$month_count = 1;
foreach($this->bymonth as $_month)
{
$date_time = new DateTime($year . '-' . $_month . '-' . $this->keep_first_month_day . ' ' . $timestamp);
if ($month_count == count($this->bymonth)) {
$this->try_date->modify('+1 year');
}
if($date_time >= $this->start_date)
{
$this->suggestions[] = clone $date_time;
}
$month_count++;
}
}
else
{
$this->suggestions[] = clone $this->try_date;
}
if($interval == "month")
{
for ($i=0; $i< $this->interval; $i++)
{
$this->try_date->modify('+ 28 days');
$this->try_date->setDate($this->try_date->format('Y'), $this->try_date->format('m'), $this->try_date->format('t'));
}
}
else
{
$this->try_date->modify($this->interval . ' ' . $interval);
}
}
public 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();
}
}
}
}
}

View file

@ -0,0 +1,112 @@
<?php
/**
* Name: When_Iterator
* Author: Thomas Planer <tplaner@gmail.com>
* Location: http://github.com/tplaner/When
* Created: November 2010
* Description: Implements PHP's Object Iteration Interface (http://us.php.net/Iterator & http://php.net/manual/en/class.iterator.php) so you can use the object within a foreach loop.
*
* Thanks to Andrew Collington for suggesting the implementation of an Iterator and supplying the base code for it.
*/
require_once('When.php');
class When_Iterator extends When implements Iterator
{
// store the current position in the array
protected $position = 0;
// store an individual result if caching is disabled
protected $result;
// store all of the results
protected $results = array();
protected $cache = false;
// caching the results will cause the script to
// use more memory but less cpu (should also perform quicker)
//
// results should always be the same regardless of cache
public function __construct($cache = false)
{
parent::__construct();
$this->position = 0;
$this->results = array();
$this->cache = $cache;
}
public function rewind()
{
if($this->cache)
{
$this->position = 0;
}
else
{
// reset the counter and try_date in the parent class
$this->counter = 0;
$this->try_date = clone $this->start_date;
}
}
public function current()
{
if($this->cache === true)
{
return $this->results[$this->position];
}
else
{
return $this->result;
}
}
// only used if caching is enabled
public function key()
{
return $this->position;
}
// only used of caching is enabled
public function next()
{
++$this->position;
}
public function valid()
{
if($this->cache === true)
{
// check to see if the current position has already been stored
if(!empty($this->results[$this->position]))
{
return isset($this->results[$this->position]);
}
// if it hasn't been found, check to see if there are more dates
elseif($next_date = parent::next())
{
$this->results[] = $next_date;
return isset($next_date);
}
}
else
{
// check to see if there is another result and set that as the result
if($next_date = parent::next())
{
$this->result = $next_date;
return isset($this->result);
}
}
// end the foreach loop when all options are exhausted
return false;
}
public function enable_cache($cache)
{
$this->cache = $cache;
}
}