Completed
Push — 16.1 ( d53335...4ee931 )
by Nathan
93:05 queued 42:29
created

calendar_rrule::from_string()   D

Complexity

Conditions 19
Paths 240

Size

Total Lines 68
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
eloc 37
nc 240
nop 2
dl 0
loc 68
rs 4.4635
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * eGroupWare - Calendar recurrence rules
4
 *
5
 * @link http://www.egroupware.org
6
 * @package calendar
7
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
8
 * @author Joerg Lehrke <[email protected]>
9
 * @copyright (c) 2009-16 by RalfBecker-At-outdoor-training.de
10
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
11
 * @version $Id$
12
 */
13
14
use EGroupware\Api;
15
16
/**
17
 * Recurrence rule iterator
18
 *
19
 * The constructor accepts times only as DateTime (or decendents like egw_date) to work timezone-correct.
20
 * The timezone of the event is determined by timezone of the startime, other times get converted to that timezone.
21
 *
22
 * There's a static factory method calendar_rrule::event2rrule(array $event,$usertime=true), which converts an
23
 * event read by calendar_bo::read() or calendar_bo::search() to a rrule iterator.
24
 *
25
 * The rrule iterator object can be casted to string, to get a human readable description of the rrule.
26
 *
27
 * There's an interactive test-form, if the class get's called directly: http://localhost/egroupware/calendar/inc/class.calendar_rrule.inc.php
28
 *
29
 * @todo Integrate iCal import and export, so all recurrence code resides just in this class
30
 * @todo Implement COUNT, can be stored in enddate assuming counts are far smaller then timestamps (eg. < 1000 is a count)
31
 * @todo Implement WKST (week start day), currently WKST=SU is used (this is not stored in current DB schema, it's a user preference)
32
 */
33
class calendar_rrule implements Iterator
34
{
35
	/**
36
	 * No recurrence
37
	 */
38
	const NONE = 0;
39
	/**
40
	 * Daily recurrence
41
	 */
42
	const DAILY = 1;
43
	/**
44
	 * Weekly recurrance on day(s) specified by bitfield in $data
45
	 */
46
	const WEEKLY = 2;
47
	/**
48
	 * Monthly recurrance iCal: monthly_bymonthday
49
	 */
50
	const MONTHLY_MDAY = 3;
51
	/**
52
	 * Monthly recurrance iCal: BYDAY (by weekday, eg. 1st Friday of month)
53
	 */
54
	const MONTHLY_WDAY = 4;
55
	/**
56
	 * Yearly recurrance
57
	 */
58
	const YEARLY = 5;
59
	/**
60
	 * Translate recure types to labels
61
	 *
62
	 * @var array
63
	 */
64
	static public $types = Array(
65
		self::NONE         => 'None',
66
		self::DAILY        => 'Daily',
67
		self::WEEKLY       => 'Weekly',
68
		self::MONTHLY_WDAY => 'Monthly (by day)',
69
		self::MONTHLY_MDAY => 'Monthly (by date)',
70
		self::YEARLY       => 'Yearly'
71
	);
72
73
	/**
74
	 * @var array $recur_egw2ical_2_0 converstaion of egw recur-type => ical FREQ
75
	 */
76
	static private $recur_egw2ical_2_0 = array(
77
		self::DAILY        => 'DAILY',
78
		self::WEEKLY       => 'WEEKLY',
79
		self::MONTHLY_WDAY => 'MONTHLY',	// BYDAY={1..7, -1}{MO..SO, last workday}
80
		self::MONTHLY_MDAY => 'MONTHLY',	// BYMONHTDAY={1..31, -1 for last day of month}
81
		self::YEARLY       => 'YEARLY',
82
	);
83
84
	/**
85
	 * @var array $recur_egw2ical_1_0 converstaion of egw recur-type => ical FREQ
86
	 */
87
	static private $recur_egw2ical_1_0 = array(
88
		self::DAILY        => 'D',
89
		self::WEEKLY       => 'W',
90
		self::MONTHLY_WDAY => 'MP',	// BYDAY={1..7,-1}{MO..SO, last workday}
91
		self::MONTHLY_MDAY => 'MD',	// BYMONHTDAY={1..31,-1}
92
		self::YEARLY       => 'YM',
93
	);
94
95
	/**
96
	 * RRule type: NONE, DAILY, WEEKLY, MONTHLY_MDAY, MONTHLY_WDAY, YEARLY
97
	 *
98
	 * @var int
99
	 */
100
	public $type = self::NONE;
101
102
	/**
103
	 * Interval
104
	 *
105
	 * @var int
106
	 */
107
	public $interval = 1;
108
109
	/**
110
	 * Number for monthly byday: 1, ..., 5, -1=last weekday of month
111
	 *
112
	 * EGroupware Calendar does NOT explicitly store it, it's only implicitly defined by series start date
113
	 *
114
	 * @var int
115
	 */
116
	public $monthly_byday_num;
117
118
	/**
119
	 * Number for monthly bymonthday: 1, ..., 31, -1=last day of month
120
	 *
121
	 * EGroupware Calendar does NOT explicitly store it, it's only implicitly defined by series start date
122
	 *
123
	 * @var int
124
	 */
125
	public $monthly_bymonthday;
126
127
	/**
128
	 * Enddate of recurring event or null, if not ending
129
	 *
130
	 * @var DateTime
131
	 */
132
	public $enddate;
133
134
	/**
135
	 * Enddate of recurring event, as Ymd integer (eg. 20091111)
136
	 *
137
	 * Or 5 years in future, if no enddate. So iterator is always limited.
138
	 *
139
	 * @var int
140
	 */
141
	public $enddate_ymd;
142
143
	/**
144
	 * Enddate of recurring event, as timestamp
145
	 *
146
	 * Or 5 years in future, if no enddate. So iterator is always limited.
147
	 *
148
	 * @var int
149
	 */
150
	public $enddate_ts;
151
152
	const SUNDAY    = 1;
153
	const MONDAY    = 2;
154
	const TUESDAY   = 4;
155
	const WEDNESDAY = 8;
156
	const THURSDAY  = 16;
157
	const FRIDAY    = 32;
158
	const SATURDAY  = 64;
159
	const WORKDAYS  = 62;	// Mo, ..., Fr
160
	const ALLDAYS   = 127;
161
	/**
162
	 * Translate weekday bitmasks to labels
163
	 *
164
	 * @var array
165
	 */
166
	static public $days = array(
167
		self::MONDAY    => 'Monday',
168
		self::TUESDAY   => 'Tuesday',
169
		self::WEDNESDAY => 'Wednesday',
170
		self::THURSDAY  => 'Thursday',
171
		self::FRIDAY    => 'Friday',
172
		self::SATURDAY  => 'Saturday',
173
		self::SUNDAY    => 'Sunday',
174
	);
175
	/**
176
	 * Bitmask of valid weekdays for weekly repeating events: self::SUNDAY|...|self::SATURDAY
177
	 *
178
	 * @var integer
179
	 */
180
	public $weekdays;
181
182
	/**
183
	 * Array of exception dates (Ymd strings)
184
	 *
185
	 * @var array
186
	 */
187
	public $exceptions=array();
188
189
	/**
190
	 * Array of exceptions as DateTime/egw_time objects
191
	 *
192
	 * @var array
193
	 */
194
	public $exceptions_objs=array();
195
196
	/**
197
	 * Starttime of series
198
	 *
199
	 * @var Api\DateTime
200
	 */
201
	public $time;
202
203
	/**
204
	 * Current "position" / time
205
	 *
206
	 * @var Api\DateTime
207
	 */
208
	public $current;
209
210
	/**
211
	 * Last day of the week according to user preferences
212
	 *
213
	 * @var int
214
	 */
215
	protected $lastdayofweek;
216
217
	/**
218
	 * Cached timezone data
219
	 *
220
	 * @var array id => data
221
	 */
222
	protected static $tz_cache = array();
223
224
	/**
225
	 * Constructor
226
	 *
227
	 * The constructor accepts on DateTime (or decendents like egw_date) for all times, to work timezone-correct.
228
	 * The timezone of the event is determined by timezone of $time, other times get converted to that timezone.
229
	 *
230
	 * @param DateTime $time start of event in it's own timezone
231
	 * @param int $type self::NONE, self::DAILY, ..., self::YEARLY
232
	 * @param int $interval =1 1, 2, ...
233
	 * @param DateTime $enddate =null enddate or null for no enddate (in which case we user '+5 year' on $time)
234
	 * @param int $weekdays =0 self::SUNDAY=1|self::MONDAY=2|...|self::SATURDAY=64
235
	 * @param array $exceptions =null DateTime objects with exceptions
236
	 */
237
	public function __construct(DateTime $time,$type,$interval=1,DateTime $enddate=null,$weekdays=0,array $exceptions=null)
238
	{
239
		switch($GLOBALS['egw_info']['user']['preferences']['calendar']['weekdaystarts'])
240
		{
241
			case 'Sunday':
242
				$this->lastdayofweek = self::SATURDAY;
243
				break;
244
			case 'Saturday':
245
				$this->lastdayofweek = self::FRIDAY;
246
				break;
247
			default: // Monday
248
				$this->lastdayofweek = self::SUNDAY;
249
		}
250
251
		$this->time = $time instanceof Api\DateTime ? $time : new Api\DateTime($time);
252
253
		if (!in_array($type,array(self::NONE, self::DAILY, self::WEEKLY, self::MONTHLY_MDAY, self::MONTHLY_WDAY, self::YEARLY)))
254
		{
255
			throw new Api\Exception\WrongParameter(__METHOD__."($time,$type,$interval,$enddate,$weekdays,...) type $type is NOT valid!");
256
		}
257
		$this->type = $type;
258
259
		// determine only implicit defined rules for RRULE=MONTHLY,BYDAY={-1, 1, ..., 5}{MO,..,SU}
260
		if ($type == self::MONTHLY_WDAY)
261
		{
262
			// check for last week of month
263
			if (($day = $this->time->format('d')) >= 21 && $day > self::daysInMonth($this->time)-7)
264
			{
265
				$this->monthly_byday_num = -1;
266
			}
267
			else
268
			{
269
				$this->monthly_byday_num = 1 + floor(($this->time->format('d')-1) / 7);
0 ignored issues
show
Documentation Bug introduced by
The property $monthly_byday_num was declared of type integer, but 1 + floor(($this->time->format('d') - 1) / 7) is of type double. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
270
			}
271
		}
272
		elseif($type == self::MONTHLY_MDAY)
273
		{
274
			$this->monthly_bymonthday = (int)$this->time->format('d');
275
			// check for last day of month
276
			if ($this->monthly_bymonthday >= 28)
277
			{
278
				$test = clone $this->time;
279
				$test->modify('1 day');
280
				if ($test->format('m') != $this->time->format('m'))
281
				{
282
					$this->monthly_bymonthday = -1;
283
				}
284
			}
285
		}
286
287
		if ((int)$interval < 1)
288
		{
289
			$interval = 1;	// calendar stores no (extra) interval as null, so using default 1 here
290
		}
291
		$this->interval = (int)$interval;
292
293
		$this->enddate = $enddate;
294
		// no recurrence --> current date is enddate
295
		if ($type == self::NONE)
296
		{
297
			$enddate = clone $this->time;
298
		}
299
		// set a maximum of 5 years if no enddate given
300
		elseif (is_null($enddate))
301
		{
302
			$enddate = clone $this->time;
303
			$enddate->modify('5 year');
304
		}
305
		// convert enddate to timezone of time, if necessary
306
		else
307
		{
308
			$enddate->setTimezone($this->time->getTimezone());
309
		}
310
		$this->enddate_ymd = (int)$enddate->format('Ymd');
311
		$this->enddate_ts = $enddate->format('ts');
312
313
		// if no valid weekdays are given for weekly repeating, we use just the current weekday
314
		if (!($this->weekdays = (int)$weekdays) && ($type == self::WEEKLY || $type == self::MONTHLY_WDAY))
315
		{
316
			$this->weekdays = self::getWeekday($this->time);
317
		}
318
		if ($exceptions)
319
		{
320
			foreach($exceptions as $exception)
321
			{
322
				$exception->setTimezone($this->time->getTimezone());
323
				$this->exceptions[] = $exception->format('Ymd');
324
			}
325
			$this->exceptions_objs = $exceptions;
326
		}
327
	}
328
329
	/**
330
	 * Get recurrence interval duration in seconds
331
	 *
332
	 * @param int $type self::(DAILY|WEEKLY|MONTHLY_(M|W)DAY|YEARLY)
333
	 * @param int $interval =1
334
	 * @return int
335
	 */
336
	public static function recurrence_interval($type, $interval=1)
337
	{
338
		switch($type)
339
		{
340
			case self::DAILY:
341
				$duration = 24*3600;
342
				break;
343
			case self::WEEKLY:
344
				$duration = 7*24*3600;
345
				break;
346
			case self::MONTHLY_MDAY:
347
			case self::MONTHLY_WDAY:
348
				$duration = 31*24*3600;
349
				break;
350
			case self::YEARLY:
351
				$duration = 366*24*3600;
352
				break;
353
		}
354
		if ($interval > 1) $duration *= $interval;
355
356
		return $duration;
357
	}
358
359
	/**
360
	 * Get number of days in month of given date
361
	 *
362
	 * @param DateTime $time
363
	 * @return int
364
	 */
365
	private static function daysInMonth(DateTime $time)
366
	{
367
		list($year,$month) = explode('-',$time->format('Y-m'));
368
		$last_day = new Api\DateTime();
369
		$last_day->setDate($year,$month+1,0);
370
371
		return (int)$last_day->format('d');
372
	}
373
374
	/**
375
	 * Return the current element
376
	 *
377
	 * @return DateTime
378
	 */
379
	public function current()
380
	{
381
		return clone $this->current;
382
	}
383
384
	/**
385
	 * Return the key of the current element, we use a Ymd integer as key
386
	 *
387
	 * @return int
388
	 */
389
	public function key()
390
	{
391
		return (int)$this->current->format('Ymd');
392
	}
393
394
	/**
395
	 * Move forward to next recurence, not caring for exceptions
396
	 */
397
	public function next_no_exception()
398
	{
399
		switch($this->type)
400
		{
401
			case self::NONE:	// need to add at least one day, to end "series", as enddate == current date
402
			case self::DAILY:
403
				$this->current->modify($this->interval.' day');
404
				break;
405
406
			case self::WEEKLY:
407
				// advance to next valid weekday
408
				do
409
				{
410
					// interval in weekly means event runs on valid days eg. each 2. week
411
					// --> on the last day of the week we have to additionally advance interval-1 weeks
412
					if ($this->interval > 1 && self::getWeekday($this->current) == $this->lastdayofweek)
413
					{
414
						$this->current->modify(($this->interval-1).' week');
415
					}
416
					$this->current->modify('1 day');
417
					//echo __METHOD__.'() '.$this->current->format('l').', '.$this->current.": $this->weekdays & ".self::getWeekday($this->current)."<br />\n";
418
				}
419
				while(!($this->weekdays & self::getWeekday($this->current)));
420
				break;
421
422
			case self::MONTHLY_WDAY:	// iCal: BYDAY={1, ..., 5, -1}{MO..SO}
423
				// advance to start of next month
424
				list($year,$month) = explode('-',$this->current->format('Y-m'));
425
				$month += $this->interval+($this->monthly_byday_num < 0 ? 1 : 0);
426
				$this->current->setDate($year,$month,$this->monthly_byday_num < 0 ? 0 : 1);
427
				//echo __METHOD__."() $this->monthly_byday_num".substr(self::$days[$this->monthly_byday_wday],0,2).": setDate($year,$month,1): ".$this->current->format('l').', '.$this->current."<br />\n";
428
				// now advance to n-th week
429
				if ($this->monthly_byday_num > 1)
430
				{
431
					$this->current->modify(($this->monthly_byday_num-1).' week');
432
					//echo __METHOD__."() $this->monthly_byday_num".substr(self::$days[$this->monthly_byday_wday],0,2).': modify('.($this->monthly_byday_num-1).' week): '.$this->current->format('l').', '.$this->current."<br />\n";
433
				}
434
				// advance to given weekday
435
				while(!($this->weekdays & self::getWeekday($this->current)))
436
				{
437
					$this->current->modify(($this->monthly_byday_num < 0 ? -1 : 1).' day');
438
					//echo __METHOD__."() $this->monthly_byday_num".substr(self::$days[$this->monthly_byday_wday],0,2).': modify(1 day): '.$this->current->format('l').', '.$this->current."<br />\n";
439
				}
440
				break;
441
442
			case self::MONTHLY_MDAY:	// iCal: monthly_bymonthday={1, ..., 31, -1}
443
				list($year,$month) = explode('-',$this->current->format('Y-m'));
444
				$day = $this->monthly_bymonthday+($this->monthly_bymonthday < 0 ? 1 : 0);
445
				$month += $this->interval+($this->monthly_bymonthday < 0 ? 1 : 0);
446
				$this->current->setDate($year,$month,$day);
447
				//echo __METHOD__."() setDate($year,$month,$day): ".$this->current->format('l').', '.$this->current."<br />\n";
448
				break;
449
450
			case self::YEARLY:
451
				$this->current->modify($this->interval.' year');
452
				break;
453
454
			default:
455
				throw new Api\Exception\AssertionFailed(__METHOD__."() invalid type #$this->type !");
456
		}
457
	}
458
459
	/**
460
	 * Move forward to next recurence, taking into account exceptions
461
	 */
462
	public function next()
463
	{
464
		do
465
		{
466
			$this->next_no_exception();
467
		}
468
		while($this->exceptions && in_array($this->current->format('Ymd'),$this->exceptions));
469
	}
470
471
	/**
472
	 * Get weekday of $time as self::SUNDAY=1, ..., self::SATURDAY=64 integer mask
473
	 *
474
	 * @param DateTime $time
475
	 * @return int self::SUNDAY=1, ..., self::SATURDAY=64
476
	 */
477
	static protected function getWeekday(DateTime $time)
478
	{
479
		//echo __METHOD__.'('.$time->format('l').' '.$time.') 1 << '.$time->format('w').' = '.(1 << (int)$time->format('w'))."<br />\n";
480
		return 1 << (int)$time->format('w');
481
	}
482
483
	/**
484
	 * Get datetime of n-th event, 1. is original event-time
485
	 *
486
	 * This is identical on COUNT parameter of RRULE is evaluated, exceptions are NOT taken into account!
487
	 *
488
	 * @param int $count
489
	 * @return DateTime
490
	 */
491
	public function count2date($count)
492
	{
493
		if ($count <= 1)
494
		{
495
			return clone $this->time;
496
		}
497
		if (isset($this->current)) $backup = $this->current;
498
		$this->rewind();
499
500
		while(--$count > 0)
501
		{
502
			$this->next_no_exception();
503
		}
504
		$ret = clone $this->current;
505
		if ($backup) $this->current = $backup;
506
		return $ret;
507
	}
508
509
	/**
510
	 * Fix enddates which are not on a recurrence, eg. for a on Monday recurring weekly event a Tuesday
511
	 *
512
	 * @return DateTime
513
	 */
514
	public function normalize_enddate()
515
	{
516
		$this->rewind();
517
		while ($this->current < $this->enddate)
518
		{
519
			$previous = clone $this->current;
520
			$this->next_no_exception();
521
		}
522
		// if enddate is now before next acurrence, but not on same day, we use previous recurrence
523
		// this can happen if client gives an enddate which is NOT a recurrence date
524
		// eg. for a on Monday recurring weekly event a Tuesday as enddate
525
		if ($this->enddate < $this->current  && $this->current->format('Ymd') != $this->enddate->format('Ymd'))
526
		{
527
			$last = $previous;
528
		}
529
		else
530
		{
531
			$last = clone $this->current;
532
		}
533
		return $last;
534
	}
535
536
	/**
537
	 * Rewind the Iterator to the first element (called at beginning of foreach loop)
538
	 */
539
	public function rewind()
540
	{
541
		$this->current = clone $this->time;
542
		while ($this->valid() &&
543
			$this->exceptions &&
544
			in_array($this->current->format('Ymd'),$this->exceptions))
545
		{
546
			$this->next_no_exception();
547
		}
548
	}
549
550
	/**
551
	 * Checks if current position is valid
552
	 *
553
	 * @param boolean $use_just_date =false default use also time
554
	 * @return boolean
555
	 */
556
	public function valid($use_just_date=false)
557
	{
558
		if ($use_just_date)
559
		{
560
			return $this->current->format('Ymd') <= $this->enddate_ymd;
561
		}
562
		return $this->current->format('ts') < $this->enddate_ts;
563
	}
564
565
	/**
566
	 * Return string represenation of RRule
567
	 *
568
	 * @return string
569
	 */
570
	function __toString( )
571
	{
572
		$str = '';
573
		// Repeated Events
574
		if($this->type != self::NONE)
575
		{
576
			list($str) = explode(' (',lang(self::$types[$this->type]));	// remove (by day/date) from Monthly
577
578
			$str_extra = array();
579
			switch ($this->type)
580
			{
581
				case self::MONTHLY_MDAY:
582
					$str_extra[] = ($this->monthly_bymonthday == -1 ? lang('last') : $this->monthly_bymonthday.'.').' '.lang('day');
583
					break;
584
585
				case self::WEEKLY:
586
				case self::MONTHLY_WDAY:
587
					$repeat_days = array();
588
					if ($this->weekdays == self::ALLDAYS)
589
					{
590
						$repeat_days[] = $this->type == self::WEEKLY ? lang('all') : lang('day');
591
					}
592
					elseif($this->weekdays == self::WORKDAYS)
593
					{
594
						$repeat_days[] = $this->type == self::WEEKLY ? lang('workdays') : lang('workday');
595
					}
596
					else
597
					{
598
						foreach (self::$days as $mask => $label)
599
						{
600
							if ($this->weekdays & $mask)
601
							{
602
								$repeat_days[] = lang($label);
603
							}
604
						}
605
					}
606
					if($this->type == self::WEEKLY && count($repeat_days))
607
					{
608
						$str_extra[] = lang('days repeated').': '.implode(', ',$repeat_days);
609
					}
610
					elseif($this->type == self::MONTHLY_WDAY)
611
					{
612
						$str_extra[] = ($this->monthly_byday_num == -1 ? lang('last') : $this->monthly_byday_num.'.').' '.implode(', ',$repeat_days);
613
					}
614
					break;
615
616
			}
617
			if($this->interval > 1)
618
			{
619
				$str_extra[] = lang('Interval').': '.$this->interval;
620
			}
621
			if ($this->enddate)
622
			{
623
				if ($this->enddate->getTimezone()->getName() != Api\DateTime::$user_timezone->getName())
624
				{
625
					$this->enddate->setTimezone(Api\DateTime::$user_timezone);
626
				}
627
				$str_extra[] = lang('ends').': '.lang($this->enddate->format('l')).', '.$this->enddate->format(Api\DateTime::$user_dateformat);
628
			}
629
			if ($this->time->getTimezone()->getName() != Api\DateTime::$user_timezone->getName())
630
			{
631
				$str_extra[] = $this->time->getTimezone()->getName();
632
			}
633
			if(count($str_extra))
634
			{
635
				$str .= ' ('.implode(', ',$str_extra).')';
636
			}
637
		}
638
		return $str;
639
	}
640
641
	/**
642
	 * Generate a VEVENT RRULE
643
	 * @param string $version ='2.0' could be '1.0' too
644
	 *
645
	 * $return array	vCalendar RRULE
646
	 */
647
	public function generate_rrule($version='2.0')
648
	{
649
		$repeat_days = array();
650
		$rrule = array();
651
652
		if ($this->type == self::NONE) return false;	// no recuring event
653
654
		if ($version == '1.0')
655
		{
656
			$rrule['FREQ'] = self::$recur_egw2ical_1_0[$this->type] . $this->interval;
657
			switch ($this->type)
658
			{
659
				case self::WEEKLY:
660 View Code Duplication
					foreach (self::$days as $mask => $label)
661
					{
662
						if ($this->weekdays & $mask)
663
						{
664
							$repeat_days[] = strtoupper(substr($label,0,2));
665
						}
666
					}
667
					$rrule['BYDAY'] = implode(' ', $repeat_days);
668
					$rrule['FREQ'] = $rrule['FREQ'].' '.$rrule['BYDAY'];
669
					break;
670
671
				case self::MONTHLY_MDAY:	// date of the month: BYMONTDAY={1..31}
672
					break;
673
674
				case self::MONTHLY_WDAY:	// weekday of the month: BDAY={1..5}+ {MO..SO}
675
					$rrule['BYDAY'] = abs($this->monthly_byday_num);
676
					$rrule['BYDAY'] .= ($this->monthly_byday_num < 0) ? '- ' : '+ ';
677
					$rrule['BYDAY'] .= strtoupper(substr($this->time->format('l'),0,2));
678
					$rrule['FREQ'] = $rrule['FREQ'].' '.$rrule['BYDAY'];
679
					break;
680
			}
681
682
			if (!$this->enddate)
683
			{
684
				$rrule['UNTIL'] = '#0';
685
			}
686
		}
687
		else // $version == '2.0'
688
		{
689
			$rrule['FREQ'] = self::$recur_egw2ical_2_0[$this->type];
690
			switch ($this->type)
691
			{
692
				case self::WEEKLY:
693 View Code Duplication
					foreach (self::$days as $mask => $label)
694
					{
695
						if ($this->weekdays & $mask)
696
						{
697
							$repeat_days[] = strtoupper(substr($label,0,2));
698
						}
699
					}
700
					$rrule['BYDAY'] = implode(',', $repeat_days);
701
					break;
702
703
				case self::MONTHLY_MDAY:	// date of the month: BYMONTDAY={1..31}
704
					$rrule['BYMONTHDAY'] = $this->monthly_bymonthday;
705
					break;
706
707
				case self::MONTHLY_WDAY:	// weekday of the month: BDAY={1..5}{MO..SO}
708
					$rrule['BYDAY'] = $this->monthly_byday_num .
709
						strtoupper(substr($this->time->format('l'),0,2));
710
					break;
711
			}
712
			if ($this->interval > 1)
713
			{
714
				$rrule['INTERVAL'] = $this->interval;
715
			}
716
		}
717
718
		if ($this->enddate)
719
		{
720
			// our enddate is the end-time, not start-time of last event!
721
			$this->rewind();
722
			$enddate = $this->current();
723
			do
724
			{
725
				$this->next_no_exception();
726
				$occurrence = $this->current();
727
			}
728
			while ($this->valid() && ($enddate = $occurrence));
729
			$rrule['UNTIL'] = $enddate;
730
		}
731
732
		return $rrule;
733
	}
734
735
	/**
736
	 * Get instance for a given event array
737
	 *
738
	 * @param array $event
739
	 * @param boolean $usertime =true true: event timestamps are usertime (default for calendar_bo::(read|search), false: servertime
740
	 * @param string $to_tz			timezone for exports (null for event's timezone)
741
	 *
742
	 * @return calendar_rrule		false on error
743
	 */
744
	public static function event2rrule(array $event,$usertime=true,$to_tz=null)
745
	{
746
		if (!is_array($event)  || !isset($event['tzid'])) return false;
747
		if (!$to_tz) $to_tz = $event['tzid'];
748
		$timestamp_tz = $usertime ? Api\DateTime::$user_timezone : Api\DateTime::$server_timezone;
749
		$time = is_a($event['start'],'DateTime') ? $event['start'] : new Api\DateTime($event['start'],$timestamp_tz);
750
751
		if (!isset(self::$tz_cache[$to_tz]))
752
		{
753
			self::$tz_cache[$to_tz] = calendar_timezones::DateTimeZone($to_tz);
754
		}
755
756
		self::rrule2tz($event, $time, $to_tz);
757
758
		$time->setTimezone(self::$tz_cache[$to_tz]);
759
760
		if ($event['recur_enddate'])
761
		{
762
			$enddate = is_a($event['recur_enddate'],'DateTime') ? $event['recur_enddate'] : new Api\DateTime($event['recur_enddate'],self::$tz_cache[$to_tz]);
763
			$enddate->setTime(23,59,59);
764
		}
765
		if (is_array($event['recur_exception']))
766
		{
767
			foreach($event['recur_exception'] as $exception)
768
			{
769
				$exceptions[] = is_a($exception,'DateTime') ? $exception : new Api\DateTime($exception,$timestamp_tz);
770
			}
771
		}
772
		return new calendar_rrule($time,$event['recur_type'],$event['recur_interval'],$enddate,$event['recur_data'],$exceptions);
773
	}
774
775
	/**
776
	 * Generate a rrule from a string generated by __toString().
777
	 * @param String $rrule Recurrence rule in string format, as generated by __toString()
778
	 * @param DateTime date Optional date to work from, defaults to today
779
	 */
780
	public static function from_string(String $rrule, DateTime $date)
781
	{
782
		$time = $date ? $date : new Api\DateTime();
783
		$type_id = self::NONE;
784
		$interval = 1;
785
		$enddate = null;
786
		$weekdays = 0;
787
		$exceptions = array();
788
789
		list($type, $conditions) = explode(' (', $rrule);
790
		$conditions = explode(', ', substr($conditions, 0, -1));
791
792
		foreach(static::$types as $id => $type_name)
793
		{
794
			list($str) = explode(' (',lang($type_name));
795
			if($str == $type)
796
			{
797
				$type_id = $id;
798
				break;
799
			}
800
		}
801
802
		// Rejoin some extra splits for conditions with multiple values
803
		foreach($conditions as $condition_index => $condition)
804
		{
805
			if(((int)$condition || strpos($condition, lang('last')) === 0) &&
806
					substr_compare( $condition, lang('day'), -strlen( lang('day') ) ) === 0)
807
			{
808
				$time->setDate($time->format('Y'), $time->format('m'), (int)($condition) ? (int)$condition : $time->format('t'));
809
				unset($conditions[$condition_index]);
810
				continue;
811
			}
812
			if(!strpos($condition, ':') && strpos($conditions[$condition_index-1], ':'))
813
			{
814
				$conditions[$condition_index-1] .= ', ' . $condition;
815
				unset($conditions[$condition_index]);
816
			}
817
		}
818
		foreach($conditions as $condition_index => $condition)
819
		{
820
			list($condition_name, $value) = explode(': ', $condition);
821
			if($condition_name == lang('days repeated'))
822
			{
823
				foreach (self::$days as $mask => $label)
824
				{
825
					if (strpos($value, $label) !== FALSE || strpos($value, lang($label)) !== FALSE)
826
					{
827
						$weekdays += $mask;
828
					}
829
				}
830
				if(stripos($condition, lang('all')) !== false)
831
				{
832
					$weekdays = self::ALLDAYS;
833
				}
834
			}
835
			else if ($condition_name == lang('interval'))
836
			{
837
				$interval = (int)$value;
838
			}
839
			else if ($condition_name == lang('ends'))
840
			{
841
				list($dow, $date) = explode(', ', $value);
0 ignored issues
show
Unused Code introduced by
The assignment to $dow is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
842
				$enddate = new DateTime($date);
843
			}
844
		}
845
		
846
		return new calendar_rrule($time,$type_id,$interval,$enddate,$weekdays,$exceptions);
847
	}
848
	/**
849
	 * Get recurrence data (keys 'recur_*') to merge into an event
850
	 *
851
	 * @return array
852
	 */
853
	public function rrule2event()
854
	{
855
		return array(
856
			'recur_type' => $this->type,
857
			'recur_interval' => $this->interval,
858
			'recur_enddate' => $this->enddate ? $this->enddate->format('ts') : null,
859
			'recur_data' => $this->weekdays,
860
			'recur_exception' => $this->exceptions,
861
		);
862
	}
863
864
	/**
865
	 * Shift a recurrence rule to a new timezone
866
	 *
867
	 * @param array $event			recurring event
868
	 * @param DateTime/string		starttime of the event (in servertime)
869
	 * @param string $to_tz			new timezone
870
	 */
871
	public static function rrule2tz(array &$event,$starttime,$to_tz)
872
	{
873
		// We assume that the difference between timezones can result
874
		// in a maximum of one day
875
876
		if (!is_array($event) ||
877
			!isset($event['recur_type']) ||
878
			$event['recur_type'] == self::NONE ||
879
			empty($event['recur_data']) || $event['recur_data'] == ALLDAYS ||
880
			empty($event['tzid']) || empty($to_tz) ||
881
			$event['tzid'] == $to_tz) return;
882
883 View Code Duplication
		if (!isset(self::$tz_cache[$event['tzid']]))
884
		{
885
			self::$tz_cache[$event['tzid']] = calendar_timezones::DateTimeZone($event['tzid']);
886
		}
887
		if (!isset(self::$tz_cache[$to_tz]))
888
		{
889
			self::$tz_cache[$to_tz] = calendar_timezones::DateTimeZone($to_tz);
890
		}
891
892
		$time = is_a($starttime,'DateTime') ?
893
			$starttime : new Api\DateTime($starttime, Api\DateTime::$server_timezone);
894
		$time->setTimezone(self::$tz_cache[$event['tzid']]);
895
		$remote = clone $time;
896
		$remote->setTimezone(self::$tz_cache[$to_tz]);
897
		$delta = (int)$remote->format('w') - (int)$time->format('w');
898
		if ($delta)
899
		{
900
			// We have to generate a shifted rrule
901
			switch ($event['recur_type'])
902
			{
903
				case self::MONTHLY_WDAY:
904
				case self::WEEKLY:
905
					$mask = (int)$event['recur_data'];
906
907
					if ($delta == 1 || $delta == -6)
908
					{
909
						$mask = $mask << 1;
910
						if ($mask & 128) $mask = $mask - 127; // overflow
911
					}
912
					else
913
					{
914
						if ($mask & 1) $mask = $mask + 128; // underflow
915
						$mask = $mask >> 1;
916
					}
917
					$event['recur_data'] = $mask;
918
			}
919
		}
920
	}
921
}
922
923
if (isset($_SERVER['SCRIPT_FILENAME']) && $_SERVER['SCRIPT_FILENAME'] == __FILE__)	// some tests
924
{
925
	ini_set('display_errors',1);
926
	error_reporting(E_ALL & ~E_NOTICE);
927
	function lang($str) { return $str; }
0 ignored issues
show
Best Practice introduced by
The function lang() has been defined more than once; this definition is ignored, only the first definition in api/src/loader/common.php (L379-387) is considered.

This check looks for functions that have already been defined in other files.

Some Codebases, like WordPress, make a practice of defining functions multiple times. This may lead to problems with the detection of function parameters and types. If you really need to do this, you can mark the duplicate definition with the @ignore annotation.

/**
 * @ignore
 */
function getUser() {

}

function getUser($id, $realm) {

}

See also the PhpDoc documentation for @ignore.

Loading history...
928
	$GLOBALS['egw_info']['user']['preferences']['common']['tz'] = $_REQUEST['user-tz'] ? $_REQUEST['user-tz'] : 'Europe/Berlin';
929
	require_once('../../api/src/autoload.php');
930
931
	if (!isset($_REQUEST['time']))
932
	{
933
		$now = new Api\DateTime('now',new DateTimeZone($_REQUEST['tz'] = 'UTC'));
934
		$_REQUEST['time'] = $now->format();
935
		$_REQUEST['type'] = calendar_rrule::WEEKLY;
936
		$_REQUEST['interval'] = 2;
937
		$now->modify('2 month');
938
		$_REQUEST['enddate'] = $now->format('Y-m-d');
939
		$_REQUEST['user-tz'] = 'Europe/Berlin';
940
	}
941
	echo "<html>\n<head>\n\t<title>Test calendar_rrule class</title>\n</head>\n<body>\n<form method='GET'>\n";
942
	echo "<p>Date+Time: ".Api\Html::input('time',$_REQUEST['time']).
943
		Api\Html::select('tz',$_REQUEST['tz'],Api\DateTime::getTimezones())."</p>\n";
944
	echo "<p>Type: ".Api\Html::select('type',$_REQUEST['type'],calendar_rrule::$types)."\n".
945
		"Interval: ".Api\Html::input('interval',$_REQUEST['interval'])."</p>\n";
946
	echo "<table><tr><td>\n";
947
	echo "Weekdays:<br />".Api\Html::checkbox_multiselect('weekdays',$_REQUEST['weekdays'],calendar_rrule::$days,false,'','7',false,'height: 150px;')."\n";
948
	echo "</td><td>\n";
949
	echo "<p>Exceptions:<br />".Api\Html::textarea('exceptions',$_REQUEST['exceptions'],'style="height: 150px;"')."\n";
950
	echo "</td></tr></table>\n";
951
	echo "<p>Enddate: ".Api\Html::input('enddate',$_REQUEST['enddate'])."</p>\n";
952
	echo "<p>Display recurances in ".Api\Html::select('user-tz',$_REQUEST['user-tz'],Api\DateTime::getTimezones())."</p>\n";
953
	echo "<p>".Api\Html::submit_button('calc','Calculate')."</p>\n";
954
	echo "</form>\n";
955
956
	$tz = new DateTimeZone($_REQUEST['tz']);
957
	$time = new Api\DateTime($_REQUEST['time'],$tz);
958
	if ($_REQUEST['enddate']) $enddate = new Api\DateTime($_REQUEST['enddate'],$tz);
959
	$weekdays = 0; foreach((array)$_REQUEST['weekdays'] as $mask) { $weekdays |= $mask; }
960
	if ($_REQUEST['exceptions']) foreach(preg_split("/[,\r\n]+ ?/",$_REQUEST['exceptions']) as $exception) { $exceptions[] = new Api\DateTime($exception); }
961
962
	$rrule = new calendar_rrule($time,$_REQUEST['type'],$_REQUEST['interval'],$enddate,$weekdays,$exceptions);
963
	echo "<h3>".$time->format('l').', '.$time.' ('.$tz->getName().') '.$rrule."</h3>\n";
964
	foreach($rrule as $rtime)
965
	{
966
		$rtime->setTimezone(Api\DateTime::$user_timezone);
967
		echo ++$n.': '.$rtime->format('l').', '.$rtime."<br />\n";
968
	}
969
	echo "</body>\n</html>\n";
970
}