1: <?php
2:
3: namespace intouch\ical;
4:
5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26:
27: class Freq {
28: protected $weekdays = array('MO'=>'monday', 'TU'=>'tuesday', 'WE'=>'wednesday', 'TH'=>'thursday', 'FR'=>'friday', 'SA'=>'saturday', 'SU'=>'sunday');
29: protected $knownRules = array('month', 'weekno', 'day', 'monthday', 'yearday', 'hour', 'minute');
30: protected $ruleModifiers = array('wkst');
31: protected $simpleMode = true;
32:
33: protected $rules = array('freq'=>'yearly', 'interval'=>1);
34: protected $start = 0;
35: protected $freq = '';
36:
37: protected $excluded;
38: protected $added;
39:
40: protected $cache;
41:
42: 43: 44: 45: 46: 47: 48:
49: public function __construct( $rule, $start, $excluded=array(), $added=array()) {
50: $this->start = $start;
51: $this->excluded = array();
52:
53: $rules = array();
54: foreach( explode(';', $rule) AS $v) {
55: list($k, $v) = explode('=', $v);
56: $this->rules[ strtolower($k) ] = $v;
57: }
58:
59: if( isset($this->rules['until']) && is_string($this->rules['until']) ) {
60: $this->rules['until'] = strtotime($this->rules['until']);
61: }
62: $this->freq = strtolower($this->rules['freq']);
63:
64: foreach( $this->knownRules AS $rule ) {
65: if( isset($this->rules['by' . $rule]) ) {
66: if( $this->isPrerule($rule, $this->freq) ) {
67: $this->simpleMode = false;
68: }
69: }
70: }
71:
72: if(!$this->simpleMode) {
73: if(! (isset($this->rules['byday']) || isset($this->rules['bymonthday']) || isset($this->rules['byyearday']))) {
74: $this->rules['bymonthday'] = date('d', $this->start);
75: }
76: }
77:
78:
79: if( isset($this->rules['count']) ) {
80:
81: $cache[$ts] = $ts = $this->start;
82: for($n=1; $n < $this->rules['count']; $n++) {
83: $ts = $this->findNext($ts);
84: $cache[$ts] = $ts;
85: }
86: $this->rules['until'] = $ts;
87:
88:
89: if (!empty($excluded)) {
90: foreach($excluded as $ts) {
91: unset($cache[$ts]);
92: }
93: }
94:
95: if (!empty($added)) {
96: $cache = $cache + $added;
97: asort($cache);
98: }
99:
100: $this->cache = array_values($cache);
101: }
102:
103: $this->excluded = $excluded;
104: $this->added = $added;
105: }
106:
107:
108: 109: 110: 111:
112: public function getAllOccurrences() {
113: if (empty($this->cache)) {
114:
115: $next = $this->firstOccurrence();
116: while ($next) {
117: $cache[] = $next;
118: $next = $this->findNext($next);
119: }
120: if (!empty($this->added)) {
121: $cache = $cache + $this->added;
122: asort($cache);
123: }
124: $this->cache = $cache;
125: }
126: return $this->cache;
127: }
128:
129: 130: 131: 132: 133: 134:
135: public function previousOccurrence( $offset ) {
136: if (!empty($this->cache)) {
137: $t2=$this->start;
138: foreach($this->cache as $ts) {
139: if ($ts >= $offset)
140: return $t2;
141: $t2 = $ts;
142: }
143: } else {
144: $ts = $this->start;
145: while( ($t2 = $this->findNext($ts)) < $offset) {
146: if( $t2 == false ){
147: break;
148: }
149: $ts = $t2;
150: }
151: }
152: return $ts;
153: }
154:
155: 156: 157: 158: 159:
160: public function nextOccurrence( $offset ) {
161: if ($offset < $this->start)
162: return $this->firstOccurrence();
163: return $this->findNext($offset);
164: }
165:
166: 167: 168: 169:
170: public function firstOccurrence() {
171: $t = $this->start;
172: if (in_array($t, $this->excluded))
173: $t = $this->findNext($t);
174: return $t;
175: }
176:
177: 178: 179: 180: 181:
182: public function lastOccurrence() {
183:
184: $this->getAllOccurrences();
185:
186: return end($this->cache);
187: }
188:
189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211:
212: public function findNext($offset) {
213: if (!empty($this->cache)) {
214: foreach($this->cache as $ts) {
215: if ($ts > $offset)
216: return $ts;
217: }
218: }
219:
220: $debug = false;
221:
222:
223: if( $offset === false || (isset($this->rules['until']) && $offset > $this->rules['until']) ) {
224: if($debug) echo 'STOP: ' . date('r', $offset) . "\n";
225: return false;
226: }
227:
228: $found = true;
229:
230:
231:
232: if($debug) echo 'O: ' . date('r', $offset) . "\n";
233: $hour = (in_array($this->freq, array('hourly','minutely')) && $offset > $this->start) ? date('H', $offset) : date('H', $this->start);
234: $minute = (($this->freq == 'minutely' || isset($this->rules['byminute'])) && $offset > $this->start) ? date('i', $offset) : date('i', $this->start);
235: $t = mktime($hour, $minute, date('s', $this->start), date('m', $offset), date('d', $offset), date('Y',$offset));
236: if($debug) echo 'START: ' . date('r', $t) . "\n";
237:
238: if( $this->simpleMode ) {
239: if( $offset < $t ) {
240: $ts = $t;
241: if ($ts && in_array($ts, $this->excluded))
242: $ts = $this->findNext($ts);
243: } else {
244: $ts = $this->findStartingPoint( $t, $this->rules['interval'], false );
245: if( !$this->validDate( $ts ) ) {
246: $ts = $this->findNext($ts);
247: }
248: }
249: return $ts;
250: }
251:
252: $eop = $this->findEndOfPeriod($offset);
253: if($debug) echo 'EOP: ' . date('r', $eop) . "\n";
254:
255: foreach( $this->knownRules AS $rule ) {
256: if( $found && isset($this->rules['by' . $rule]) ) {
257: if( $this->isPrerule($rule, $this->freq) ) {
258: $subrules = explode(',', $this->rules['by' . $rule]);
259: $_t = null;
260: foreach( $subrules AS $subrule ) {
261: $imm = call_user_func_array(array($this, 'ruleBy' . $rule), array($subrule, $t));
262: if( $imm === false ) {
263: break;
264: }
265: if($debug) echo strtoupper($rule) . ': ' . date('r', $imm) . ' A: ' . ((int) ($imm > $offset && $imm < $eop)) . "\n";
266: if( $imm > $offset && $imm < $eop && ($_t == null || $imm < $_t) ) {
267: $_t = $imm;
268: }
269: }
270: if( $_t !== null ) {
271: $t = $_t;
272: } else {
273: $found = $this->validDate($t);
274: }
275: }
276: }
277: }
278:
279: if( $offset < $this->start && $this->start < $t ) {
280: $ts = $this->start;
281: } else if( $found && ($t != $offset)) {
282: if( $this->validDate( $t ) ) {
283: if($debug) echo 'OK' . "\n";
284: $ts = $t;
285: } else {
286: if($debug) echo 'Invalid' . "\n";
287: $ts = $this->findNext($t);
288: }
289: } else {
290: if($debug) echo 'Not found' . "\n";
291: $ts = $this->findNext( $this->findStartingPoint( $offset, $this->rules['interval'] ) );
292: }
293: if ($ts && in_array($ts, $this->excluded))
294: return $this->findNext($ts);
295:
296: return $ts;
297: }
298:
299: 300: 301: 302: 303: 304: 305: 306:
307: private function findStartingPoint( $offset, $interval, $truncate = true ) {
308: $_freq = ($this->freq == 'daily') ? 'day__' : $this->freq;
309: $t = '+' . $interval . ' ' . substr($_freq,0,-2) . 's';
310: if( $_freq == 'monthly' && $truncate ) {
311: if( $interval > 1) {
312: $offset = strtotime('+' . ($interval - 1) . ' months ', $offset);
313: }
314: $t = '+' . (date('t', $offset) - date('d', $offset) + 1) . ' days';
315: }
316:
317: $sp = strtotime($t, $offset);
318:
319: if( $truncate ) {
320: $sp = $this->truncateToPeriod($sp, $this->freq);
321: }
322:
323: return $sp;
324: }
325:
326: 327: 328: 329: 330:
331: public function findEndOfPeriod($offset) {
332: return $this->findStartingPoint($offset, 1);
333: }
334:
335: 336: 337: 338: 339: 340: 341: 342: 343: 344:
345: private function truncateToPeriod( $time, $freq ) {
346: $date = getdate($time);
347: switch( $freq ) {
348: case "yearly":
349: $date['mon'] = 1;
350: case "monthly":
351: $date['mday'] = 1;
352: case "daily":
353: $date['hours'] = 0;
354: case 'hourly':
355: $date['minutes'] = 0;
356: case "minutely":
357: $date['seconds'] = 0;
358: break;
359: case "weekly":
360: if( date('N', $time) == 1) {
361: $date['hours'] = 0;
362: $date['minutes'] = 0;
363: $date['seconds'] = 0;
364: } else {
365: $date = getdate(strtotime("last monday 0:00", $time));
366: }
367: break;
368: }
369: $d = mktime($date['hours'], $date['minutes'], $date['seconds'], $date['mon'], $date['mday'], $date['year']);
370: return $d;
371: }
372:
373: 374: 375: 376: 377: 378:
379: private function ruleByday($rule, $t) {
380: $dir = ($rule{0} == '-') ? -1 : 1;
381: $dir_t = ($dir == 1) ? 'next' : 'last';
382:
383:
384: $d = $this->weekdays[substr($rule,-2)];
385: $s = $dir_t . ' ' . $d . ' ' . date('H:i:s',$t);
386:
387: if( $rule == substr($rule, -2) ) {
388: if( date('l', $t) == ucfirst($d) ) {
389: $s = 'today ' . date('H:i:s',$t);
390: }
391:
392: $_t = strtotime($s, $t);
393:
394: if( $_t == $t && in_array($this->freq, array('monthly', 'yearly')) ) {
395:
396: $s = 'next ' . $d . ' ' . date('H:i:s',$t);
397: $_t = strtotime($s, $_t);
398: }
399:
400: return $_t;
401: } else {
402: $_f = $this->freq;
403: if( isset($this->rules['bymonth']) && $this->freq == 'yearly' ) {
404: $this->freq = 'monthly';
405: }
406: if( $dir == -1 ) {
407: $_t = $this->findEndOfPeriod($t);
408: } else {
409: $_t = $this->truncateToPeriod($t, $this->freq);
410: }
411: $this->freq = $_f;
412:
413: $c = preg_replace('/[^0-9]/','',$rule);
414: $c = ($c == '') ? 1 : $c;
415:
416: $n = $_t;
417: while($c > 0 ) {
418: if( $dir == 1 && $c == 1 && date('l', $t) == ucfirst($d) ) {
419: $s = 'today ' . date('H:i:s',$t);
420: }
421: $n = strtotime($s, $n);
422: $c--;
423: }
424:
425: return $n;
426: }
427: }
428:
429: private function ruleBymonth($rule, $t) {
430: $_t = mktime(date('H',$t), date('i',$t), date('s',$t), $rule, date('d', $t), date('Y', $t));
431: if( $t == $_t && isset($this->rules['byday']) ) {
432:
433: return false;
434: } else {
435: return $_t;
436: }
437: }
438:
439: private function ruleBymonthday($rule, $t) {
440: if( $rule < 0 ) {
441: $rule = date('t', $t) + $rule + 1;
442: }
443: return mktime(date('H',$t), date('i',$t), date('s',$t), date('m', $t), $rule, date('Y', $t));
444: }
445:
446: private function ruleByyearday($rule, $t) {
447: if( $rule < 0 ) {
448: $_t = $this->findEndOfPeriod();
449: $d = '-';
450: } else {
451: $_t = $this->truncateToPeriod($t, $this->freq);
452: $d = '+';
453: }
454: $s = $d . abs($rule -1) . ' days ' . date('H:i:s',$t);
455: return strtotime($s, $_t);
456: }
457:
458: private function ruleByweekno($rule, $t) {
459: if( $rule < 0 ) {
460: $_t = $this->findEndOfPeriod();
461: $d = '-';
462: } else {
463: $_t = $this->truncateToPeriod($t, $this->freq);
464: $d = '+';
465: }
466:
467: $sub = (date('W', $_t) == 1) ? 2 : 1;
468: $s = $d . abs($rule - $sub) . ' weeks ' . date('H:i:s',$t);
469: $_t = strtotime($s, $_t);
470:
471: return $_t;
472: }
473:
474: private function ruleByhour($rule, $t) {
475: $_t = mktime($rule, date('i',$t), date('s',$t), date('m',$t), date('d', $t), date('Y', $t));
476: return $_t;
477: }
478:
479: private function ruleByminute($rule, $t) {
480: $_t = mktime(date('h',$t), $rule, date('s',$t), date('m',$t), date('d', $t), date('Y', $t));
481: return $_t;
482: }
483:
484: private function validDate( $t ) {
485: if( isset($this->rules['until']) && $t > $this->rules['until'] ) {
486: return false;
487: }
488:
489: if (in_array($t, $this->excluded)) {
490: return false;
491: }
492:
493: if( isset($this->rules['bymonth']) ) {
494: $months = explode(',', $this->rules['bymonth']);
495: if( !in_array(date('m', $t), $months)) {
496: return false;
497: }
498: }
499: if( isset($this->rules['byday']) ) {
500: $days = explode(',', $this->rules['byday']);
501: foreach( $days As $i => $k ) {
502: $days[$i] = $this->weekdays[ preg_replace('/[^A-Z]/', '', $k)];
503: }
504: if( !in_array(strtolower(date('l', $t)), $days)) {
505: return false;
506: }
507: }
508: if( isset($this->rules['byweekno']) ) {
509: $weeks = explode(',', $this->rules['byweekno']);
510: if( !in_array(date('W', $t), $weeks)) {
511: return false;
512: }
513: }
514: if( isset($this->rules['bymonthday'])) {
515: $weekdays = explode(',', $this->rules['bymonthday']);
516: foreach( $weekdays As $i => $k ) {
517: if( $k < 0 ) {
518: $weekdays[$i] = date('t', $t) + $k + 1;
519: }
520: }
521: if( !in_array(date('d', $t), $weekdays)) {
522: return false;
523: }
524: }
525: if( isset($this->rules['byhour']) ) {
526: $hours = explode(',', $this->rules['byhour']);
527: if( !in_array(date('H', $t), $hours)) {
528: return false;
529: }
530: }
531:
532: return true;
533: }
534:
535: private function isPrerule($rule, $freq) {
536: if( $rule == 'year')
537: return false;
538: if( $rule == 'month' && $freq == 'yearly')
539: return true;
540: if( $rule == 'monthday' && in_array($freq, array('yearly', 'monthly')) && !isset($this->rules['byday']))
541: return true;
542:
543: if( $rule == 'yearday' && $freq == 'yearly' )
544: return true;
545: if( $rule == 'weekno' && $freq == 'yearly' )
546: return true;
547: if( $rule == 'day' && in_array($freq, array('yearly', 'monthly', 'weekly')))
548: return true;
549: if( $rule == 'hour' && in_array($freq, array('yearly', 'monthly', 'weekly', 'daily')))
550: return true;
551: if( $rule == 'minute' )
552: return true;
553:
554: return false;
555: }
556: }
557: