commiting uncommited changes on live site
[weblabels.fsf.org.git] / crm.fsf.org / 20131203 / files / sites / all / modules-old / civicrm / packages / When / When.php
1 <?php
2 /**
3 * Name: When
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)
9 */
10 class When
11 {
12 protected $frequency;
13
14 protected $start_date;
15 protected $try_date;
16
17 protected $end_date;
18
19 protected $gobymonth;
20 protected $bymonth;
21
22 protected $gobyweekno;
23 protected $byweekno;
24
25 protected $gobyyearday;
26 protected $byyearday;
27
28 protected $gobymonthday;
29 protected $bymonthday;
30
31 protected $gobyday;
32 protected $byday;
33
34 protected $gobysetpos;
35 protected $bysetpos;
36
37 protected $suggestions;
38
39 protected $count;
40 protected $counter;
41
42 protected $goenddate;
43
44 protected $interval;
45
46 protected $wkst;
47
48 protected $valid_week_days;
49 protected $valid_frequency;
50
51 protected $keep_first_month_day;
52
53 /**
54 * __construct
55 */
56 public function __construct()
57 {
58 $this->frequency = null;
59
60 $this->gobymonth = false;
61 $this->bymonth = range(1,12);
62
63 $this->gobymonthday = false;
64 $this->bymonthday = range(1,31);
65
66 $this->gobyday = false;
67 // setup the valid week days (0 = sunday)
68 $this->byday = range(0,6);
69
70 $this->gobyyearday = false;
71 $this->byyearday = range(0,366);
72
73 $this->gobysetpos = false;
74 $this->bysetpos = range(1,366);
75
76 $this->gobyweekno = false;
77 // setup the range for valid weeks
78 $this->byweekno = range(0,54);
79
80 $this->suggestions = array();
81
82 // this will be set if a count() is specified
83 $this->count = 0;
84 // how many *valid* results we returned
85 $this->counter = 0;
86
87 // max date we'll return
88 $this->end_date = new DateTime('9999-12-31');
89
90 // the interval to increase the pattern by
91 $this->interval = 1;
92
93 // what day does the week start on? (0 = sunday)
94 $this->wkst = 0;
95
96 $this->valid_week_days = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
97
98 $this->valid_frequency = array('SECONDLY', 'MINUTELY', 'HOURLY', 'DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY');
99 }
100
101 /**
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
104 */
105 public function recur($start_date, $frequency = "daily")
106 {
107 try
108 {
109 if(is_object($start_date))
110 {
111 $this->start_date = clone $start_date;
112 }
113 else
114 {
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);
118 }
119
120 $this->try_date = clone $this->start_date;
121 }
122 catch(Exception $e)
123 {
124 throw new InvalidArgumentException('Invalid start date DateTime: ' . $e);
125 }
126
127 $this->freq($frequency);
128
129 return $this;
130 }
131
132 public function freq($frequency)
133 {
134 if(in_array(strtoupper($frequency), $this->valid_frequency))
135 {
136 $this->frequency = strtoupper($frequency);
137 }
138 else
139 {
140 throw new InvalidArgumentException('Invalid frequency type.');
141 }
142
143 return $this;
144 }
145
146 // accepts an rrule directly
147 public function rrule($rrule)
148 {
149 // strip off a trailing semi-colon
150 $rrule = trim($rrule, ";");
151
152 $parts = explode(";", $rrule);
153
154 foreach($parts as $part)
155 {
156 list($rule, $param) = explode("=", $part);
157
158 $rule = strtoupper($rule);
159 $param = strtoupper($param);
160
161 switch($rule)
162 {
163 case "FREQ":
164 $this->frequency = $param;
165 break;
166 case "UNTIL":
167 $this->until($param);
168 break;
169 case "COUNT":
170 $this->count($param);
171 $this->counter = 0;
172 break;
173 case "INTERVAL":
174 $this->interval($param);
175 break;
176 case "BYDAY":
177 $params = explode(",", $param);
178 $this->byday($params);
179 break;
180 case "BYMONTHDAY":
181 $params = explode(",", $param);
182 $this->bymonthday($params);
183 break;
184 case "BYYEARDAY":
185 $params = explode(",", $param);
186 $this->byyearday($params);
187 break;
188 case "BYWEEKNO":
189 $params = explode(",", $param);
190 $this->byweekno($params);
191 break;
192 case "BYMONTH":
193 $params = explode(",", $param);
194 $this->bymonth($params);
195 break;
196 case "BYSETPOS":
197 $params = explode(",", $param);
198 $this->bysetpos($params);
199 break;
200 case "WKST":
201 $this->wkst($param);
202 break;
203 }
204 }
205
206 return $this;
207 }
208
209 //max number of items to return based on the pattern
210 public function count($count)
211 {
212 $this->count = (int)$count;
213
214 return $this;
215 }
216
217 // how often the recurrence rule repeats
218 public function interval($interval)
219 {
220 $this->interval = (int)$interval;
221
222 return $this;
223 }
224
225 // starting day of the week
226 public function wkst($day)
227 {
228 switch($day)
229 {
230 case 'SU':
231 $this->wkst = 0;
232 break;
233 case 'MO':
234 $this->wkst = 1;
235 break;
236 case 'TU':
237 $this->wkst = 2;
238 break;
239 case 'WE':
240 $this->wkst = 3;
241 break;
242 case 'TH':
243 $this->wkst = 4;
244 break;
245 case 'FR':
246 $this->wkst = 5;
247 break;
248 case 'SA':
249 $this->wkst = 6;
250 break;
251 }
252
253 return $this;
254 }
255
256 // max date
257 public function until($end_date)
258 {
259 try
260 {
261 if(is_object($end_date))
262 {
263 $this->end_date = clone $end_date;
264 }
265 else
266 {
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);
270 }
271 }
272 catch(Exception $e)
273 {
274 throw new InvalidArgumentException('Invalid end date DateTime: ' . $e);
275 }
276
277 return $this;
278 }
279
280 public function bymonth($months)
281 {
282 if(is_array($months))
283 {
284 $this->gobymonth = true;
285 $this->bymonth = $months;
286 }
287
288 return $this;
289 }
290
291 public function bymonthday($days)
292 {
293 if(is_array($days))
294 {
295 $this->gobymonthday = true;
296 $this->bymonthday = $days;
297 }
298
299 return $this;
300 }
301
302 public function byweekno($weeks)
303 {
304 $this->gobyweekno = true;
305
306 if(is_array($weeks))
307 {
308 $this->byweekno = $weeks;
309 }
310
311 return $this;
312 }
313
314 public function bysetpos($days)
315 {
316 $this->gobysetpos = true;
317
318 if(is_array($days))
319 {
320 $this->bysetpos = $days;
321 }
322
323 return $this;
324 }
325
326 public function byday($days)
327 {
328 $this->gobyday = true;
329
330 if(is_array($days))
331 {
332 $this->byday = array();
333 foreach($days as $day)
334 {
335 $len = strlen($day);
336
337 $as = '+';
338
339 // 0 mean no occurence is set
340 $occ = 0;
341
342 if($len == 3)
343 {
344 $occ = substr($day, 0, 1);
345 }
346 if($len == 4)
347 {
348 $as = substr($day, 0, 1);
349 $occ = substr($day, 1, 1);
350 }
351
352 if($as == '-')
353 {
354 $occ = '-' . $occ;
355 }
356 else
357 {
358 $occ = '+' . $occ;
359 }
360
361 $day = substr($day, -2, 2);
362 switch($day)
363 {
364 case 'SU':
365 $this->byday[] = $occ . 'SU';
366 break;
367 case 'MO':
368 $this->byday[] = $occ . 'MO';
369 break;
370 case 'TU':
371 $this->byday[] = $occ . 'TU';
372 break;
373 case 'WE':
374 $this->byday[] = $occ . 'WE';
375 break;
376 case 'TH':
377 $this->byday[] = $occ . 'TH';
378 break;
379 case 'FR':
380 $this->byday[] = $occ . 'FR';
381 break;
382 case 'SA':
383 $this->byday[] = $occ . 'SA';
384 break;
385 }
386 }
387 }
388
389 return $this;
390 }
391
392 public function byyearday($days)
393 {
394 $this->gobyyearday = true;
395
396 if(is_array($days))
397 {
398 $this->byyearday = $days;
399 }
400
401 return $this;
402 }
403
404 // this creates a basic list of dates to "try"
405 protected function create_suggestions()
406 {
407 switch($this->frequency)
408 {
409 case "YEARLY":
410 $interval = 'year';
411 break;
412 case "MONTHLY":
413 $interval = 'month';
414 break;
415 case "WEEKLY":
416 $interval = 'week';
417 break;
418 case "DAILY":
419 $interval = 'day';
420 break;
421 case "HOURLY":
422 $interval = 'hour';
423 break;
424 case "MINUTELY":
425 $interval = 'minute';
426 break;
427 case "SECONDLY":
428 $interval = 'second';
429 break;
430 }
431
432 $month_day = $this->try_date->format('j');
433 $month = $this->try_date->format('n');
434 $year = $this->try_date->format('Y');
435
436
437
438 $timestamp = $this->try_date->format('H:i:s');
439
440 if($this->gobysetpos)
441 {
442 if($this->try_date == $this->start_date)
443 {
444 $this->suggestions[] = clone $this->try_date;
445 }
446 else
447 {
448 if($this->gobyday)
449 {
450 foreach($this->bysetpos as $_pos)
451 {
452 $tmp_array = array();
453 $_mdays = range(1, date('t',mktime(0,0,0,$month,1,$year)));
454 foreach($_mdays as $_mday)
455 {
456 $date_time = new DateTime($year . '-' . $month . '-' . $_mday . ' ' . $timestamp);
457
458 $occur = ceil($_mday / 7);
459
460 $day_of_week = $date_time->format('l');
461 $dow_abr = strtoupper(substr($day_of_week, 0, 2));
462
463 // set the day of the month + (positive)
464 $occur = '+' . $occur . $dow_abr;
465 $occur_zero = '+0' . $dow_abr;
466
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;
470
471 $day_from_end_of_month = $date_time->format('t') + 1 - $_mday;
472
473 if(in_array($occur, $this->byday) || in_array($occur_zero, $this->byday) || in_array($occur_neg, $this->byday))
474 {
475 $tmp_array[] = clone $date_time;
476 }
477 }
478
479 if($_pos > 0)
480 {
481 $this->suggestions[] = clone $tmp_array[$_pos - 1];
482 }
483 else
484 {
485 $this->suggestions[] = clone $tmp_array[count($tmp_array) + $_pos];
486 }
487
488 }
489 }
490 }
491 }
492 elseif($this->gobyyearday)
493 {
494 foreach($this->byyearday as $_day)
495 {
496 if($_day >= 0)
497 {
498 $_day--;
499
500 $_time = strtotime('+' . $_day . ' days', mktime(0, 0, 0, 1, 1, $year));
501 $this->suggestions[] = new Datetime(date('Y-m-d', $_time) . ' ' . $timestamp);
502 }
503 else
504 {
505 $year_day_neg = 365 + $_day;
506 $leap_year = $this->try_date->format('L');
507 if($leap_year == 1)
508 {
509 $year_day_neg = 366 + $_day;
510 }
511
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);
514 }
515 }
516 }
517 // special case because for years you need to loop through the months too
518 elseif($this->gobyday && $interval == "year")
519 {
520 foreach($this->bymonth as $_month)
521 {
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)
525 {
526 $date_time = new DateTime($year . '-' . $_month . '-' . $_mday . ' ' . $timestamp);
527
528 // get the week of the month (1, 2, 3, 4, 5, etc)
529 $week = $date_time->format('W');
530
531 if($date_time >= $this->start_date && in_array($week, $this->byweekno))
532 {
533 $this->suggestions[] = clone $date_time;
534 }
535 }
536 }
537 }
538 elseif($interval == "day")
539 {
540 $this->suggestions[] = clone $this->try_date;
541 }
542 elseif($interval == "week")
543 {
544 $this->suggestions[] = clone $this->try_date;
545
546 if($this->gobyday)
547 {
548 $week_day = $this->try_date->format('w');
549
550 $days_in_month = $this->try_date->format('t');
551
552 $overflow_count = 1;
553 $_day = $month_day;
554
555 $run = true;
556 while($run)
557 {
558 $_day++;
559 if($_day <= $days_in_month)
560 {
561 $tmp_date = new DateTime($year . '-' . $month . '-' . $_day . ' ' . $timestamp);
562 }
563 else
564 {
565 //$tmp_month = $month+1;
566 $tmp_date = new DateTime($year . '-' . $month . '-' . $overflow_count . ' ' . $timestamp);
567 $tmp_date->modify('+1 month');
568 $overflow_count++;
569 }
570
571 $week_day = $tmp_date->format('w');
572
573 if($this->try_date == $this->start_date)
574 {
575 if($week_day == $this->wkst)
576 {
577 $this->try_date = clone $tmp_date;
578 $this->try_date->modify('-7 days');
579 $run = false;
580 }
581 }
582
583 if($week_day != $this->wkst)
584 {
585 $this->suggestions[] = clone $tmp_date;
586 }
587 else
588 {
589 $run = false;
590 }
591 }
592 }
593 }
594 elseif($this->gobyday || ($this->gobymonthday && $interval == "month"))
595 {
596 $_mdays = range(1, date('t',mktime(0,0,0,$month,1,$year)));
597 foreach($_mdays as $_mday)
598 {
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');
602
603 if($date_time >= $this->start_date && in_array($week, $this->byweekno))
604 {
605 $this->suggestions[] = clone $date_time;
606 }
607 }
608 }
609 elseif($this->gobymonth)
610 {
611 foreach($this->bymonth as $_month)
612 {
613 $date_time = new DateTime($year . '-' . $_month . '-' . $month_day . ' ' . $timestamp);
614
615 if($date_time >= $this->start_date)
616 {
617 $this->suggestions[] = clone $date_time;
618 }
619 }
620 }
621 elseif($interval == "month")
622 {
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;
626 }
627
628 $month_count = 1;
629 foreach($this->bymonth as $_month)
630 {
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');
634 }
635
636 if($date_time >= $this->start_date)
637 {
638 $this->suggestions[] = clone $date_time;
639 }
640 $month_count++;
641 }
642 }
643 else
644 {
645 $this->suggestions[] = clone $this->try_date;
646 }
647
648 if($interval == "month")
649 {
650 for ($i=0; $i< $this->interval; $i++)
651 {
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'));
654 }
655 }
656 else
657 {
658 $this->try_date->modify($this->interval . ' ' . $interval);
659 }
660 }
661
662 public function valid_date($date)
663 {
664 $year = $date->format('Y');
665 $month = $date->format('n');
666 $day = $date->format('j');
667
668 $year_day = $date->format('z') + 1;
669
670 $year_day_neg = -366 + $year_day;
671 $leap_year = $date->format('L');
672 if($leap_year == 1)
673 {
674 $year_day_neg = -367 + $year_day;
675 }
676
677 // this is the nth occurence of the date
678 $occur = ceil($day / 7);
679
680 $week = $date->format('W');
681
682 $day_of_week = $date->format('l');
683 $dow_abr = strtoupper(substr($day_of_week, 0, 2));
684
685 // set the day of the month + (positive)
686 $occur = '+' . $occur . $dow_abr;
687 $occur_zero = '+0' . $dow_abr;
688
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;
692
693 $day_from_end_of_month = $date->format('t') + 1 - $day;
694
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)))
700 {
701 return true;
702 }
703 else
704 {
705 return false;
706 }
707 }
708
709 // return the next valid DateTime object which matches the pattern and follows the rules
710 public function next()
711 {
712 // check the counter is set
713 if($this->count !== 0)
714 {
715 if($this->counter >= $this->count)
716 {
717 return false;
718 }
719 }
720
721 // create initial set of suggested dates
722 if(count($this->suggestions) === 0)
723 {
724 $this->create_suggestions();
725 }
726
727 // loop through the suggested dates
728 while(count($this->suggestions) > 0)
729 {
730 // get the first one on the array
731 $try_date = array_shift($this->suggestions);
732
733 // make sure the date doesn't exceed the max date
734 if($try_date > $this->end_date)
735 {
736 return false;
737 }
738
739 // make sure it falls within the allowed days
740 if($this->valid_date($try_date) === true)
741 {
742 $this->counter++;
743 return $try_date;
744 }
745 else
746 {
747 // we might be out of suggested days, so load some more
748 if(count($this->suggestions) === 0)
749 {
750 $this->create_suggestions();
751 }
752 }
753 }
754 }
755 }