calendar_rrule::__toString()   F
last analyzed

Complexity

Conditions 21
Paths 1033

Size

Total Lines 69
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 21
eloc 35
nc 1033
nop 0
dl 0
loc 69
rs 0
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
	 * Hourly recurrance
61
	 */
62
	const HOURLY = 8;
63
	/**
64
	 * Minutely recurrance
65
	 */
66
	const MINUTELY = 7;
67
68
	/**
69
	 * Translate recure types to labels
70
	 *
71
	 * @var array
72
	 */
73
	static public $types = Array(
74
		self::NONE         => 'None',
75
		self::DAILY        => 'Daily',
76
		self::WEEKLY       => 'Weekly',
77
		self::MONTHLY_WDAY => 'Monthly (by day)',
78
		self::MONTHLY_MDAY => 'Monthly (by date)',
79
		self::YEARLY       => 'Yearly',
80
	);
81
82
	/**
83
	 * @var array $recur_egw2ical_2_0 converstaion of egw recur-type => ical FREQ
84
	 */
85
	static private $recur_egw2ical_2_0 = array(
86
		self::DAILY        => 'DAILY',
87
		self::WEEKLY       => 'WEEKLY',
88
		self::MONTHLY_WDAY => 'MONTHLY',	// BYDAY={1..7, -1}{MO..SO, last workday}
89
		self::MONTHLY_MDAY => 'MONTHLY',	// BYMONHTDAY={1..31, -1 for last day of month}
90
		self::YEARLY       => 'YEARLY',
91
		self::HOURLY       => 'HOURLY',
92
		self::MINUTELY     => 'MINUTELY',
93
	);
94
95
	/**
96
	 * @var array $recur_egw2ical_1_0 converstaion of egw recur-type => ical FREQ
97
	 */
98
	static private $recur_egw2ical_1_0 = array(
99
		self::DAILY        => 'D',
100
		self::WEEKLY       => 'W',
101
		self::MONTHLY_WDAY => 'MP',	// BYDAY={1..7,-1}{MO..SO, last workday}
102
		self::MONTHLY_MDAY => 'MD',	// BYMONHTDAY={1..31,-1}
103
		self::YEARLY       => 'YM',
104
	);
105
106
	/**
107
	 * RRule type: NONE, DAILY, WEEKLY, MONTHLY_MDAY, MONTHLY_WDAY, YEARLY
108
	 *
109
	 * @var int
110
	 */
111
	public $type = self::NONE;
112
113
	/**
114
	 * Interval
115
	 *
116
	 * @var int
117
	 */
118
	public $interval = 1;
119
120
	/**
121
	 * Number for monthly byday: 1, ..., 5, -1=last weekday of month
122
	 *
123
	 * EGroupware Calendar does NOT explicitly store it, it's only implicitly defined by series start date
124
	 *
125
	 * @var int
126
	 */
127
	public $monthly_byday_num;
128
129
	/**
130
	 * Number for monthly bymonthday: 1, ..., 31, -1=last day of month
131
	 *
132
	 * EGroupware Calendar does NOT explicitly store it, it's only implicitly defined by series start date
133
	 *
134
	 * @var int
135
	 */
136
	public $monthly_bymonthday;
137
138
	/**
139
	 * Enddate of recurring event or null, if not ending
140
	 *
141
	 * @var DateTime
142
	 */
143
	public $enddate;
144
145
	/**
146
	 * Enddate of recurring event, as Ymd integer (eg. 20091111)
147
	 *
148
	 * Or 5 years in future, if no enddate. So iterator is always limited.
149
	 *
150
	 * @var int
151
	 */
152
	public $enddate_ymd;
153
154
	/**
155
	 * Enddate of recurring event, as timestamp
156
	 *
157
	 * Or 5 years in future, if no enddate. So iterator is always limited.
158
	 *
159
	 * @var int
160
	 */
161
	public $enddate_ts;
162
163
	const SUNDAY    = 1;
164
	const MONDAY    = 2;
165
	const TUESDAY   = 4;
166
	const WEDNESDAY = 8;
167
	const THURSDAY  = 16;
168
	const FRIDAY    = 32;
169
	const SATURDAY  = 64;
170
	const WORKDAYS  = 62;	// Mo, ..., Fr
171
	const ALLDAYS   = 127;
172
	/**
173
	 * Translate weekday bitmasks to labels
174
	 *
175
	 * @var array
176
	 */
177
	static public $days = array(
178
		self::MONDAY    => 'Monday',
179
		self::TUESDAY   => 'Tuesday',
180
		self::WEDNESDAY => 'Wednesday',
181
		self::THURSDAY  => 'Thursday',
182
		self::FRIDAY    => 'Friday',
183
		self::SATURDAY  => 'Saturday',
184
		self::SUNDAY    => 'Sunday',
185
	);
186
	/**
187
	 * Bitmask of valid weekdays for weekly repeating events: self::SUNDAY|...|self::SATURDAY
188
	 *
189
	 * @var integer
190
	 */
191
	public $weekdays;
192
193
	/**
194
	 * Array of exception dates (Ymd strings)
195
	 *
196
	 * @var array
197
	 */
198
	public $exceptions=array();
199
200
	/**
201
	 * Array of exceptions as DateTime/egw_time objects
202
	 *
203
	 * @var array
204
	 */
205
	public $exceptions_objs=array();
206
207
	/**
208
	 * Starttime of series
209
	 *
210
	 * @var Api\DateTime
211
	 */
212
	public $time;
213
214
	/**
215
	 * Current "position" / time
216
	 *
217
	 * @var Api\DateTime
218
	 */
219
	public $current;
220
221
	/**
222
	 * Last day of the week according to user preferences
223
	 *
224
	 * @var int
225
	 */
226
	protected $lastdayofweek;
227
228
	/**
229
	 * Cached timezone data
230
	 *
231
	 * @var array id => data
232
	 */
233
	protected static $tz_cache = array();
234
235
	/**
236
	 * Constructor
237
	 *
238
	 * The constructor accepts on DateTime (or decendents like egw_date) for all times, to work timezone-correct.
239
	 * The timezone of the event is determined by timezone of $time, other times get converted to that timezone.
240
	 *
241
	 * @param DateTime $time start of event in it's own timezone
242
	 * @param int $type self::NONE, self::DAILY, ..., self::YEARLY
243
	 * @param int $interval =1 1, 2, ...
244
	 * @param DateTime $enddate =null enddate or null for no enddate (in which case we user '+5 year' on $time)
245
	 * @param int $weekdays =0 self::SUNDAY=1|self::MONDAY=2|...|self::SATURDAY=64
246
	 * @param array $exceptions =null DateTime objects with exceptions
247
	 */
248
	public function __construct(DateTime $time,$type,$interval=1,DateTime $enddate=null,$weekdays=0,array $exceptions=null)
249
	{
250
		switch($GLOBALS['egw_info']['user']['preferences']['calendar']['weekdaystarts'])
251
		{
252
			case 'Sunday':
253
				$this->lastdayofweek = self::SATURDAY;
254
				break;
255
			case 'Saturday':
256
				$this->lastdayofweek = self::FRIDAY;
257
				break;
258
			default: // Monday
259
				$this->lastdayofweek = self::SUNDAY;
260
		}
261
262
		$this->time = $time instanceof Api\DateTime ? $time : new Api\DateTime($time);
263
264
		if (!in_array($type,array(self::NONE, self::DAILY, self::WEEKLY, self::MONTHLY_MDAY, self::MONTHLY_WDAY, self::YEARLY, self::HOURLY, self::MINUTELY)))
265
		{
266
			throw new Api\Exception\WrongParameter(__METHOD__."($time,$type,$interval,$enddate,$weekdays,...) type $type is NOT valid!");
267
		}
268
		$this->type = $type;
269
270
		// determine only implicit defined rules for RRULE=MONTHLY,BYDAY={-1, 1, ..., 5}{MO,..,SU}
271
		if ($type == self::MONTHLY_WDAY)
272
		{
273
			// check for last week of month
274
			if (($day = $this->time->format('d')) >= 21 && $day > self::daysInMonth($this->time)-7)
275
			{
276
				$this->monthly_byday_num = -1;
277
			}
278
			else
279
			{
280
				$this->monthly_byday_num = 1 + floor(($this->time->format('d')-1) / 7);
281
			}
282
		}
283
		elseif($type == self::MONTHLY_MDAY)
284
		{
285
			$this->monthly_bymonthday = (int)$this->time->format('d');
286
			// check for last day of month
287
			if ($this->monthly_bymonthday >= 28)
288
			{
289
				$test = clone $this->time;
290
				$test->modify('1 day');
291
				if ($test->format('m') != $this->time->format('m'))
292
				{
293
					$this->monthly_bymonthday = -1;
294
				}
295
			}
296
		}
297
298
		if ((int)$interval < 1)
299
		{
300
			$interval = 1;	// calendar stores no (extra) interval as null, so using default 1 here
301
		}
302
		$this->interval = (int)$interval;
303
304
		$this->enddate = $enddate;
305
		// no recurrence --> current date is enddate
306
		if ($type == self::NONE)
307
		{
308
			$enddate = clone $this->time;
309
		}
310
		// set a maximum of 5 years if no enddate given
311
		elseif (is_null($enddate))
312
		{
313
			$enddate = clone $this->time;
314
			$enddate->modify('5 year');
315
		}
316
		// convert enddate to timezone of time, if necessary
317
		else
318
		{
319
			$enddate->setTimezone($this->time->getTimezone());
320
		}
321
		$this->enddate_ymd = (int)$enddate->format('Ymd');
322
		$this->enddate_ts = $enddate->format('ts');
0 ignored issues
show
Documentation Bug introduced by
It seems like $enddate->format('ts') of type DateTime or EGroupware\Api\DateTime or string is incompatible with the declared type integer of property $enddate_ts.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
323
324
		// if no valid weekdays are given for weekly repeating, we use just the current weekday
325
		if (!($this->weekdays = (int)$weekdays) && ($type == self::WEEKLY || $type == self::MONTHLY_WDAY))
326
		{
327
			$this->weekdays = self::getWeekday($this->time);
328
		}
329
		if ($exceptions)
330
		{
331
			foreach($exceptions as $exception)
332
			{
333
				$exception->setTimezone($this->time->getTimezone());
334
				$this->exceptions[] = $exception->format('Ymd');
335
			}
336
			$this->exceptions_objs = $exceptions;
337
		}
338
	}
339
340
	/**
341
	 * Get recurrence interval duration in seconds
342
	 *
343
	 * @param int $type self::(DAILY|WEEKLY|MONTHLY_(M|W)DAY|YEARLY)
344
	 * @param int $interval =1
345
	 * @return int
346
	 */
347
	public static function recurrence_interval($type, $interval=1)
348
	{
349
		switch($type)
350
		{
351
			case self::DAILY:
352
				$duration = 24*3600;
353
				break;
354
			case self::WEEKLY:
355
				$duration = 7*24*3600;
356
				break;
357
			case self::MONTHLY_MDAY:
358
			case self::MONTHLY_WDAY:
359
				$duration = 31*24*3600;
360
				break;
361
			case self::YEARLY:
362
				$duration = 366*24*3600;
363
				break;
364
		}
365
		if ($interval > 1) $duration *= $interval;
366
367
		return $duration;
368
	}
369
370
	/**
371
	 * Get number of days in month of given date
372
	 *
373
	 * @param DateTime $time
374
	 * @return int
375
	 */
376
	private static function daysInMonth(DateTime $time)
377
	{
378
		list($year,$month) = explode('-',$time->format('Y-m'));
379
		$last_day = new Api\DateTime();
380
		$last_day->setDate($year,$month+1,0);
381
382
		return (int)$last_day->format('d');
383
	}
384
385
	/**
386
	 * Return the current element
387
	 *
388
	 * @return DateTime
389
	 */
390
	public function current()
391
	{
392
		return clone $this->current;
393
	}
394
395
	/**
396
	 * Return the key of the current element, we use a Ymd integer as key
397
	 *
398
	 * @return int
399
	 */
400
	public function key()
401
	{
402
		return (int)$this->current->format('Ymd');
403
	}
404
405
	/**
406
	 * Move forward to next recurence, not caring for exceptions
407
	 */
408
	public function next_no_exception()
409
	{
410
		switch($this->type)
411
		{
412
			case self::NONE:	// need to add at least one day, to end "series", as enddate == current date
413
			case self::DAILY:
414
				$this->current->modify($this->interval.' day');
415
				break;
416
417
			case self::WEEKLY:
418
				// advance to next valid weekday
419
				do
420
				{
421
					// interval in weekly means event runs on valid days eg. each 2. week
422
					// --> on the last day of the week we have to additionally advance interval-1 weeks
423
					if ($this->interval > 1 && self::getWeekday($this->current) == $this->lastdayofweek)
424
					{
425
						$this->current->modify(($this->interval-1).' week');
426
					}
427
					$this->current->modify('1 day');
428
					//echo __METHOD__.'() '.$this->current->format('l').', '.$this->current.": $this->weekdays & ".self::getWeekday($this->current)."<br />\n";
429
				}
430
				while(!($this->weekdays & self::getWeekday($this->current)));
431
				break;
432
433
			case self::MONTHLY_WDAY:	// iCal: BYDAY={1, ..., 5, -1}{MO..SO}
434
				// advance to start of next month
435
				list($year,$month) = explode('-',$this->current->format('Y-m'));
436
				$month += $this->interval+($this->monthly_byday_num < 0 ? 1 : 0);
437
				$this->current->setDate($year,$month,$this->monthly_byday_num < 0 ? 0 : 1);
438
				//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";
439
				// now advance to n-th week
440
				if ($this->monthly_byday_num > 1)
441
				{
442
					$this->current->modify(($this->monthly_byday_num-1).' week');
443
					//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";
444
				}
445
				// advance to given weekday
446
				while(!($this->weekdays & self::getWeekday($this->current)))
447
				{
448
					$this->current->modify(($this->monthly_byday_num < 0 ? -1 : 1).' day');
449
					//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";
450
				}
451
				break;
452
453
			case self::MONTHLY_MDAY:	// iCal: monthly_bymonthday={1, ..., 31, -1}
454
				list($year,$month) = explode('-',$this->current->format('Y-m'));
455
				$day = $this->monthly_bymonthday+($this->monthly_bymonthday < 0 ? 1 : 0);
456
				$month += $this->interval+($this->monthly_bymonthday < 0 ? 1 : 0);
457
				$this->current->setDate($year,$month,$day);
458
				//echo __METHOD__."() setDate($year,$month,$day): ".$this->current->format('l').', '.$this->current."<br />\n";
459
				break;
460
461
			case self::YEARLY:
462
				$this->current->modify($this->interval.' year');
463
				break;
464
465
			case self::HOURLY:
466
				$this->current->modify($this->interval.' hour');
467
				break;
468
469
			case self::MINUTELY:
470
				$this->current->modify($this->interval.' minute');
471
				break;
472
473
			default:
474
				throw new Api\Exception\AssertionFailed(__METHOD__."() invalid type #$this->type !");
475
		}
476
	}
477
478
	/**
479
	 * Move forward to next recurence, taking into account exceptions
480
	 */
481
	public function next()
482
	{
483
		do
484
		{
485
			$this->next_no_exception();
486
		}
487
		while($this->exceptions && in_array($this->current->format('Ymd'),$this->exceptions));
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->exceptions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
488
	}
489
490
	/**
491
	 * Get weekday of $time as self::SUNDAY=1, ..., self::SATURDAY=64 integer mask
492
	 *
493
	 * @param DateTime $time
494
	 * @return int self::SUNDAY=1, ..., self::SATURDAY=64
495
	 */
496
	static protected function getWeekday(DateTime $time)
497
	{
498
		//echo __METHOD__.'('.$time->format('l').' '.$time.') 1 << '.$time->format('w').' = '.(1 << (int)$time->format('w'))."<br />\n";
499
		return 1 << (int)$time->format('w');
500
	}
501
502
	/**
503
	 * Get datetime of n-th event, 1. is original event-time
504
	 *
505
	 * This is identical on COUNT parameter of RRULE is evaluated, exceptions are NOT taken into account!
506
	 *
507
	 * @param int $count
508
	 * @return DateTime
509
	 */
510
	public function count2date($count)
511
	{
512
		if ($count <= 1)
513
		{
514
			return clone $this->time;
515
		}
516
		if (isset($this->current)) $backup = $this->current;
517
		$this->rewind();
518
519
		while(--$count > 0)
520
		{
521
			$this->next_no_exception();
522
		}
523
		$ret = clone $this->current;
524
		if ($backup) $this->current = $backup;
525
		return $ret;
526
	}
527
528
	/**
529
	 * Fix enddates which are not on a recurrence, eg. for a on Monday recurring weekly event a Tuesday
530
	 *
531
	 * @return DateTime
532
	 */
533
	public function normalize_enddate()
534
	{
535
		$this->rewind();
536
		while ($this->current < $this->enddate)
537
		{
538
			$previous = clone $this->current;
539
			$this->next_no_exception();
540
		}
541
		// if enddate is now before next acurrence, but not on same day, we use previous recurrence
542
		// this can happen if client gives an enddate which is NOT a recurrence date
543
		// eg. for a on Monday recurring weekly event a Tuesday as enddate
544
		if ($this->enddate < $this->current  && $this->current->format('Ymd') != $this->enddate->format('Ymd'))
545
		{
546
			$last = $previous;
547
		}
548
		else
549
		{
550
			$last = clone $this->current;
551
		}
552
		return $last;
553
	}
554
555
	/**
556
	 * Rewind the Iterator to the first element (called at beginning of foreach loop)
557
	 */
558
	public function rewind()
559
	{
560
		$this->current = clone $this->time;
561
		while ($this->valid() &&
562
			$this->exceptions &&
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->exceptions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
563
			in_array($this->current->format('Ymd'),$this->exceptions))
564
		{
565
			$this->next_no_exception();
566
		}
567
	}
568
569
	/**
570
	 * Checks if current position is valid
571
	 *
572
	 * @param boolean $use_just_date =false default use also time
573
	 * @return boolean
574
	 */
575
	public function valid($use_just_date=false)
576
	{
577
		if ($use_just_date)
578
		{
579
			return $this->current->format('Ymd') <= $this->enddate_ymd;
580
		}
581
		return $this->current->format('ts') < $this->enddate_ts;
582
	}
583
584
	/**
585
	 * Return string represenation of RRule
586
	 *
587
	 * @return string
588
	 */
589
	function __toString( )
590
	{
591
		$str = '';
592
		// Repeated Events
593
		if($this->type != self::NONE)
594
		{
595
			$str = lang(self::$types[$this->type]);
596
597
			$str_extra = array();
598
			switch ($this->type)
599
			{
600
				case self::MONTHLY_MDAY:
601
					$str_extra[] = ($this->monthly_bymonthday == -1 ? lang('last') : $this->monthly_bymonthday.'.').' '.lang('day');
602
					break;
603
604
				case self::WEEKLY:
605
				case self::MONTHLY_WDAY:
606
					$repeat_days = array();
607
					if ($this->weekdays == self::ALLDAYS)
608
					{
609
						$repeat_days[] = $this->type == self::WEEKLY ? lang('all') : lang('day');
610
					}
611
					elseif($this->weekdays == self::WORKDAYS)
612
					{
613
						$repeat_days[] = $this->type == self::WEEKLY ? lang('workdays') : lang('workday');
614
					}
615
					else
616
					{
617
						foreach (self::$days as $mask => $label)
618
						{
619
							if ($this->weekdays & $mask)
620
							{
621
								$repeat_days[] = lang($label);
622
							}
623
						}
624
					}
625
					if($this->type == self::WEEKLY && count($repeat_days))
626
					{
627
						$str_extra[] = lang('days repeated').': '.implode(', ',$repeat_days);
628
					}
629
					elseif($this->type == self::MONTHLY_WDAY)
630
					{
631
						$str_extra[] = ($this->monthly_byday_num == -1 ? lang('last') : $this->monthly_byday_num.'.').' '.implode(', ',$repeat_days);
632
					}
633
					break;
634
635
			}
636
			if($this->interval > 1)
637
			{
638
				$str_extra[] = lang('Interval').': '.$this->interval;
639
			}
640
			if ($this->enddate)
641
			{
642
				if ($this->enddate->getTimezone()->getName() != Api\DateTime::$user_timezone->getName())
643
				{
644
					$this->enddate->setTimezone(Api\DateTime::$user_timezone);
645
				}
646
				$str_extra[] = lang('ends').': '.lang($this->enddate->format('l')).', '.$this->enddate->format(Api\DateTime::$user_dateformat);
647
			}
648
			if ($this->time->getTimezone()->getName() != Api\DateTime::$user_timezone->getName())
649
			{
650
				$str_extra[] = $this->time->getTimezone()->getName();
651
			}
652
			if(count($str_extra))
653
			{
654
				$str .= ' ('.implode(', ',$str_extra).')';
655
			}
656
		}
657
		return $str;
658
	}
659
660
	/**
661
	 * Generate a VEVENT RRULE
662
	 * @param string $version ='2.0' could be '1.0' too
663
	 *
664
	 * $return array	vCalendar RRULE
665
	 */
666
	public function generate_rrule($version='2.0')
667
	{
668
		$repeat_days = array();
669
		$rrule = array();
670
671
		if ($this->type == self::NONE) return false;	// no recuring event
672
673
		if ($version == '1.0')
674
		{
675
			$rrule['FREQ'] = self::$recur_egw2ical_1_0[$this->type] . $this->interval;
676
			switch ($this->type)
677
			{
678
				case self::WEEKLY:
679
					foreach (self::$days as $mask => $label)
680
					{
681
						if ($this->weekdays & $mask)
682
						{
683
							$repeat_days[] = strtoupper(substr($label,0,2));
684
						}
685
					}
686
					$rrule['BYDAY'] = implode(' ', $repeat_days);
687
					$rrule['FREQ'] = $rrule['FREQ'].' '.$rrule['BYDAY'];
688
					break;
689
690
				case self::MONTHLY_MDAY:	// date of the month: BYMONTDAY={1..31}
691
					break;
692
693
				case self::MONTHLY_WDAY:	// weekday of the month: BDAY={1..5}+ {MO..SO}
694
					$rrule['BYDAY'] = abs($this->monthly_byday_num);
695
					$rrule['BYDAY'] .= ($this->monthly_byday_num < 0) ? '- ' : '+ ';
696
					$rrule['BYDAY'] .= strtoupper(substr($this->time->format('l'),0,2));
697
					$rrule['FREQ'] = $rrule['FREQ'].' '.$rrule['BYDAY'];
698
					break;
699
			}
700
701
			if (!$this->enddate)
702
			{
703
				$rrule['UNTIL'] = '#0';
704
			}
705
		}
706
		else // $version == '2.0'
707
		{
708
			$rrule['FREQ'] = self::$recur_egw2ical_2_0[$this->type];
709
			switch ($this->type)
710
			{
711
				case self::WEEKLY:
712
					foreach (self::$days as $mask => $label)
713
					{
714
						if ($this->weekdays & $mask)
715
						{
716
							$repeat_days[] = strtoupper(substr($label,0,2));
717
						}
718
					}
719
					$rrule['BYDAY'] = implode(',', $repeat_days);
720
					break;
721
722
				case self::MONTHLY_MDAY:	// date of the month: BYMONTDAY={1..31}
723
					$rrule['BYMONTHDAY'] = $this->monthly_bymonthday;
724
					break;
725
726
				case self::MONTHLY_WDAY:	// weekday of the month: BDAY={1..5}{MO..SO}
727
					$rrule['BYDAY'] = $this->monthly_byday_num .
728
						strtoupper(substr($this->time->format('l'),0,2));
729
					break;
730
			}
731
			if ($this->interval > 1)
732
			{
733
				$rrule['INTERVAL'] = $this->interval;
734
			}
735
		}
736
737
		if ($this->enddate)
738
		{
739
			// our enddate is the end-time, not start-time of last event!
740
			$this->rewind();
741
			$enddate = $this->current();
742
			do
743
			{
744
				$this->next_no_exception();
745
				$occurrence = $this->current();
746
			}
747
			while ($this->valid() && ($enddate = $occurrence));
748
			$rrule['UNTIL'] = $enddate;
749
		}
750
751
		return $rrule;
752
	}
753
754
	/**
755
	 * Get instance for a given event array
756
	 *
757
	 * @param array $event
758
	 * @param boolean $usertime =true true: event timestamps are usertime (default for calendar_bo::(read|search), false: servertime
759
	 * @param string $to_tz			timezone for exports (null for event's timezone)
760
	 *
761
	 * @return calendar_rrule		false on error
762
	 */
763
	public static function event2rrule(array $event,$usertime=true,$to_tz=null)
764
	{
765
		if (!is_array($event)  || !isset($event['tzid'])) return false;
0 ignored issues
show
introduced by
The condition is_array($event) is always true.
Loading history...
766
		if (!$to_tz) $to_tz = $event['tzid'];
767
		$timestamp_tz = $usertime ? Api\DateTime::$user_timezone : Api\DateTime::$server_timezone;
768
		$time = is_a($event['start'],'DateTime') ? $event['start'] : new Api\DateTime($event['start'],$timestamp_tz);
769
770
		if (!isset(self::$tz_cache[$to_tz]))
771
		{
772
			self::$tz_cache[$to_tz] = calendar_timezones::DateTimeZone($to_tz);
773
		}
774
775
		self::rrule2tz($event, $time, $to_tz);
776
777
		$time->setTimezone(self::$tz_cache[$to_tz]);
778
779
		if ($event['recur_enddate'])
780
		{
781
			$enddate = is_a($event['recur_enddate'],'DateTime') ? clone $event['recur_enddate'] : new Api\DateTime($event['recur_enddate'],$timestamp_tz);
782
783
			// Check to see if switching timezones changes the date, we'll need to adjust for that
784
			$enddate_event_timezone = clone $enddate;
785
			$enddate->setTimezone($timestamp_tz);
786
			$delta = (int)$enddate_event_timezone->format('z') - (int)$enddate->format('z');
787
			$enddate->add("$delta days");
788
789
			$end = is_a($event['end'],'DateTime') ? clone $event['end'] : new Api\DateTime($event['end'],$timestamp_tz);
790
			$end->setTimezone($enddate->getTimezone());
791
			$enddate->setTime($end->format('H'),$end->format('i'),0);
792
			if($event['whole_day'])
793
			{
794
				$enddate->setTime(23,59,59);
795
			}
796
		}
797
		if (is_array($event['recur_exception']))
798
		{
799
			foreach($event['recur_exception'] as $exception)
800
			{
801
				$exceptions[] = is_a($exception,'DateTime') ? $exception : new Api\DateTime($exception,$timestamp_tz);
802
			}
803
		}
804
		return new calendar_rrule($time,$event['recur_type'],$event['recur_interval'],$enddate,$event['recur_data'],$exceptions);
805
	}
806
807
	/**
808
	 * Generate a rrule from a string generated by __toString().
809
	 *
810
	 * @param String $rrule Recurrence rule in string format, as generated by __toString()
811
	 * @param DateTime date Optional date to work from, defaults to today
812
	 */
813
	public static function from_string(String $rrule, DateTime $date)
814
	{
815
		$time = $date ? $date : new Api\DateTime();
0 ignored issues
show
introduced by
$date is of type DateTime, thus it always evaluated to true.
Loading history...
816
		$type_id = self::NONE;
817
		$interval = 1;
818
		$enddate = null;
819
		$weekdays = 0;
820
		$exceptions = array();
821
822
		list($type, $sub, $conds) = explode(' (', $rrule);
823
		if(!$conds)
824
		{
825
			$conds = $sub;
826
		}
827
		else
828
		{
829
			$type .= " ($sub";
830
		}
831
		$conditions = explode(', ', substr($conds, 0, -1));
832
833
		foreach(static::$types as $id => $type_name)
834
		{
835
			$str = lang($type_name);
836
			if($str == $type)
837
			{
838
				$type_id = $id;
839
				break;
840
			}
841
		}
842
843
		// Rejoin some extra splits for conditions with multiple values
844
		foreach($conditions as $condition_index => $condition)
845
		{
846
			if(((int)$condition || strpos($condition, lang('last')) === 0) &&
847
					substr_compare( $condition, lang('day'), -strlen( lang('day') ) ) === 0)
848
			{
849
				$time->setDate($time->format('Y'), $time->format('m'), (int)($condition) ? (int)$condition : $time->format('t'));
850
				unset($conditions[$condition_index]);
851
				continue;
852
			}
853
			if(!strpos($condition, ':') && strpos($conditions[$condition_index-1], ':'))
854
			{
855
				$conditions[$condition_index-1] .= ', ' . $condition;
856
				unset($conditions[$condition_index]);
857
			}
858
		}
859
		foreach($conditions as $condition_index => $condition)
860
		{
861
			list($condition_name, $value) = explode(': ', $condition);
862
			if($condition_name == lang('days repeated'))
863
			{
864
				foreach (self::$days as $mask => $label)
865
				{
866
					if (strpos($value, $label) !== FALSE || strpos($value, lang($label)) !== FALSE)
867
					{
868
						$weekdays += $mask;
869
					}
870
				}
871
				if(stripos($condition, lang('all')) !== false)
872
				{
873
					$weekdays = self::ALLDAYS;
874
				}
875
			}
876
			else if ($condition_name == lang('interval'))
877
			{
878
				$interval = (int)$value;
879
			}
880
			else if ($condition_name == lang('ends'))
881
			{
882
				list(, $date) = explode(', ', $value);
883
				$enddate = new DateTime($date);
884
			}
885
		}
886
887
		return new calendar_rrule($time,$type_id,$interval,$enddate,$weekdays,$exceptions);
888
	}
889
890
	/**
891
	 * Get recurrence data (keys 'recur_*') to merge into an event
892
	 *
893
	 * @return array
894
	 */
895
	public function rrule2event()
896
	{
897
		return array(
898
			'recur_type' => $this->type,
899
			'recur_interval' => $this->interval,
900
			'recur_enddate' => $this->enddate ? $this->enddate->format('ts') : null,
901
			'recur_data' => $this->weekdays,
902
			'recur_exception' => $this->exceptions,
903
		);
904
	}
905
906
	/**
907
	 * Shift a recurrence rule to a new timezone
908
	 *
909
	 * @param array $event			recurring event
910
	 * @param DateTime/string		starttime of the event (in servertime)
0 ignored issues
show
Bug introduced by
The type starttime was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
911
	 * @param string $to_tz			new timezone
912
	 */
913
	public static function rrule2tz(array &$event,$starttime,$to_tz)
914
	{
915
		// We assume that the difference between timezones can result
916
		// in a maximum of one day
917
918
		if (!is_array($event) ||
0 ignored issues
show
introduced by
The condition is_array($event) is always true.
Loading history...
919
			!isset($event['recur_type']) ||
920
			$event['recur_type'] == self::NONE ||
921
			empty($event['recur_data']) || $event['recur_data'] == self::ALLDAYS ||
922
			empty($event['tzid']) || empty($to_tz) ||
923
			$event['tzid'] == $to_tz) return;
924
925
		if (!isset(self::$tz_cache[$event['tzid']]))
926
		{
927
			self::$tz_cache[$event['tzid']] = calendar_timezones::DateTimeZone($event['tzid']);
928
		}
929
		if (!isset(self::$tz_cache[$to_tz]))
930
		{
931
			self::$tz_cache[$to_tz] = calendar_timezones::DateTimeZone($to_tz);
932
		}
933
934
		$time = is_a($starttime,'DateTime') ?
935
			$starttime : new Api\DateTime($starttime, Api\DateTime::$server_timezone);
936
		$time->setTimezone(self::$tz_cache[$event['tzid']]);
937
		$remote = clone $time;
938
		$remote->setTimezone(self::$tz_cache[$to_tz]);
939
		$delta = (int)$remote->format('w') - (int)$time->format('w');
940
		if ($delta)
941
		{
942
			// We have to generate a shifted rrule
943
			switch ($event['recur_type'])
944
			{
945
				case self::MONTHLY_WDAY:
946
				case self::WEEKLY:
947
					$mask = (int)$event['recur_data'];
948
949
					if ($delta == 1 || $delta == -6)
950
					{
951
						$mask = $mask << 1;
952
						if ($mask & 128) $mask = $mask - 127; // overflow
953
					}
954
					else
955
					{
956
						if ($mask & 1) $mask = $mask + 128; // underflow
957
						$mask = $mask >> 1;
958
					}
959
					$event['recur_data'] = $mask;
960
			}
961
		}
962
	}
963
964
	/**
965
     * Parses a DateTime field and returns a unix timestamp. If the
966
     * field cannot be parsed then the original text is returned
967
     * unmodified.
968
     *
969
     * @param string $text  The Icalendar datetime field value.
970
     * @param string $tzid =null  A timezone identifier.
971
     *
972
     * @return integer  A unix timestamp.
973
	 */
974
	private static function parseIcalDateTime($text, $tzid=null)
975
	{
976
		static $vcal = null;
977
		if (!isset($vcal)) $vcal = new Horde_Icalendar;
978
979
		return $vcal->_parseDateTime($text, $tzid);
980
	}
981
982
	/**
983
	 * Parse an iCal recurrence-rule string
984
	 *
985
	 * @param type $recurence
986
	 * @param bool $support_below_daily =false true: support FREQ=HOURLY|MINUTELY
987
	 * @return type
988
	 */
989
	public static function parseRrule($recurence, $support_below_daily=false)
990
	{
991
		$vcardData = array();
992
		$vcardData['recur_interval'] = 1;
993
		$matches = null;
994
		$type = preg_match('/FREQ=([^;: ]+)/i',$recurence,$matches) ? $matches[1] : $recurence[0];
995
		// vCard 2.0 values for all types
996
		if (preg_match('/UNTIL=([0-9TZ]+)/',$recurence,$matches))
997
		{
998
			$vcardData['recur_enddate'] = self::parseIcalDateTime($matches[1]);
999
			// If it couldn't be parsed, treat it as not set
1000
			if(is_string($vcardData['recur_enddate']))
0 ignored issues
show
introduced by
The condition is_string($vcardData['recur_enddate']) is always false.
Loading history...
1001
			{
1002
				unset($vcardData['recur_enddate']);
1003
			}
1004
		}
1005
		elseif (preg_match('/COUNT=([0-9]+)/',$recurence,$matches))
1006
		{
1007
			$vcardData['recur_count'] = (int)$matches[1];
1008
		}
1009
		if (preg_match('/INTERVAL=([0-9]+)/',$recurence,$matches))
1010
		{
1011
			$vcardData['recur_interval'] = (int) $matches[1] ? (int) $matches[1] : 1;
1012
		}
1013
		$vcardData['recur_data'] = 0;
1014
		switch($type)
1015
		{
1016
			case 'D':	// 1.0
1017
				$recurenceMatches = null;
1018
				if (preg_match('/D(\d+) #(\d+)/', $recurence, $recurenceMatches))
1019
				{
1020
					$vcardData['recur_interval'] = $recurenceMatches[1];
1021
					$vcardData['recur_count'] = $recurenceMatches[2];
1022
				}
1023
				elseif (preg_match('/D(\d+) (.*)/', $recurence, $recurenceMatches))
1024
				{
1025
					$vcardData['recur_interval'] = $recurenceMatches[1];
1026
					$vcardData['recur_enddate'] = self::parseIcalDateTime(trim($recurenceMatches[2]));
1027
				}
1028
				else break;
1029
				// fall-through
1030
			case 'DAILY':	// 2.0
1031
				$vcardData['recur_type'] = self::DAILY;
1032
				if (stripos($recurence, 'BYDAY') === false) break;
1033
				// hack to handle TYPE=DAILY;BYDAY= as WEEKLY, which is true as long as there's no interval
1034
				// fall-through
1035
			case 'W':
1036
			case 'WEEKLY':
1037
				$days = array();
1038
				if (preg_match('/W(\d+) *((?i: [AEFHMORSTUW]{2})+)?( +([^ ]*))$/',$recurence, $recurenceMatches))		// 1.0
1039
				{
1040
					$vcardData['recur_interval'] = $recurenceMatches[1];
1041
					if (empty($recurenceMatches[2]))
1042
					{
1043
						$days[0] = strtoupper(substr(date('D', $vcardData['start']),0,2));
1044
					}
1045
					else
1046
					{
1047
						$days = explode(' ',trim($recurenceMatches[2]));
1048
					}
1049
1050
					$repeatMatches = null;
1051
					if (preg_match('/#(\d+)/',$recurenceMatches[4],$repeatMatches))
1052
					{
1053
						if ($repeatMatches[1]) $vcardData['recur_count'] = $repeatMatches[1];
1054
					}
1055
					else
1056
					{
1057
						$vcardData['recur_enddate'] = self::parseIcalDateTime($recurenceMatches[4]);
1058
					}
1059
				}
1060
				elseif (preg_match('/BYDAY=([^;: ]+)/',$recurence,$recurenceMatches))	// 2.0
1061
				{
1062
					$days = explode(',',$recurenceMatches[1]);
1063
				}
1064
				else	// no day given, use the day of dtstart
1065
				{
1066
					$vcardData['recur_data'] |= 1 << (int)date('w',$vcardData['start']);
1067
					$vcardData['recur_type'] = self::WEEKLY;
1068
				}
1069
				if ($days)
1070
				{
1071
					foreach (self::$days as $id => $day)
1072
					{
1073
						if (in_array(strtoupper(substr($day,0,2)),$days))
1074
						{
1075
							$vcardData['recur_data'] |= $id;
1076
						}
1077
					}
1078
					$vcardData['recur_type'] = self::WEEKLY;
1079
				}
1080
				break;
1081
1082
			case 'M':
1083
				if (preg_match('/MD(\d+)(?: [^ ]+)? #(\d+)/', $recurence, $recurenceMatches))
1084
				{
1085
					$vcardData['recur_type'] = self::MONTHLY_MDAY;
1086
					$vcardData['recur_interval'] = $recurenceMatches[1];
1087
					$vcardData['recur_count'] = $recurenceMatches[2];
1088
				}
1089
				elseif (preg_match('/MD(\d+)(?: [^ ]+)? ([0-9TZ]+)/',$recurence, $recurenceMatches))
1090
				{
1091
					$vcardData['recur_type'] = self::MONTHLY_MDAY;
1092
					$vcardData['recur_interval'] = $recurenceMatches[1];
1093
					$vcardData['recur_enddate'] = self::parseIcalDateTime($recurenceMatches[2]);
1094
				}
1095
				elseif (preg_match('/MP(\d+) (.*) (.*) (.*)/',$recurence, $recurenceMatches))
1096
				{
1097
					$vcardData['recur_type'] = self::MONTHLY_WDAY;
1098
					$vcardData['recur_interval'] = $recurenceMatches[1];
1099
					if (preg_match('/#(\d+)/',$recurenceMatches[4],$recurenceMatches))
1100
					{
1101
						$vcardData['recur_count'] = $recurenceMatches[1];
1102
					}
1103
					else
1104
					{
1105
						$vcardData['recur_enddate'] = self::parseIcalDateTime(trim($recurenceMatches[4]));
1106
					}
1107
				}
1108
				break;
1109
1110
			case 'Y':		// 1.0
1111
				if (preg_match('/YM(\d+)(?: [^ ]+)? #(\d+)/', $recurence, $recurenceMatches))
1112
				{
1113
					$vcardData['recur_interval'] = $recurenceMatches[1];
1114
					$vcardData['recur_count'] = $recurenceMatches[2];
1115
				}
1116
				elseif (preg_match('/YM(\d+)(?: [^ ]+)? ([0-9TZ]+)/',$recurence, $recurenceMatches))
1117
				{
1118
					$vcardData['recur_interval'] = $recurenceMatches[1];
1119
					$vcardData['recur_enddate'] = self::parseIcalDateTime($recurenceMatches[2]);
1120
				} else break;
1121
				// fall-through
1122
			case 'YEARLY':	// 2.0
1123
				if (strpos($recurence, 'BYDAY') === false)
1124
				{
1125
					$vcardData['recur_type'] = self::YEARLY;
1126
					break;
1127
				}
1128
				// handle FREQ=YEARLY;BYDAY= as FREQ=MONTHLY;BYDAY= with 12*INTERVAL
1129
				$vcardData['recur_interval'] = $vcardData['recur_interval'] ?
1130
					12*$vcardData['recur_interval'] : 12;
1131
				// fall-through
1132
			case 'MONTHLY':
1133
				// does currently NOT parse BYDAY or BYMONTH, it has to be specified/identical to DTSTART
1134
				$vcardData['recur_type'] = strpos($recurence,'BYDAY') !== false ?
1135
					self::MONTHLY_WDAY : self::MONTHLY_MDAY;
1136
				break;
1137
			case 'HOURLY':
1138
				if ($support_below_daily) $vcardData['recur_type'] = self::HOURLY;
1139
				break;
1140
			case 'MINUTELY':
1141
				if ($support_below_daily) $vcardData['recur_type'] = self::MINUTELY;
1142
				break;
1143
		}
1144
		return $vcardData;
1145
	}
1146
}
1147
1148
if (isset($_SERVER['SCRIPT_FILENAME']) && $_SERVER['SCRIPT_FILENAME'] == __FILE__)	// some tests
1149
{
1150
	ini_set('display_errors',1);
1151
	error_reporting(E_ALL & ~E_NOTICE);
1152
	function lang($str) { return $str; }
1153
	$GLOBALS['egw_info']['user']['preferences']['common']['tz'] = $_REQUEST['user-tz'] ? $_REQUEST['user-tz'] : 'Europe/Berlin';
1154
	require_once('../../api/src/autoload.php');
1155
1156
	if (!isset($_REQUEST['time']))
1157
	{
1158
		$now = new Api\DateTime('now',new DateTimeZone($_REQUEST['tz'] = 'UTC'));
1159
		$_REQUEST['time'] = $now->format();
1160
		$_REQUEST['type'] = calendar_rrule::WEEKLY;
1161
		$_REQUEST['interval'] = 2;
1162
		$now->modify('2 month');
1163
		$_REQUEST['enddate'] = $now->format('Y-m-d');
1164
		$_REQUEST['user-tz'] = 'Europe/Berlin';
1165
	}
1166
	echo "<html>\n<head>\n\t<title>Test calendar_rrule class</title>\n</head>\n<body>\n<form method='GET'>\n";
1167
	echo "<p>Date+Time: ".Api\Html::input('time',$_REQUEST['time']).
1168
		Api\Html::select('tz',$_REQUEST['tz'],Api\DateTime::getTimezones())."</p>\n";
1169
	echo "<p>Type: ".Api\Html::select('type',$_REQUEST['type'],calendar_rrule::$types)."\n".
1170
		"Interval: ".Api\Html::input('interval',$_REQUEST['interval'])."</p>\n";
1171
	echo "<table><tr><td>\n";
1172
	echo "Weekdays:<br />".Api\Html::checkbox_multiselect('weekdays',$_REQUEST['weekdays'],calendar_rrule::$days,false,'','7',false,'height: 150px;')."\n";
1173
	echo "</td><td>\n";
1174
	echo "<p>Exceptions:<br />".Api\Html::textarea('exceptions',$_REQUEST['exceptions'],'style="height: 150px;"')."\n";
1175
	echo "</td></tr></table>\n";
1176
	echo "<p>Enddate: ".Api\Html::input('enddate',$_REQUEST['enddate'])."</p>\n";
1177
	echo "<p>Display recurances in ".Api\Html::select('user-tz',$_REQUEST['user-tz'],Api\DateTime::getTimezones())."</p>\n";
1178
	echo "<p>".Api\Html::submit_button('calc','Calculate')."</p>\n";
1179
	echo "</form>\n";
1180
1181
	$tz = new DateTimeZone($_REQUEST['tz']);
1182
	$time = new Api\DateTime($_REQUEST['time'],$tz);
1183
	if ($_REQUEST['enddate']) $enddate = new Api\DateTime($_REQUEST['enddate'],$tz);
1184
	$weekdays = 0; foreach((array)$_REQUEST['weekdays'] as $mask) { $weekdays |= $mask; }
1185
	if ($_REQUEST['exceptions']) foreach(preg_split("/[,\r\n]+ ?/",$_REQUEST['exceptions']) as $exception) { $exceptions[] = new Api\DateTime($exception); }
1186
1187
	$rrule = new calendar_rrule($time,$_REQUEST['type'],$_REQUEST['interval'],$enddate,$weekdays,$exceptions);
1188
	echo "<h3>".$time->format('l').', '.$time.' ('.$tz->getName().') '.$rrule."</h3>\n";
1189
	foreach($rrule as $rtime)
1190
	{
1191
		$rtime->setTimezone(Api\DateTime::$user_timezone);
1192
		echo ++$n.': '.$rtime->format('l').', '.$rtime."<br />\n";
1193
	}
1194
	echo "</body>\n</html>\n";
1195
}