Overview

Namespaces

  • intouch
    • ical

Classes

  • Duration
  • Factory
  • Freq
  • iCal
  • Line
  • Parser
  • Query
  • Recurrence
  • VCalendar
  • VEvent
  • VTimeZone
  • Overview
  • Namespace
  • Class
  • Tree
  1: <?php // BUILD: Remove line
  2: 
  3: namespace intouch\ical;
  4: 
  5: /**
  6:  * A class to store Frequency-rules in. Will allow a easy way to find the
  7:  * last and next occurrence of the rule.
  8:  *
  9:  * No - this is so not pretty. But.. ehh.. You do it better, and I will
 10:  * gladly accept patches.
 11:  *
 12:  * Created by trail-and-error on the examples given in the RFC.
 13:  *
 14:  * TODO: Update to a better way of doing calculating the different options.
 15:  * Instead of only keeping track of the best of the current dates found
 16:  * it should instead keep a array of all the calculated dates within the
 17:  * period.
 18:  * This should fix the issues with multi-rule + multi-rule interference,
 19:  * and make it possible to implement the SETPOS rule.
 20:  * By pushing the next period onto the stack as the last option will
 21:  * (hopefully) remove the need for the awful simpleMode
 22:  *
 23:  * @author Morten Fangel (C) 2008
 24:  * @author Michael Kahn (C) 2013
 25:  * @license http://creativecommons.org/licenses/by-sa/2.5/dk/deed.en_GB CC-BY-SA-DK
 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'); //others : 'setpos', 'second'
 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; //EXDATE
 38:     protected $added;    //RDATE
 39: 
 40:     protected $cache; // getAllOccurrences()
 41: 
 42:     /**
 43:      * Constructs a new Freqency-rule
 44:      * @param $rule string
 45:      * @param $start int Unix-timestamp (important : Need to be the start of Event)
 46:      * @param $excluded array of int (timestamps), see EXDATE documentation
 47:      * @param $added array of int (timestamps), see RDATE documentation
 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:         //set until, and cache
 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:             //EXDATE
 89:             if (!empty($excluded)) {
 90:                 foreach($excluded as $ts) {
 91:                     unset($cache[$ts]);
 92:                 }
 93:             }
 94:             //RDATE
 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:      * Returns all timestamps array(), build the cache if not made before
110:      * @return array
111:      */
112:     public function getAllOccurrences() {
113:         if (empty($this->cache)) {
114:             //build cache
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:      * Returns the previous (most recent) occurrence of the rule from the
131:      * given offset
132:      * @param int $offset
133:      * @return int
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:      * Returns the next occurrence of this rule after the given offset
157:      * @param int $offset
158:      * @return int
159:      */
160:     public function nextOccurrence( $offset ) {
161:         if ($offset < $this->start)
162:             return $this->firstOccurrence();
163:         return $this->findNext($offset);
164:     }
165: 
166:     /**
167:      * Finds the first occurrence of the rule.
168:      * @return int timestamp
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:      * Finds the absolute last occurrence of the rule from the given offset.
179:      * Builds also the cache, if not set before...
180:      * @return int timestamp
181:      */
182:     public function lastOccurrence() {
183:         //build cache if not done
184:         $this->getAllOccurrences();
185:         //return last timestamp in cache
186:         return end($this->cache);
187:     }
188: 
189:     /**
190:      * Calculates the next time after the given offset that the rule
191:      * will apply.
192:      *
193:      * The approach to finding the next is as follows:
194:      * First we establish a timeframe to find timestamps in. This is
195:      * between $offset and the end of the period that $offset is in.
196:      *
197:      * We then loop though all the rules (that is a Prerule in the
198:      * current freq.), and finds the smallest timestamp inside the
199:      * timeframe.
200:      *
201:      * If we find something, we check if the date is a valid recurrence
202:      * (with validDate). If it is, we return it. Otherwise we try to
203:      * find a new date inside the same timeframe (but using the new-
204:      * found date as offset)
205:      *
206:      * If no new timestamps were found in the period, we try in the
207:      * next period
208:      *
209:      * @param int $offset
210:      * @return int
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:         //make sure the offset is valid
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:         //set the timestamp of the offset (ignoring hours and minutes unless we want them to be
231:         //part of the calculations.
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:      * Finds the starting point for the next rule. It goes $interval
301:      * 'freq' forward in time since the given offset
302:      * @param int $offset
303:      * @param int $interval
304:      * @param boolean $truncate
305:      * @return int
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:      * Finds the earliest timestamp posible outside this perioid
328:      * @param int $offset
329:      * @return int
330:      */
331:     public function findEndOfPeriod($offset) {
332:         return $this->findStartingPoint($offset, 1);
333:     }
334: 
335:     /**
336:      * Resets the timestamp to the beginning of the
337:      * period specified by freq
338:      *
339:      * Yes - the fall-through is on purpose!
340:      *
341:      * @param int $time
342:      * @param int $freq
343:      * @return int
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:      * Applies the BYDAY rule to the given timestamp
375:      * @param string $rule
376:      * @param int $t
377:      * @return int
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:                 // Yes. This is not a great idea.. but hey, it works.. for now
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:             // TODO: this should check if one of the by*day's exists, and have a multi-day value
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:         // TODO: is it faster to do monthday first, and ignore day if monthday exists? - prolly by a factor of 4..
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: 
intouch-iCalendar API documentation generated by ApiGen 2.8.0