4 * Author: Thomas Planer <tplaner@gmail.com>
5 * Location: http://github.com/tplaner/When
6 * Created: September 2010
7 * Description: Determines the next date of recursion given an iCalendar "rrule" like pattern.
8 * Requirements: PHP 5.3+ - makes extensive use of the Date and Time library (http://us2.php.net/manual/en/book.datetime.php)
14 protected $start_date;
22 protected $gobyweekno;
25 protected $gobyyearday;
28 protected $gobymonthday;
29 protected $bymonthday;
34 protected $gobysetpos;
37 protected $suggestions;
48 protected $valid_week_days;
49 protected $valid_frequency;
51 protected $keep_first_month_day;
56 public function __construct()
58 $this->frequency
= null;
60 $this->gobymonth
= false;
61 $this->bymonth
= range(1,12);
63 $this->gobymonthday
= false;
64 $this->bymonthday
= range(1,31);
66 $this->gobyday
= false;
67 // setup the valid week days (0 = sunday)
68 $this->byday
= range(0,6);
70 $this->gobyyearday
= false;
71 $this->byyearday
= range(0,366);
73 $this->gobysetpos
= false;
74 $this->bysetpos
= range(1,366);
76 $this->gobyweekno
= false;
77 // setup the range for valid weeks
78 $this->byweekno
= range(0,54);
80 $this->suggestions
= array();
82 // this will be set if a count() is specified
84 // how many *valid* results we returned
87 // max date we'll return
88 $this->end_date
= new DateTime('9999-12-31');
90 // the interval to increase the pattern by
93 // what day does the week start on? (0 = sunday)
96 $this->valid_week_days
= array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
98 $this->valid_frequency
= array('SECONDLY', 'MINUTELY', 'HOURLY', 'DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY');
102 * @param DateTime|string $start_date of the recursion - also is the first return value.
103 * @param string $frequency of the recrusion, valid frequencies: secondly, minutely, hourly, daily, weekly, monthly, yearly
105 public function recur($start_date, $frequency = "daily")
109 if(is_object($start_date))
111 $this->start_date
= clone $start_date;
115 // timestamps within the RFC have a 'Z' at the end of them, remove this.
116 $start_date = trim($start_date, 'Z');
117 $this->start_date
= new DateTime($start_date);
120 $this->try_date
= clone $this->start_date
;
124 throw new InvalidArgumentException('Invalid start date DateTime: ' . $e);
127 $this->freq($frequency);
132 public function freq($frequency)
134 if(in_array(strtoupper($frequency), $this->valid_frequency
))
136 $this->frequency
= strtoupper($frequency);
140 throw new InvalidArgumentException('Invalid frequency type.');
146 // accepts an rrule directly
147 public function rrule($rrule)
149 // strip off a trailing semi-colon
150 $rrule = trim($rrule, ";");
152 $parts = explode(";", $rrule);
154 foreach($parts as $part)
156 list($rule, $param) = explode("=", $part);
158 $rule = strtoupper($rule);
159 $param = strtoupper($param);
164 $this->frequency
= $param;
167 $this->until($param);
170 $this->count($param);
174 $this->interval($param);
177 $params = explode(",", $param);
178 $this->byday($params);
181 $params = explode(",", $param);
182 $this->bymonthday($params);
185 $params = explode(",", $param);
186 $this->byyearday($params);
189 $params = explode(",", $param);
190 $this->byweekno($params);
193 $params = explode(",", $param);
194 $this->bymonth($params);
197 $params = explode(",", $param);
198 $this->bysetpos($params);
209 //max number of items to return based on the pattern
210 public function count($count)
212 $this->count
= (int)$count;
217 // how often the recurrence rule repeats
218 public function interval($interval)
220 $this->interval
= (int)$interval;
225 // starting day of the week
226 public function wkst($day)
257 public function until($end_date)
261 if(is_object($end_date))
263 $this->end_date
= clone $end_date;
267 // timestamps within the RFC have a 'Z' at the end of them, remove this.
268 $end_date = trim($end_date, 'Z');
269 $this->end_date
= new DateTime($end_date);
274 throw new InvalidArgumentException('Invalid end date DateTime: ' . $e);
280 public function bymonth($months)
282 if(is_array($months))
284 $this->gobymonth
= true;
285 $this->bymonth
= $months;
291 public function bymonthday($days)
295 $this->gobymonthday
= true;
296 $this->bymonthday
= $days;
302 public function byweekno($weeks)
304 $this->gobyweekno
= true;
308 $this->byweekno
= $weeks;
314 public function bysetpos($days)
316 $this->gobysetpos
= true;
320 $this->bysetpos
= $days;
326 public function byday($days)
328 $this->gobyday
= true;
332 $this->byday
= array();
333 foreach($days as $day)
339 // 0 mean no occurence is set
344 $occ = substr($day, 0, 1);
348 $as = substr($day, 0, 1);
349 $occ = substr($day, 1, 1);
361 $day = substr($day, -2, 2);
365 $this->byday
[] = $occ . 'SU';
368 $this->byday
[] = $occ . 'MO';
371 $this->byday
[] = $occ . 'TU';
374 $this->byday
[] = $occ . 'WE';
377 $this->byday
[] = $occ . 'TH';
380 $this->byday
[] = $occ . 'FR';
383 $this->byday
[] = $occ . 'SA';
392 public function byyearday($days)
394 $this->gobyyearday
= true;
398 $this->byyearday
= $days;
404 // this creates a basic list of dates to "try"
405 protected function create_suggestions()
407 switch($this->frequency
)
425 $interval = 'minute';
428 $interval = 'second';
432 $month_day = $this->try_date
->format('j');
433 $month = $this->try_date
->format('n');
434 $year = $this->try_date
->format('Y');
438 $timestamp = $this->try_date
->format('H:i:s');
440 if($this->gobysetpos
)
442 if($this->try_date
== $this->start_date
)
444 $this->suggestions
[] = clone $this->try_date
;
450 foreach($this->bysetpos
as $_pos)
452 $tmp_array = array();
453 $_mdays = range(1, date('t',mktime(0,0,0,$month,1,$year)));
454 foreach($_mdays as $_mday)
456 $date_time = new DateTime($year . '-' . $month . '-' . $_mday . ' ' . $timestamp);
458 $occur = ceil($_mday / 7);
460 $day_of_week = $date_time->format('l');
461 $dow_abr = strtoupper(substr($day_of_week, 0, 2));
463 // set the day of the month + (positive)
464 $occur = '+' . $occur . $dow_abr;
465 $occur_zero = '+0' . $dow_abr;
467 // set the day of the month - (negative)
468 $total_days = $date_time->format('t') - $date_time->format('j');
469 $occur_neg = '-' . ceil(($total_days +
1)/7) . $dow_abr;
471 $day_from_end_of_month = $date_time->format('t') +
1 - $_mday;
473 if(in_array($occur, $this->byday
) ||
in_array($occur_zero, $this->byday
) ||
in_array($occur_neg, $this->byday
))
475 $tmp_array[] = clone $date_time;
481 $this->suggestions
[] = clone $tmp_array[$_pos - 1];
485 $this->suggestions
[] = clone $tmp_array[count($tmp_array) +
$_pos];
492 elseif($this->gobyyearday
)
494 foreach($this->byyearday
as $_day)
500 $_time = strtotime('+' . $_day . ' days', mktime(0, 0, 0, 1, 1, $year));
501 $this->suggestions
[] = new Datetime(date('Y-m-d', $_time) . ' ' . $timestamp);
505 $year_day_neg = 365 +
$_day;
506 $leap_year = $this->try_date
->format('L');
509 $year_day_neg = 366 +
$_day;
512 $_time = strtotime('+' . $year_day_neg . ' days', mktime(0, 0, 0, 1, 1, $year));
513 $this->suggestions
[] = new Datetime(date('Y-m-d', $_time) . ' ' . $timestamp);
517 // special case because for years you need to loop through the months too
518 elseif($this->gobyday
&& $interval == "year")
520 foreach($this->bymonth
as $_month)
522 // this creates an array of days of the month
523 $_mdays = range(1, date('t',mktime(0,0,0,$_month,1,$year)));
524 foreach($_mdays as $_mday)
526 $date_time = new DateTime($year . '-' . $_month . '-' . $_mday . ' ' . $timestamp);
528 // get the week of the month (1, 2, 3, 4, 5, etc)
529 $week = $date_time->format('W');
531 if($date_time >= $this->start_date
&& in_array($week, $this->byweekno
))
533 $this->suggestions
[] = clone $date_time;
538 elseif($interval == "day")
540 $this->suggestions
[] = clone $this->try_date
;
542 elseif($interval == "week")
544 $this->suggestions
[] = clone $this->try_date
;
548 $week_day = $this->try_date
->format('w');
550 $days_in_month = $this->try_date
->format('t');
559 if($_day <= $days_in_month)
561 $tmp_date = new DateTime($year . '-' . $month . '-' . $_day . ' ' . $timestamp);
565 //$tmp_month = $month+1;
566 $tmp_date = new DateTime($year . '-' . $month . '-' . $overflow_count . ' ' . $timestamp);
567 $tmp_date->modify('+1 month');
571 $week_day = $tmp_date->format('w');
573 if($this->try_date
== $this->start_date
)
575 if($week_day == $this->wkst
)
577 $this->try_date
= clone $tmp_date;
578 $this->try_date
->modify('-7 days');
583 if($week_day != $this->wkst
)
585 $this->suggestions
[] = clone $tmp_date;
594 elseif($this->gobyday ||
($this->gobymonthday
&& $interval == "month"))
596 $_mdays = range(1, date('t',mktime(0,0,0,$month,1,$year)));
597 foreach($_mdays as $_mday)
599 $date_time = new DateTime($year . '-' . $month . '-' . $_mday . ' ' . $timestamp);
600 // get the week of the month (1, 2, 3, 4, 5, etc)
601 $week = $date_time->format('W');
603 if($date_time >= $this->start_date
&& in_array($week, $this->byweekno
))
605 $this->suggestions
[] = clone $date_time;
609 elseif($this->gobymonth
)
611 foreach($this->bymonth
as $_month)
613 $date_time = new DateTime($year . '-' . $_month . '-' . $month_day . ' ' . $timestamp);
615 if($date_time >= $this->start_date
)
617 $this->suggestions
[] = clone $date_time;
621 elseif($interval == "month")
623 // Keep track of the original day of the month that was used
624 if ($this->keep_first_month_day
=== null) {
625 $this->keep_first_month_day
= $month_day;
629 foreach($this->bymonth
as $_month)
631 $date_time = new DateTime($year . '-' . $_month . '-' . $this->keep_first_month_day
. ' ' . $timestamp);
632 if ($month_count == count($this->bymonth
)) {
633 $this->try_date
->modify('+1 year');
636 if($date_time >= $this->start_date
)
638 $this->suggestions
[] = clone $date_time;
645 $this->suggestions
[] = clone $this->try_date
;
648 if($interval == "month")
650 for ($i=0; $i< $this->interval
; $i++
)
652 $this->try_date
->modify('+ 28 days');
653 $this->try_date
->setDate($this->try_date
->format('Y'), $this->try_date
->format('m'), $this->try_date
->format('t'));
658 $this->try_date
->modify($this->interval
. ' ' . $interval);
662 public function valid_date($date)
664 $year = $date->format('Y');
665 $month = $date->format('n');
666 $day = $date->format('j');
668 $year_day = $date->format('z') +
1;
670 $year_day_neg = -366 +
$year_day;
671 $leap_year = $date->format('L');
674 $year_day_neg = -367 +
$year_day;
677 // this is the nth occurence of the date
678 $occur = ceil($day / 7);
680 $week = $date->format('W');
682 $day_of_week = $date->format('l');
683 $dow_abr = strtoupper(substr($day_of_week, 0, 2));
685 // set the day of the month + (positive)
686 $occur = '+' . $occur . $dow_abr;
687 $occur_zero = '+0' . $dow_abr;
689 // set the day of the month - (negative)
690 $total_days = $date->format('t') - $date->format('j');
691 $occur_neg = '-' . ceil(($total_days +
1)/7) . $dow_abr;
693 $day_from_end_of_month = $date->format('t') +
1 - $day;
695 if(in_array($month, $this->bymonth
) &&
696 (in_array($occur, $this->byday
) ||
in_array($occur_zero, $this->byday
) ||
in_array($occur_neg, $this->byday
)) &&
697 in_array($week, $this->byweekno
) &&
698 (in_array($day, $this->bymonthday
) ||
in_array(-$day_from_end_of_month, $this->bymonthday
)) &&
699 (in_array($year_day, $this->byyearday
) ||
in_array($year_day_neg, $this->byyearday
)))
709 // return the next valid DateTime object which matches the pattern and follows the rules
710 public function next()
712 // check the counter is set
713 if($this->count
!== 0)
715 if($this->counter
>= $this->count
)
721 // create initial set of suggested dates
722 if(count($this->suggestions
) === 0)
724 $this->create_suggestions();
727 // loop through the suggested dates
728 while(count($this->suggestions
) > 0)
730 // get the first one on the array
731 $try_date = array_shift($this->suggestions
);
733 // make sure the date doesn't exceed the max date
734 if($try_date > $this->end_date
)
739 // make sure it falls within the allowed days
740 if($this->valid_date($try_date) === true)
747 // we might be out of suggested days, so load some more
748 if(count($this->suggestions
) === 0)
750 $this->create_suggestions();