source: trunk/prototype/plugins/when/When.php @ 7655

Revision 7655, 15.0 KB checked in by douglasz, 11 years ago (diff)

Ticket #3236 - Melhorias de performance no codigo do Expresso.

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