Completed
Push — master ( 19a7d3...6b997b )
by Tim
13:45
created

ICal   D

Complexity

Total Complexity 82

Size/Duplication

Total Lines 447
Duplicated Lines 6.71 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
wmc 82
c 0
b 0
f 0
lcom 1
cbo 0
dl 30
loc 447
rs 4.8717

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ICal often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ICal, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This PHP-Class should only read a iCal-File (*.ics), parse it and give an 
4
 * array with its content.
5
 *
6
 * PHP Version 5
7
 *
8
 * @category Parser
9
 * @package  Ics-parser
10
 * @author   Martin Thoma <[email protected]>
11
 * @license  http://www.opensource.org/licenses/mit-license.php  MIT License
12
 * @version  SVN: <svn_id>
13
 * @link     http://code.google.com/p/ics-parser/
14
 * @example  $ical = new ical('MyCal.ics');
15
 *           print_r( $ical->events() );
16
 */
17
18
// error_reporting(E_ALL);
19
20
/**
21
 * This is the iCal-class
22
 *
23
 * @category Parser
24
 * @package  Ics-parser
25
 * @author   Martin Thoma <[email protected]>
26
 * @license  http://www.opensource.org/licenses/mit-license.php  MIT License
27
 * @link     http://code.google.com/p/ics-parser/
28
 *
29
 * @param {string} filename The name of the file which should be parsed
30
 * @constructor
31
 */
32
class ICal
33
{
34
    /* How many ToDos are in this ical? */
35
    public  /** @type {int} */ $todo_count = 0;
36
37
    /* How many events are in this ical? */
38
    public  /** @type {int} */ $event_count = 0; 
39
40
    /* The parsed calendar */
41
    public /** @type {Array} */ $cal;
42
43
    /* Which keyword has been added to cal at last? */
44
    private /** @type {string} */ $_lastKeyWord;
45
46
    /** 
47
     * Creates the iCal-Object
48
     * 
49
     * @param {string} $filename The path to the iCal-file
50
     *
51
     * @return Object The iCal-Object
52
     */ 
53
    public function __construct($filename) 
54
    {
55
        if (!$filename) {
56
            return false;
57
        }
58
        
59
        $lines = file($filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
60
        if (stristr($lines[0], 'BEGIN:VCALENDAR') === false) {
61
            return false;
62
        } else {
63
            // TODO: Fix multiline-description problem (see http://tools.ietf.org/html/rfc2445#section-4.8.1.5)
64
            foreach ($lines as $line) {
65
                $line = trim($line);
66
                $add  = $this->keyValueFromString($line);
67
                if ($add === false) {
68
                    $this->addCalendarComponentWithKeyAndValue($type, false, $line);
69
                    continue;
70
                } 
71
72
                list($keyword, $value) = $add;
73
74
                switch ($line) {
75
                // http://www.kanzaki.com/docs/ical/vtodo.html
76
                case "BEGIN:VTODO": 
77
                    $this->todo_count++;
78
                    $type = "VTODO"; 
79
                    break; 
80
81
                // http://www.kanzaki.com/docs/ical/vevent.html
82
                case "BEGIN:VEVENT": 
83
                    $this->event_count++;
84
                    $type = "VEVENT"; 
85
                    break; 
86
87
                //all other special strings
88
                case "BEGIN:VCALENDAR": 
89
                case "BEGIN:DAYLIGHT": 
90
                    // http://www.kanzaki.com/docs/ical/vtimezone.html
91
                case "BEGIN:VTIMEZONE": 
92
                case "BEGIN:STANDARD": 
93
                    $type = $value;
94
                    break; 
95
                case "END:VTODO": // end special text - goto VCALENDAR key 
96
                case "END:VEVENT": 
97
                case "END:VCALENDAR": 
98
                case "END:DAYLIGHT": 
99
                case "END:VTIMEZONE": 
100
                case "END:STANDARD": 
101
                    $type = "VCALENDAR"; 
102
                    break; 
103
                default:
104
                    $this->addCalendarComponentWithKeyAndValue($type, 
105
                                                               $keyword, 
106
                                                               $value);
107
                    break; 
108
                } 
109
            }
110
            $this->process_recurrences();
111
            return $this->cal; 
112
        }
113
    }
114
115
    /** 
116
     * Add to $this->ical array one value and key.
117
     * 
118
     * @param {string} $component This could be VTODO, VEVENT, VCALENDAR, ... 
119
     * @param {string} $keyword   The keyword, for example DTSTART
120
     * @param {string} $value     The value, for example 20110105T090000Z
121
     *
122
     * @return {None}
123
     */ 
124
    public function addCalendarComponentWithKeyAndValue($component, 
125
                                                        $keyword, 
126
                                                        $value) 
127
    {
128
        if (strstr($keyword, ';')) {
129
          // Ignore everything in keyword after a ; (things like Language, etc)
130
          $keyword = substr($keyword, 0, strpos($keyword, ";"));
131
        }
132
        if ($keyword == false) { 
133
            $keyword = $this->last_keyword; 
134
            switch ($component) {
135
            case 'VEVENT': 
136
                $value = $this->cal[$component][$this->event_count - 1]
137
                                               [$keyword].$value;
138
                break;
139
            case 'VTODO' : 
140
                $value = $this->cal[$component][$this->todo_count - 1]
141
                                               [$keyword].$value;
142
                break;
143
            }
144
        }
145
        
146
        if (stristr($keyword, "DTSTART") or stristr($keyword, "DTEND")) {
147
            $keyword = explode(";", $keyword);
148
            $keyword = $keyword[0];
149
        }
150
151
        switch ($component) { 
152
        case "VTODO": 
153
            $this->cal[$component][$this->todo_count - 1][$keyword] = $value;
154
            //$this->cal[$component][$this->todo_count]['Unix'] = $unixtime;
155
            break; 
156
        case "VEVENT": 
157
            $this->cal[$component][$this->event_count - 1][$keyword] = $value; 
158
            break; 
159
        default: 
160
            $this->cal[$component][$keyword] = $value; 
161
            break; 
162
        } 
163
        $this->last_keyword = $keyword; 
164
    }
165
166
    /**
167
     * Get a key-value pair of a string.
168
     *
169
     * @param {string} $text which is like "VCALENDAR:Begin" or "LOCATION:"
170
     *
171
     * @return {array} array("VCALENDAR", "Begin")
172
     */
173
    public function keyValueFromString($text) 
174
    {
175
        preg_match("/([^:]+)[:]([\w\W]*)/", $text, $matches);
176
        if (count($matches) == 0) {
177
            return false;
178
        }
179
        $matches = array_splice($matches, 1, 2);
180
        return $matches;
181
    }
182
183
    /** 
184
     * Return Unix timestamp from ical date time format 
185
     * 
186
     * @param {string} $icalDate A Date in the format YYYYMMDD[T]HHMMSS[Z] or
187
     *                           YYYYMMDD[T]HHMMSS
188
     *
189
     * @return {int} 
190
     */ 
191
    public function iCalDateToUnixTimestamp($icalDate) 
192
    { 
193
        $icalDate = str_replace('T', '', $icalDate); 
194
        $icalDate = str_replace('Z', '', $icalDate); 
195
196
        $pattern  = '/([0-9]{4})';   // 1: YYYY
197
        $pattern .= '([0-9]{2})';    // 2: MM
198
        $pattern .= '([0-9]{2})';    // 3: DD
199
        $pattern .= '([0-9]{0,2})';  // 4: HH
200
        $pattern .= '([0-9]{0,2})';  // 5: MM
201
        $pattern .= '([0-9]{0,2})/'; // 6: SS
202
        preg_match($pattern, $icalDate, $date); 
203
204
        // Unix timestamp can't represent dates before 1970
205
        if ($date[1] <= 1970) {
206
            return false;
207
        } 
208
        // Unix timestamps after 03:14:07 UTC 2038-01-19 might cause an overflow
209
        // if 32 bit integers are used.
210
        $timestamp = mktime((int)$date[4], 
211
                            (int)$date[5], 
212
                            (int)$date[6], 
213
                            (int)$date[2],
214
                            (int)$date[3], 
215
                            (int)$date[1]);
216
        return  $timestamp;
217
    } 
218
    
219
    /**
220
     * Processes recurrences
221
     *
222
     * @author John Grogg <[email protected]>
223
     * @return {array}
224
     */
225
    public function process_recurrences() 
226
    {
227
        $array = $this->cal;
228
        $events = $array['VEVENT'];
229
        foreach ($array['VEVENT'] as $anEvent) {
230
          if (isset($anEvent['RRULE']) && $anEvent['RRULE'] != '') {
231
            // Recurring event, parse RRULE and add appropriate duplicate events
232
            $rrules = array();
233
            $rrule_strings = explode(';',$anEvent['RRULE']);
234
            foreach ($rrule_strings as $s) {
235
              list($k,$v) = explode('=', $s);
236
              $rrules[$k] = $v;
237
            }
238
            // Get Start timestamp
239
            $start_timestamp = $this->iCalDateToUnixTimestamp($anEvent['DTSTART']);
240
            $end_timestamp = $this->iCalDateToUnixTimestamp($anEvent['DTEND']);
241
            $event_timestmap_offset = $end_timestamp - $start_timestamp;
242
            // Get Interval
243
            $interval = (isset($rrules['INTERVAL']) && $rrules['INTERVAL'] != '') ? $rrules['INTERVAL'] : 1;
244
            // Get Until
245
            $until = $this->iCalDateToUnixTimestamp($rrules['UNTIL']);
246
            // Decide how often to add events and do so
247
            switch ($rrules['FREQ']) {
248
              case 'DAILY':
249
                // Simply add a new event each interval of days until UNTIL is reached
250
                $offset = "+$interval day";
251
                $recurring_timestamp = strtotime($offset, $start_timestamp);
252
                while ($recurring_timestamp <= $until) {
253
                  // Add event
254
                  $anEvent['DTSTART'] = date('Ymd\THis',$recurring_timestamp);
255
                  $anEvent['DTEND'] = date('Ymd\THis',$recurring_timestamp+$event_timestmap_offset);
256
                  $events[] = $anEvent;
257
                  // Move forward
258
                  $recurring_timestamp = strtotime($offset,$recurring_timestamp);
259
                }
260
                break;
261
              case 'WEEKLY':
262
                // Create offset
263
                $offset = "+$interval week";
264
                // Build list of days of week to add events
265
                $weekdays = array('SU','MO','TU','WE','TH','FR','SA');
266
                $bydays = (isset($rrules['BYDAY']) && $rrules['BYDAY'] != '') ? explode(',', $rrules['BYDAY']) : array('SU','MO','TU','WE','TH','FR','SA');
267
                // Get timestamp of first day of start week
268
                $week_recurring_timestamp = (date('w', $start_timestamp) == 0) ? $start_timestamp : strtotime('last Sunday '.date('H:i:s',$start_timestamp), $start_timestamp);
269
                // Step through weeks
270
                while ($week_recurring_timestamp <= $until) {
271
                  // Add events for bydays
272
                  $day_recurring_timestamp = $week_recurring_timestamp;
273
                  foreach ($weekdays as $day) {
274
                    // Check if day should be added
275
                    if (in_array($day, $bydays) && $day_recurring_timestamp > $start_timestamp && $day_recurring_timestamp <= $until) {
276
                      // Add event to day
277
                      $anEvent['DTSTART'] = date('Ymd\THis',$day_recurring_timestamp);
278
                      $anEvent['DTEND'] = date('Ymd\THis',$day_recurring_timestamp+$event_timestmap_offset);
279
                      $events[] = $anEvent;
280
                    }
281
                    // Move forward a day
282
                    $day_recurring_timestamp = strtotime('+1 day',$day_recurring_timestamp);
283
                  }
284
                  // Move forward $interaval weeks
285
                  $week_recurring_timestamp = strtotime($offset,$week_recurring_timestamp);
286
                }
287
                break;
288
              case 'MONTHLY':
289
                // Create offset
290
                $offset = "+$interval month";
291
                $recurring_timestamp = strtotime($offset, $start_timestamp);
292
                if (isset($rrules['BYMONTHDAY']) && $rrules['BYMONTHDAY'] != '') {
293
                  // Deal with BYMONTHDAY
294
                  while ($recurring_timestamp <= $until) {
295
                    // Add event
296
                    $anEvent['DTSTART'] = date('Ym'.sprintf('%02d',$rrules['BYMONTHDAY']).'\THis',$recurring_timestamp);
297
                    $anEvent['DTEND'] = date('Ymd\THis',$this->iCalDateToUnixTimestamp($anEvent['DTSTART'])+$event_timestmap_offset);
298
                    $events[] = $anEvent;
299
                    // Move forward
300
                    $recurring_timestamp = strtotime($offset,$recurring_timestamp);
301
                  }
302
                } elseif (isset($rrules['BYDAY']) && $rrules['BYDAY'] != '') {
303
                  $start_time = date('His',$start_timestamp);
304
                  // Deal with BYDAY
305
                  $day_number = substr($rrules['BYDAY'], 0, 1);
306
                  $week_day = substr($rrules['BYDAY'], 1);
307
                  $day_cardinals = array(1 => 'first', 2 => 'second', 3 => 'third', 4 => 'fourth', 5 => 'fifth');
308
                  $weekdays = array('SU' => 'sunday','MO' => 'monday','TU' => 'tuesday','WE' => 'wednesday','TH' => 'thursday','FR' => 'friday','SA' => 'saturday');
309
                  while ($recurring_timestamp <= $until) {
310
                    $event_start_desc = "{$day_cardinals[$day_number]} {$weekdays[$week_day]} of ".date('F',$recurring_timestamp)." ".date('Y',$recurring_timestamp)." ".date('H:i:s',$recurring_timestamp);
311
                    $event_start_timestamp = strtotime($event_start_desc);
312
                    if ($event_start_timestamp > $start_timestamp && $event_start_timestamp < $until) {
313
                      $anEvent['DTSTART'] = date('Ymd\T',$event_start_timestamp).$start_time;
314
                      $anEvent['DTEND'] = date('Ymd\THis',$this->iCalDateToUnixTimestamp($anEvent['DTSTART'])+$event_timestmap_offset);
315
                      $events[] = $anEvent;
316
                    }
317
                    // Move forward
318
                    $recurring_timestamp = strtotime($offset,$recurring_timestamp);
319
                  }
320
                }
321
                break;
322
              case 'YEARLY':
323
                // Create offset
324
                $offset = "+$interval year";
325
                $recurring_timestamp = strtotime($offset, $start_timestamp);
326
                $month_names = array(1=>"January", 2=>"Februrary", 3=>"March", 4=>"April", 5=>"May", 6=>"June", 7=>"July", 8=>"August", 9=>"September", 10=>"October", 11=>"November", 12=>"December");
327
                // HACK: Exchange doesn't set a correct UNTIL for yearly events, so just go 2 years out
328
                $until = strtotime('+2 year',$start_timestamp);
329
                // Check if BYDAY rule exists
330
                if (isset($rrules['BYDAY']) && $rrules['BYDAY'] != '') {
331
                  $start_time = date('His',$start_timestamp);
332
                  // Deal with BYDAY
333
                  $day_number = substr($rrules['BYDAY'], 0, 1);
334
                  $month_day = substr($rrules['BYDAY'], 1);
335
                  $day_cardinals = array(1 => 'first', 2 => 'second', 3 => 'third', 4 => 'fourth', 5 => 'fifth');
336
                  $weekdays = array('SU' => 'sunday','MO' => 'monday','TU' => 'tuesday','WE' => 'wednesday','TH' => 'thursday','FR' => 'friday','SA' => 'saturday');
337
                  while ($recurring_timestamp <= $until) {
338
                    $event_start_desc = "{$day_cardinals[$day_number]} {$weekdays[$month_day]} of {$month_names[$rrules['BYMONTH']]} ".date('Y',$recurring_timestamp)." ".date('H:i:s',$recurring_timestamp);
339
                    $event_start_timestamp = strtotime($event_start_desc);
340
                    if ($event_start_timestamp > $start_timestamp && $event_start_timestamp < $until) {
341
                      $anEvent['DTSTART'] = date('Ymd\T',$event_start_timestamp).$start_time;
342
                      $anEvent['DTEND'] = date('Ymd\THis',$this->iCalDateToUnixTimestamp($anEvent['DTSTART'])+$event_timestmap_offset);
343
                      $events[] = $anEvent;
344
                    }
345
                    // Move forward
346
                    $recurring_timestamp = strtotime($offset,$recurring_timestamp);
347
                  }
348
                } else {
349
                  $day = date('d',$start_timestamp);
350
                  // Step throuhg years adding specific month dates
351
                  while ($recurring_timestamp <= $until) {
352
                    $event_start_desc = "$day {$month_names[$rrules['BYMONTH']]} ".date('Y',$recurring_timestamp)." ".date('H:i:s',$recurring_timestamp);
353
                    $event_start_timestamp = strtotime($event_start_desc);
354
                    if ($event_start_timestamp > $start_timestamp && $event_start_timestamp < $until) {
355
                      $anEvent['DTSTART'] = date('Ymd\T',$event_start_timestamp).$start_time;
356
                      $anEvent['DTEND'] = date('Ymd\THis',$this->iCalDateToUnixTimestamp($anEvent['DTSTART'])+$event_timestmap_offset);
357
                      $events[] = $anEvent;
358
                    }
359
                    // Move forward
360
                    $recurring_timestamp = strtotime($offset,$recurring_timestamp);
361
                  }
362
                }
363
                break;
364
            }
365
          }
366
        }
367
        $this->cal['VEVENT'] = $events;
368
    }
369
370
    /**
371
     * Returns an array of arrays with all events. Every event is an associative
372
     * array and each property is an element it.
373
     *
374
     * @return {array}
375
     */
376
    public function events() 
377
    {
378
        $array = $this->cal;
379
        return $array['VEVENT'];
380
    }
381
382
    /**
383
     * Returns a boolean value whether thr current calendar has events or not
384
     *
385
     * @return {boolean}
386
     */
387
    public function hasEvents() 
388
    {
389
        return ( count($this->events()) > 0 ? true : false );
390
    }
391
392
    /**
393
     * Returns false when the current calendar has no events in range, else the
394
     * events.
395
     * 
396
     * Note that this function makes use of a UNIX timestamp. This might be a 
397
     * problem on January the 29th, 2038.
398
     * See http://en.wikipedia.org/wiki/Unix_time#Representing_the_number
399
     *
400
     * @param {boolean} $rangeStart Either true or false
401
     * @param {boolean} $rangeEnd   Either true or false
402
     *
403
     * @return {mixed}
404
     */
405
    public function eventsFromRange($rangeStart = false, $rangeEnd = false) 
406
    {
407
        $events = $this->sortEventsWithOrder($this->events(), SORT_ASC);
408
409
        if (!$events) {
410
            return false;
411
        }
412
413
        $extendedEvents = array();
414
        
415
        if ($rangeStart === false) {
416
            $rangeStart = new DateTime();
417
        } else {
418
            $rangeStart = new DateTime($rangeStart);
419
        }
420
421
        if ($rangeEnd === false or $rangeEnd <= 0) {
422
            $rangeEnd = new DateTime('2038/01/18');
423
        } else {
424
            $rangeEnd = new DateTime($rangeEnd);
425
        }
426
427
        $rangeStart = $rangeStart->format('U');
428
        $rangeEnd   = $rangeEnd->format('U');
429
430
        
431
432
        // loop through all events by adding two new elements
433
        foreach ($events as $anEvent) {
434
            $timestamp = $this->iCalDateToUnixTimestamp($anEvent['DTSTART']);
435
            if ($timestamp >= $rangeStart && $timestamp <= $rangeEnd) {
436
                $extendedEvents[] = $anEvent;
437
            }
438
        }
439
440
        return $extendedEvents;
441
    }
442
443
    /**
444
     * Returns a boolean value whether thr current calendar has events or not
445
     *
446
     * @param {array} $events    An array with events.
447
     * @param {array} $sortOrder Either SORT_ASC, SORT_DESC, SORT_REGULAR, 
448
     *                           SORT_NUMERIC, SORT_STRING
449
     *
450
     * @return {boolean}
451
     */
452
    public function sortEventsWithOrder($events, $sortOrder = SORT_ASC)
453
    {
454
        $extendedEvents = array();
455
        
456
        // loop through all events by adding two new elements
457
        foreach ($events as $anEvent) {
458
            if (!array_key_exists('UNIX_TIMESTAMP', $anEvent)) {
459
                $anEvent['UNIX_TIMESTAMP'] = 
460
                            $this->iCalDateToUnixTimestamp($anEvent['DTSTART']);
461
            }
462
463
            if (!array_key_exists('REAL_DATETIME', $anEvent)) {
464
                $anEvent['REAL_DATETIME'] = 
465
                            date("d.m.Y", $anEvent['UNIX_TIMESTAMP']);
466
            }
467
            
468
            $extendedEvents[] = $anEvent;
469
        }
470
        
471
        foreach ($extendedEvents as $key => $value) {
472
            $timestamp[$key] = $value['UNIX_TIMESTAMP'];
473
        }
474
        array_multisort($timestamp, $sortOrder, $extendedEvents);
475
476
        return $extendedEvents;
477
    }
478
} 
479
?>
480