Adjusted time.php to honor note on separate row option.
[timetracker.git] / WEB-INF / lib / tdcron / class.tdcron.php
1 <?php
2
3         define('IDX_MINUTE',                    0);
4         define('IDX_HOUR',                      1);
5         define('IDX_DAY',                       2);
6         define('IDX_MONTH',                     3);
7         define('IDX_WEEKDAY',                   4);
8         define('IDX_YEAR',                      5);
9
10         /*
11          * tdCron v0.0.1 beta - CRON-Parser for PHP
12          *
13          * Copyright (c) 2010 Christian Land / tagdocs.de
14          *
15          * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
16          * associated documentation files (the "Software"), to deal in the Software without restriction,
17          * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
18          * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
19          * subject to the following conditions:
20          *
21          * The above copyright notice and this permission notice shall be included in all copies or substantial
22          * portions of the Software.
23          *
24          * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
25          * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
26          * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
27          * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
28          * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
29          *
30          * @author      Christian Land <devel@tagdocs.de>
31          * @package     tdCron
32          * @copyright   Copyright (c) 2010, Christian Land / tagdocs.de
33          * @version     v0.0.1 beta
34          */
35         class tdCron {
36
37                 /**
38                  * Parsed cron-expressions cache.
39                  * @var mixed
40                  */
41                 static private $pcron = array();
42
43                 /**
44                  * getNextOccurrence() uses a cron-expression to calculate the time and date at which a cronjob
45                  * should be executed the next time. If a reference-time is passed, the next time and date
46                  * after that time is calculated.
47                  *
48                  * @access      public
49                  * @param       string          $expression     cron-expression to use
50                  * @param       int             $timestamp      optional reference-time
51                  * @return      int
52                  */
53                 static public function getNextOccurrence($expression, $timestamp = null) {
54
55                         try {
56
57                                 // Convert timestamp to array
58
59                                 $next           = self::getTimestamp($timestamp);
60
61                                 // Calculate date/time
62
63                                 $next_time      = self::calculateDateTime($expression, $next);
64
65                         } catch (Exception $e) {
66
67                                 throw $e;
68
69                         }
70
71                         // return calculated time
72
73                         return $next_time;
74
75                 }
76
77                 /**
78                  * getLastOccurrence() does pretty much the same as getNextOccurrence(). The only difference
79                  * is, that it doesn't calculate the next but the last time a cronjob should have been executed.
80                  *
81                  * @access      public
82                  * @param       string          $expression     cron-expression to use
83                  * @param       int             $timestamp      optional reference-time
84                  * @return      int
85                  */
86
87                 static public function getLastOccurrence($expression, $timestamp = null) {
88
89                         try {
90
91                                 // Convert timestamp to array
92
93                                 $last           = self::getTimestamp($timestamp);
94
95                                 // Calculate date/time
96
97                                 $last_time      = self::calculateDateTime($expression, $last, false);
98
99                         } catch (Exception $e) {
100
101                                 throw $e;
102
103                         }
104
105                         // return calculated time
106
107                         return $last_time;
108
109                 }
110
111                 /**
112                  * calculateDateTime() is the function where all the magic happens :-)
113                  *
114                  * It calculates the time and date at which the next/last call of a cronjob is/was due.
115                  *
116                  * @access      private
117                  * @param       mixed           $value          cron-expression
118                  * @param       mixed           $rtime          reference-time
119                  * @param       bool            $next           true = nextOccurence, false = lastOccurence
120                  * @return      int
121                  */
122
123                 static private function calculateDateTime($expression, $rtime, $next = true) {
124
125                         // Initialize vars
126  
127                         $calc_date      = true;
128
129                         // Parse cron-expression (if neccessary)
130
131                         $cron           = self::getExpression($expression, !$next);
132
133                         // OK, lets see if the day/month/weekday of the reference-date exist in our
134                         // $cron-array.
135
136                         if (!in_array($rtime[IDX_DAY], $cron[IDX_DAY]) ||
137                             !in_array($rtime[IDX_MONTH], $cron[IDX_MONTH]) ||
138                             !in_array($rtime[IDX_WEEKDAY], $cron[IDX_WEEKDAY])) {
139
140                                 // OK, things are easy. The day/month/weekday of the reference time
141                                 // can't be found in the $cron-array. This means that no matter what
142                                 // happens, we WILL end up at at a different date than that of our
143                                 // reference-time. And in this case, the lastOccurrence will ALWAYS
144                                 // happen at the latest possible time of the day and the nextOccurrence
145                                 // at the earliest possible time.
146                                 //
147                                 // In both cases, the time can be found in the first elements of the
148                                 // hour/minute cron-arrays.
149
150                                 $rtime[IDX_HOUR]        = reset($cron[IDX_HOUR]);
151                                 $rtime[IDX_MINUTE]      = reset($cron[IDX_MINUTE]);
152
153                         } else {
154
155                                 // OK, things are getting a little bit more complicated...
156  
157                                 $nhour          = self::findValue($rtime[IDX_HOUR], $cron[IDX_HOUR], $next);
158
159                                 // Meh. Such a cruel world. Something has gone awry. Lets see HOW awry it went.
160
161                                 if ($nhour === false) { // Fix as per http://www.phpclasses.org/discuss/package/6699/thread/3/
162
163                                         // Ah, the hour-part went wrong. Thats easy. Wrong hour means that no
164                                         // matter what we do we'll end up at a different date. Thus we can use
165                                         // some simple operations to make things look pretty ;-)
166                                         //
167                                         // As alreasy mentioned before -> different date means earliest/latest
168                                         // time:
169
170                                         $rtime[IDX_HOUR]        = reset($cron[IDX_HOUR]);
171                                         $rtime[IDX_MINUTE]      = reset($cron[IDX_MINUTE]);
172
173                                         // Now all we have to do is add/subtract a day to get a new reference time
174                                         // to use later to find the right date. The following line probably looks
175                                         // a little odd but thats the easiest way of adding/substracting a day without
176                                         // screwing up the date. Just trust me on that one ;-)
177
178                                         $rtime                  = explode(',', strftime('%M,%H,%d,%m,%w,%Y', mktime($rtime[IDX_HOUR], $rtime[IDX_MINUTE], 0, $rtime[IDX_MONTH], $rtime[IDX_DAY], $rtime[IDX_YEAR]) + ((($next) ? 1 : -1) * 86400)));
179
180                                 } else {
181
182                                         // OK, there is a higher/lower hour available. Check the minutes-part.
183
184                                         $nminute        = self::findValue($rtime[IDX_MINUTE], $cron[IDX_MINUTE], $next);
185
186                                         if ($nminute === false) {
187
188                                                 // No matching minute-value found... lets see what happens if we substract/add an hour
189
190                                                 $nhour          = self::findValue($rtime[IDX_HOUR] + (($next) ? 1 : -1), $cron[IDX_HOUR], $next);
191
192                                                 if ($nhour === false) {
193
194                                                         // No more hours available... add/substract a day... you know what happens ;-)
195
196                                                         $nminute        = reset($cron[IDX_MINUTE]);
197                                                         $nhour          = reset($cron[IDX_HOUR]);
198
199                                                         $rtime          = explode(',', strftime('%M,%H,%d,%m,%w,%Y', mktime($nhour, $nminute, 0, $rtime[IDX_MONTH], $rtime[IDX_DAY], $rtime[IDX_YEAR]) + ((($next) ? 1 : -1) * 86400)));
200
201                                                 } else {
202
203                                                         // OK, there was another hour. Set the right minutes-value
204
205                                                         $rtime[IDX_HOUR]        = $nhour;
206                                                         $rtime[IDX_MINUTE]      = (($next) ? reset($cron[IDX_MINUTE]) : end($cron[IDX_MINUTE]));
207
208                                                         $calc_date      = false;
209
210                                                 }
211
212                                         } else {
213
214                                                 // OK, there is a matching minute... reset minutes if hour has changed
215
216                                                 if ($nhour <> $rtime[IDX_HOUR]) {
217                                                         $nminute                = reset($cron[IDX_MINUTE]);
218                                                 }
219
220                                                 // Set time
221  
222                                                 $rtime[IDX_HOUR]        = $nhour;
223                                                 $rtime[IDX_MINUTE]      = $nminute;
224  
225                                                 $calc_date      = false;
226
227                                         }
228
229                                 }
230
231                         }
232
233                         // If we have to calculate the date... we'll do so
234
235                         if ($calc_date) {
236
237                                 if (in_array($rtime[IDX_DAY], $cron[IDX_DAY]) &&
238                                     in_array($rtime[IDX_MONTH], $cron[IDX_MONTH]) &&
239                                     in_array($rtime[IDX_WEEKDAY], $cron[IDX_WEEKDAY])) {
240
241                                         return mktime($rtime[1], $rtime[0], 0, $rtime[3], $rtime[2], $rtime[5]);
242
243                                 } else {
244
245                                         // OK, some searching necessary...
246
247                                         $cdate  = mktime(0, 0, 0, $rtime[IDX_MONTH], $rtime[IDX_DAY], $rtime[IDX_YEAR]);
248
249                                         // OK, these three nested loops are responsible for finding the date...
250                                         //
251                                         // The class has 2 limitations/bugs right now:
252                                         //
253                                         //      -> it doesn't work for dates in 2036 or later!
254                                         //      -> it will most likely fail if you search for a Feburary, 29th with a given weekday
255                                         //         (this does happen because the class only searches in the next/last 10 years! And
256                                         //         while it usually takes less than 10 years for a "normal" date to iterate through
257                                         //         all weekdays, it can take 20+ years for Feb, 29th to iterate through all weekdays!
258
259                                         for ($nyear = $rtime[IDX_YEAR];(($next) ? ($nyear <= $rtime[IDX_YEAR] + 10) : ($nyear >= $rtime[IDX_YEAR] -10));$nyear = $nyear + (($next) ? 1 : -1)) {
260
261                                                 foreach ($cron[IDX_MONTH] as $nmonth) {
262
263                                                         foreach ($cron[IDX_DAY] as $nday) {
264
265                                                                 if (checkdate($nmonth,$nday,$nyear)) {
266
267                                                                         $ndate  = mktime(0,0,1,$nmonth,$nday,$nyear);
268
269                                                                         if (($next) ? ($ndate >= $cdate) : ($ndate <= $cdate)) {
270
271                                                                                 $dow    = date('w',$ndate);
272
273                                                                                 // The date is "OK" - lets see if the weekday matches, too...
274
275                                                                                 if (in_array($dow,$cron[IDX_WEEKDAY])) {
276
277                                                                                         // WIN! :-) We found a valid date...
278
279                                                                                         $rtime                  = explode(',', strftime('%M,%H,%d,%m,%w,%Y', mktime($rtime[IDX_HOUR], $rtime[IDX_MINUTE], 0, $nmonth, $nday, $nyear)));
280
281                                                                                         return mktime($rtime[1], $rtime[0], 0, $rtime[3], $rtime[2], $rtime[5]);
282
283                                                                                 }
284
285                                                                         }
286
287                                                                 }
288
289                                                         }
290
291                                                 }
292
293                                         }
294
295                                 }
296
297                                 throw new Exception('Failed to find date, No matching date found in a 10 years range!', 10004);
298
299                         }
300
301                         return mktime($rtime[1], $rtime[0], 0, $rtime[3], $rtime[2], $rtime[5]);
302
303                 }
304
305                 /**
306                  * getTimestamp() converts an unix-timestamp to an array. The returned array contains the following values:
307                  *
308                  *      [0]     -> minute
309                  *      [1]     -> hour
310                  *      [2]     -> day
311                  *      [3]     -> month
312                  *      [4]     -> weekday
313                  *      [5]     -> year
314                  *
315                  * The array is used by various functions.
316                  *
317                  * @access      private
318                  * @param       int             $timestamp      If none is given, the current time is used
319                  * @return      mixed
320                  */
321
322                 static private function getTimestamp($timestamp = null) {
323
324                         if (is_null($timestamp)) {
325                                 $arr    = explode(',', strftime('%M,%H,%d,%m,%w,%Y', time()));
326                         } else {
327                                 $arr    = explode(',', strftime('%M,%H,%d,%m,%w,%Y', $timestamp));
328                         }
329
330                         // Remove leading zeros (or we'll get in trouble ;-)
331
332                         foreach ($arr as $key=>$value) {
333                                 $arr[$key]      = (int)ltrim($value,'0');
334                         }
335
336                         return $arr;
337
338                 }
339
340                 /**
341                  * findValue() checks if the given value exists in an array. If it does not exist, the next
342                  * higher/lower value is returned (depending on $next). If no higher/lower value exists,
343                  * false is returned.
344                  *
345                  * @access      public
346                  * @param       int             $value
347                  * @param       mixed           $data
348                  * @param       bool            $next
349                  * @return      mixed
350                  */
351
352                 static private function findValue($value, $data, $next = true) {
353
354                         if (in_array($value, $data)) {
355
356                                 return (int)$value;
357
358                         } else {
359
360                                 if (($next) ? ($value <= end($data)) : ($value >= end($data))) {
361
362                                         foreach ($data as $curval) {
363
364                                                 if (($next) ? ($value <= (int)$curval) : ($curval <= $value)) {
365
366                                                         return (int)$curval;
367
368                                                 }
369
370                                         }
371
372                                 }
373
374                         }
375
376                         return false;
377
378                 }
379
380                 /**
381                  * getExpression() returns a parsed cron-expression. Parsed cron-expressions are cached to reduce
382                  * unneccessary calls of the parser.
383                  *
384                  * @access      public
385                  * @param       string          $value
386                  * @param       bool            $reverse
387                  * @return      mixed
388                  */
389
390                  static private function getExpression($expression, $reverse=false) {
391
392                         // First of all we cleanup the expression and remove all duplicate tabs/spaces/etc.
393                         // For example "*              * *    * *" would be converted to "* * * * *", etc.
394
395                         $expression     = preg_replace('/(\s+)/', ' ', strtolower(trim($expression)));
396
397                         // Lets see if we've already parsed that expression
398
399                         if (!isset(self::$pcron[$expression])) {
400
401                                 // Nope - parse it!
402
403                                 try {
404
405                                         self::$pcron[$expression]               = tdCronEntry::parse($expression);
406                                         self::$pcron['reverse'][$expression]    = self::arrayReverse(self::$pcron[$expression]);
407
408                                 } catch (Exception $e) {
409
410                                         throw $e;
411
412                                 }
413
414                         }
415
416                         return ($reverse ? self::$pcron['reverse'][$expression] : self::$pcron[$expression]);
417
418                 }
419
420                 /**
421                  * arrayReverse() reverses all sub-arrays of our cron array. The reversed values are used for calculations
422                  * that are run when getLastOccurence() is called.
423                  *
424                  * @access      public
425                  * @param       mixed           $cron
426                  * @return      mixed
427                  */
428
429                 static private function arrayReverse($cron) {
430
431                         foreach ($cron as $key=>$value) {
432
433                                 $cron[$key]     = array_reverse($value);
434
435                         }
436
437                         return $cron;
438
439                 }
440
441         }