Test Failed
Push — main ( c8394f...8477f1 )
by Rafael
66:21
created

DateTimeParser::parseDuration()   F

Complexity

Conditions 23
Paths 1453

Size

Total Lines 91
Code Lines 49

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 23
eloc 49
nc 1453
nop 2
dl 0
loc 91
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
namespace Sabre\VObject;
4
5
use DateInterval;
6
use DateTimeImmutable;
7
use DateTimeZone;
8
9
/**
10
 * DateTimeParser.
11
 *
12
 * This class is responsible for parsing the several different date and time
13
 * formats iCalendar and vCards have.
14
 *
15
 * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
16
 * @author Evert Pot (http://evertpot.com/)
17
 * @license http://sabre.io/license/ Modified BSD License
18
 */
19
class DateTimeParser
20
{
21
    /**
22
     * Parses an iCalendar (rfc5545) formatted datetime and returns a
23
     * DateTimeImmutable object.
24
     *
25
     * Specifying a reference timezone is optional. It will only be used
26
     * if the non-UTC format is used. The argument is used as a reference, the
27
     * returned DateTimeImmutable object will still be in the UTC timezone.
28
     *
29
     * @param string       $dt
30
     * @param DateTimeZone $tz
31
     *
32
     * @return DateTimeImmutable
33
     */
34
    public static function parseDateTime($dt, DateTimeZone $tz = null)
35
    {
36
        // Format is YYYYMMDD + "T" + hhmmss
37
        $result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])T([0-2][0-9])([0-5][0-9])([0-5][0-9])([Z]?)$/', $dt, $matches);
38
39
        if (!$result) {
40
            throw new InvalidDataException('The supplied iCalendar datetime value is incorrect: ' . $dt);
41
        }
42
43
        if ('Z' === $matches[7] || is_null($tz)) {
44
            $tz = new DateTimeZone('UTC');
45
        }
46
47
        try {
48
            $date = new DateTimeImmutable($matches[1] . '-' . $matches[2] . '-' . $matches[3] . ' ' . $matches[4] . ':' . $matches[5] . ':' . $matches[6], $tz);
49
        } catch (\Exception $e) {
50
            throw new InvalidDataException('The supplied iCalendar datetime value is incorrect: ' . $dt);
51
        }
52
53
        return $date;
54
    }
55
56
    /**
57
     * Parses an iCalendar (rfc5545) formatted date and returns a DateTimeImmutable object.
58
     *
59
     * @param string       $date
60
     * @param DateTimeZone $tz
61
     *
62
     * @return DateTimeImmutable
63
     */
64
    public static function parseDate($date, DateTimeZone $tz = null)
65
    {
66
        // Format is YYYYMMDD
67
        $result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])$/', $date, $matches);
68
69
        if (!$result) {
70
            throw new InvalidDataException('The supplied iCalendar date value is incorrect: ' . $date);
71
        }
72
73
        if (is_null($tz)) {
74
            $tz = new DateTimeZone('UTC');
75
        }
76
77
        try {
78
            $date = new DateTimeImmutable($matches[1] . '-' . $matches[2] . '-' . $matches[3], $tz);
79
        } catch (\Exception $e) {
80
            throw new InvalidDataException('The supplied iCalendar date value is incorrect: ' . $date);
81
        }
82
83
        return $date;
84
    }
85
86
    /**
87
     * Parses an iCalendar (RFC5545) formatted duration value.
88
     *
89
     * This method will either return a DateTimeInterval object, or a string
90
     * suitable for strtotime or DateTime::modify.
91
     *
92
     * @param string $duration
93
     * @param bool   $asString
94
     *
95
     * @return DateInterval|string
96
     */
97
    public static function parseDuration($duration, $asString = false)
98
    {
99
        $result = preg_match('/^(?<plusminus>\+|-)?P((?<week>\d+)W)?((?<day>\d+)D)?(T((?<hour>\d+)H)?((?<minute>\d+)M)?((?<second>\d+)S)?)?$/', $duration, $matches);
100
        if (!$result) {
101
            throw new InvalidDataException('The supplied iCalendar duration value is incorrect: ' . $duration);
102
        }
103
104
        if (!$asString) {
105
            $invert = false;
106
107
            if (isset($matches['plusminus']) && '-' === $matches['plusminus']) {
108
                $invert = true;
109
            }
110
111
            $parts = [
112
                'week',
113
                'day',
114
                'hour',
115
                'minute',
116
                'second',
117
            ];
118
119
            foreach ($parts as $part) {
120
                $matches[$part] = isset($matches[$part]) && $matches[$part] ? (int) $matches[$part] : 0;
121
            }
122
123
            // We need to re-construct the $duration string, because weeks and
124
            // days are not supported by DateInterval in the same string.
125
            $duration = 'P';
126
            $days = $matches['day'];
127
128
            if ($matches['week']) {
129
                $days += $matches['week'] * 7;
130
            }
131
132
            if ($days) {
133
                $duration .= $days . 'D';
134
            }
135
136
            if ($matches['minute'] || $matches['second'] || $matches['hour']) {
137
                $duration .= 'T';
138
139
                if ($matches['hour']) {
140
                    $duration .= $matches['hour'] . 'H';
141
                }
142
143
                if ($matches['minute']) {
144
                    $duration .= $matches['minute'] . 'M';
145
                }
146
147
                if ($matches['second']) {
148
                    $duration .= $matches['second'] . 'S';
149
                }
150
            }
151
152
            if ('P' === $duration) {
153
                $duration = 'PT0S';
154
            }
155
156
            $iv = new DateInterval($duration);
157
158
            if ($invert) {
159
                $iv->invert = true;
160
            }
161
162
            return $iv;
163
        }
164
165
        $parts = [
166
            'week',
167
            'day',
168
            'hour',
169
            'minute',
170
            'second',
171
        ];
172
173
        $newDur = '';
174
175
        foreach ($parts as $part) {
176
            if (isset($matches[$part]) && $matches[$part]) {
177
                $newDur .= ' ' . $matches[$part] . ' ' . $part . 's';
178
            }
179
        }
180
181
        $newDur = ('-' === $matches['plusminus'] ? '-' : '+') . trim($newDur);
182
183
        if ('+' === $newDur) {
184
            $newDur = '+0 seconds';
185
        }
186
187
        return $newDur;
188
    }
189
190
    /**
191
     * Parses either a Date or DateTime, or Duration value.
192
     *
193
     * @param string              $date
194
     * @param DateTimeZone|string $referenceTz
195
     *
196
     * @return DateTimeImmutable|DateInterval
197
     */
198
    public static function parse($date, $referenceTz = null)
199
    {
200
        if ('P' === $date[0] || ('-' === $date[0] && 'P' === $date[1])) {
201
            return self::parseDuration($date);
202
        } elseif (8 === strlen($date)) {
203
            return self::parseDate($date, $referenceTz);
204
        } else {
205
            return self::parseDateTime($date, $referenceTz);
206
        }
207
    }
208
209
    /**
210
     * This method parses a vCard date and or time value.
211
     *
212
     * This can be used for the DATE, DATE-TIME, TIMESTAMP and
213
     * DATE-AND-OR-TIME value.
214
     *
215
     * This method returns an array, not a DateTime value.
216
     *
217
     * The elements in the array are in the following order:
218
     * year, month, date, hour, minute, second, timezone
219
     *
220
     * Almost any part of the string may be omitted. It's for example legal to
221
     * just specify seconds, leave out the year, etc.
222
     *
223
     * Timezone is either returned as 'Z' or as '+0800'
224
     *
225
     * For any non-specified values null is returned.
226
     *
227
     * List of date formats that are supported:
228
     * YYYY
229
     * YYYY-MM
230
     * YYYYMMDD
231
     * --MMDD
232
     * ---DD
233
     *
234
     * YYYY-MM-DD
235
     * --MM-DD
236
     * ---DD
237
     *
238
     * List of supported time formats:
239
     *
240
     * HH
241
     * HHMM
242
     * HHMMSS
243
     * -MMSS
244
     * --SS
245
     *
246
     * HH
247
     * HH:MM
248
     * HH:MM:SS
249
     * -MM:SS
250
     * --SS
251
     *
252
     * A full basic-format date-time string looks like :
253
     * 20130603T133901
254
     *
255
     * A full extended-format date-time string looks like :
256
     * 2013-06-03T13:39:01
257
     *
258
     * Times may be postfixed by a timezone offset. This can be either 'Z' for
259
     * UTC, or a string like -0500 or +1100.
260
     *
261
     * @param string $date
262
     *
263
     * @return array
264
     */
265
    public static function parseVCardDateTime($date)
266
    {
267
        $regex = '/^
268
            (?:  # date part
269
                (?:
270
                    (?: (?<year> [0-9]{4}) (?: -)?| --)
271
                    (?<month> [0-9]{2})?
272
                |---)
273
                (?<date> [0-9]{2})?
274
            )?
275
            (?:T  # time part
276
                (?<hour> [0-9]{2} | -)
277
                (?<minute> [0-9]{2} | -)?
278
                (?<second> [0-9]{2})?
279
280
                (?: \.[0-9]{3})? # milliseconds
281
                (?P<timezone> # timezone offset
282
283
                    Z | (?: \+|-)(?: [0-9]{4})
284
285
                )?
286
287
            )?
288
            $/x';
289
290
        if (!preg_match($regex, $date, $matches)) {
291
            // Attempting to parse the extended format.
292
            $regex = '/^
293
                (?: # date part
294
                    (?: (?<year> [0-9]{4}) - | -- )
295
                    (?<month> [0-9]{2}) -
296
                    (?<date> [0-9]{2})
297
                )?
298
                (?:T # time part
299
300
                    (?: (?<hour> [0-9]{2}) : | -)
301
                    (?: (?<minute> [0-9]{2}) : | -)?
302
                    (?<second> [0-9]{2})?
303
304
                    (?: \.[0-9]{3})? # milliseconds
305
                    (?P<timezone> # timezone offset
306
307
                        Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2})
308
309
                    )?
310
311
                )?
312
                $/x';
313
314
            if (!preg_match($regex, $date, $matches)) {
315
                throw new InvalidDataException('Invalid vCard date-time string: ' . $date);
316
            }
317
        }
318
        $parts = [
319
            'year',
320
            'month',
321
            'date',
322
            'hour',
323
            'minute',
324
            'second',
325
            'timezone',
326
        ];
327
328
        $result = [];
329
        foreach ($parts as $part) {
330
            if (empty($matches[$part])) {
331
                $result[$part] = null;
332
            } elseif ('-' === $matches[$part] || '--' === $matches[$part]) {
333
                $result[$part] = null;
334
            } else {
335
                $result[$part] = $matches[$part];
336
            }
337
        }
338
339
        return $result;
340
    }
341
342
    /**
343
     * This method parses a vCard TIME value.
344
     *
345
     * This method returns an array, not a DateTime value.
346
     *
347
     * The elements in the array are in the following order:
348
     * hour, minute, second, timezone
349
     *
350
     * Almost any part of the string may be omitted. It's for example legal to
351
     * just specify seconds, leave out the hour etc.
352
     *
353
     * Timezone is either returned as 'Z' or as '+08:00'
354
     *
355
     * For any non-specified values null is returned.
356
     *
357
     * List of supported time formats:
358
     *
359
     * HH
360
     * HHMM
361
     * HHMMSS
362
     * -MMSS
363
     * --SS
364
     *
365
     * HH
366
     * HH:MM
367
     * HH:MM:SS
368
     * -MM:SS
369
     * --SS
370
     *
371
     * A full basic-format time string looks like :
372
     * 133901
373
     *
374
     * A full extended-format time string looks like :
375
     * 13:39:01
376
     *
377
     * Times may be postfixed by a timezone offset. This can be either 'Z' for
378
     * UTC, or a string like -0500 or +11:00.
379
     *
380
     * @param string $date
381
     *
382
     * @return array
383
     */
384
    public static function parseVCardTime($date)
385
    {
386
        $regex = '/^
387
            (?<hour> [0-9]{2} | -)
388
            (?<minute> [0-9]{2} | -)?
389
            (?<second> [0-9]{2})?
390
391
            (?: \.[0-9]{3})? # milliseconds
392
            (?P<timezone> # timezone offset
393
394
                Z | (?: \+|-)(?: [0-9]{4})
395
396
            )?
397
            $/x';
398
399
        if (!preg_match($regex, $date, $matches)) {
400
            // Attempting to parse the extended format.
401
            $regex = '/^
402
                (?: (?<hour> [0-9]{2}) : | -)
403
                (?: (?<minute> [0-9]{2}) : | -)?
404
                (?<second> [0-9]{2})?
405
406
                (?: \.[0-9]{3})? # milliseconds
407
                (?P<timezone> # timezone offset
408
409
                    Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2})
410
411
                )?
412
                $/x';
413
414
            if (!preg_match($regex, $date, $matches)) {
415
                throw new InvalidDataException('Invalid vCard time string: ' . $date);
416
            }
417
        }
418
        $parts = [
419
            'hour',
420
            'minute',
421
            'second',
422
            'timezone',
423
        ];
424
425
        $result = [];
426
        foreach ($parts as $part) {
427
            if (empty($matches[$part])) {
428
                $result[$part] = null;
429
            } elseif ('-' === $matches[$part]) {
430
                $result[$part] = null;
431
            } else {
432
                $result[$part] = $matches[$part];
433
            }
434
        }
435
436
        return $result;
437
    }
438
439
    /**
440
     * This method parses a vCard date and or time value.
441
     *
442
     * This can be used for the DATE, DATE-TIME and
443
     * DATE-AND-OR-TIME value.
444
     *
445
     * This method returns an array, not a DateTime value.
446
     * The elements in the array are in the following order:
447
     *     year, month, date, hour, minute, second, timezone
448
     * Almost any part of the string may be omitted. It's for example legal to
449
     * just specify seconds, leave out the year, etc.
450
     *
451
     * Timezone is either returned as 'Z' or as '+0800'
452
     *
453
     * For any non-specified values null is returned.
454
     *
455
     * List of date formats that are supported:
456
     *     20150128
457
     *     2015-01
458
     *     --01
459
     *     --0128
460
     *     ---28
461
     *
462
     * List of supported time formats:
463
     *     13
464
     *     1353
465
     *     135301
466
     *     -53
467
     *     -5301
468
     *     --01 (unreachable, see the tests)
469
     *     --01Z
470
     *     --01+1234
471
     *
472
     * List of supported date-time formats:
473
     *     20150128T13
474
     *     --0128T13
475
     *     ---28T13
476
     *     ---28T1353
477
     *     ---28T135301
478
     *     ---28T13Z
479
     *     ---28T13+1234
480
     *
481
     * See the regular expressions for all the possible patterns.
482
     *
483
     * Times may be postfixed by a timezone offset. This can be either 'Z' for
484
     * UTC, or a string like -0500 or +1100.
485
     *
486
     * @param string $date
487
     *
488
     * @return array
489
     */
490
    public static function parseVCardDateAndOrTime($date)
491
    {
492
        // \d{8}|\d{4}-\d\d|--\d\d(\d\d)?|---\d\d
493
        $valueDate = '/^(?J)(?:' .
494
                         '(?<year>\d{4})(?<month>\d\d)(?<date>\d\d)' .
495
                         '|(?<year>\d{4})-(?<month>\d\d)' .
496
                         '|--(?<month>\d\d)(?<date>\d\d)?' .
497
                         '|---(?<date>\d\d)' .
498
                         ')$/';
499
500
        // (\d\d(\d\d(\d\d)?)?|-\d\d(\d\d)?|--\d\d)(Z|[+\-]\d\d(\d\d)?)?
501
        $valueTime = '/^(?J)(?:' .
502
                         '((?<hour>\d\d)((?<minute>\d\d)(?<second>\d\d)?)?' .
503
                         '|-(?<minute>\d\d)(?<second>\d\d)?' .
504
                         '|--(?<second>\d\d))' .
505
                         '(?<timezone>(Z|[+\-]\d\d(\d\d)?))?' .
506
                         ')$/';
507
508
        // (\d{8}|--\d{4}|---\d\d)T\d\d(\d\d(\d\d)?)?(Z|[+\-]\d\d(\d\d?)?
509
        $valueDateTime = '/^(?:' .
510
                         '((?<year0>\d{4})(?<month0>\d\d)(?<date0>\d\d)' .
511
                         '|--(?<month1>\d\d)(?<date1>\d\d)' .
512
                         '|---(?<date2>\d\d))' .
513
                         'T' .
514
                         '(?<hour>\d\d)((?<minute>\d\d)(?<second>\d\d)?)?' .
515
                         '(?<timezone>(Z|[+\-]\d\d(\d\d?)))?' .
516
                         ')$/';
517
518
        // date-and-or-time is date | date-time | time
519
        // in this strict order.
520
521
        if (
522
            0 === preg_match($valueDate, $date, $matches)
523
            && 0 === preg_match($valueDateTime, $date, $matches)
524
            && 0 === preg_match($valueTime, $date, $matches)
525
        ) {
526
            throw new InvalidDataException('Invalid vCard date-time string: ' . $date);
527
        }
528
529
        $parts = [
530
            'year' => null,
531
            'month' => null,
532
            'date' => null,
533
            'hour' => null,
534
            'minute' => null,
535
            'second' => null,
536
            'timezone' => null,
537
        ];
538
539
        // The $valueDateTime expression has a bug with (?J) so we simulate it.
540
        $parts['date0'] = &$parts['date'];
541
        $parts['date1'] = &$parts['date'];
542
        $parts['date2'] = &$parts['date'];
543
        $parts['month0'] = &$parts['month'];
544
        $parts['month1'] = &$parts['month'];
545
        $parts['year0'] = &$parts['year'];
546
547
        foreach ($parts as $part => &$value) {
548
            if (!empty($matches[$part])) {
549
                $value = $matches[$part];
550
            }
551
        }
552
553
        unset($parts['date0']);
554
        unset($parts['date1']);
555
        unset($parts['date2']);
556
        unset($parts['month0']);
557
        unset($parts['month1']);
558
        unset($parts['year0']);
559
560
        return $parts;
561
    }
562
}
563