Passed
Push — master ( 156469...7634cf )
by Mars
01:57
created

TimePeriodHelper::extend()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 30
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 14
c 1
b 0
f 0
nc 4
nop 4
dl 0
loc 30
rs 9.7998
1
<?php
2
3
namespace marsapp\helper\timeperiod;
4
5
/**
6
 * Time Period Helper
7
 * 
8
 * Note:
9
 * 1. Format: $timePeriods = [[$startDatetime1, $endDatetime1], [$startDatetime2, $endDatetime2], ...];
10
 * - $Datetime = Y-m-d H:i:s ; Y-m-d H:i:00 ; Y-m-d H:00:00 ;
11
 * 2. If it is hour/minute/second, the end point is usually not included, for example, 8 o'clock to 9 o'clock is 1 hour.
12
 * 3. If it is a day/month/year, it usually includes an end point, for example, January to March is 3 months.
13
 * 4. When processing, assume that the $timePeriods format is correct. If necessary, you need to call the verification function to verify the data.
14
 * 5. Ensure performance by keeping the $timePeriods format correct:
15
 * - a. When getting the raw $timePeriods, sort out it by format(), filter(), union().
16
 * - b. Handle $timePeriods using only the functions provided by TimePeriodHelper (Will not break the format, sort)
17
 * - c. When you achieve the two operations described above, you can turn off auto sort out (TimePeriodHelper::setSortOut(false)) to improve performance.
18
 * 
19
 * @version 0.5.2
20
 * @author Mars Hung <[email protected]>
21
 * @see https://github.com/marshung24/TimePeriodHelper
22
 */
23
class TimePeriodHelper
24
{
25
    /**
26
     * Option set
27
     * @var array
28
     */
29
    protected static $_options = [
30
        'unit' => [
31
            // Time calculate unit - hour, minute, second(default)
32
            'time' => 'second',
33
            // Format unit - hour, minute, second(default)
34
            'format' => 'second',
35
        ],
36
        'unitMap' => [
37
            'hour' => 'hour',
38
            'hours' => 'hour',
39
            'h' => 'hour',
40
            'minute' => 'minute',
41
            'minutes' => 'minute',
42
            'i' => 'minute',
43
            'm' => 'minute',
44
            'second' => 'second',
45
            'seconds' => 'second',
46
            's' => 'second',
47
        ],
48
        'filter' => [
49
            'isDatetime' => true
50
        ],
51
        // Default sort out $timePeriods
52
        'sortOut' => true,
53
    ];
54
55
56
    /**
57
     * ************************************************
58
     * ************** Operation Function **************
59
     * ************************************************
60
     */
61
62
    /**
63
     * Sort time periods (Order by ASC)
64
     * 
65
     * 1. When sorting, sort the start time first, if the start time is the same, then sort the end time
66
     * 2. Sort Priority: Start Time => End Time
67
     * 
68
     * @param array $timePeriods
69
     * @return array
70
     */
71
    public static function sort(array $timePeriods)
72
    {
73
        // Closure in PHP 7.0.X loop maybe die
74
        usort($timePeriods, function ($a, $b) {
75
            if ($a[0] == $b[0]) {
76
                // Start time is equal, compare end time
77
                $r = $a[1] < $b[1] ? -1 : 1;
78
            } else {
79
                // Compare Start time
80
                $r = $a[0] < $b[0] ? -1 : 1;
81
            }
82
83
            return $r;
84
        });
85
86
        return $timePeriods;
87
    }
88
89
    /**
90
     * Union one or more time periods
91
     * 
92
     * 1. Sort and merge one or more time periods with contacts
93
     * 2. TimePeriodHelper::union($timePeriods1, $timePeriods2, $timePeriods3, ......);
94
     * 
95
     * @param array $timePeriods
96
     * @return array
97
     */
98
    public static function union()
99
    {
100
        $opt = [];
101
102
        // Combine and sort
103
        $merge = call_user_func_array('array_merge', func_get_args());
104
        $merge = self::sort($merge);
105
106
        if (empty($merge)) {
107
            return $opt;
108
        }
109
110
        $tmp = array_shift($merge);
111
        foreach ($merge as $k => $tp) {
112
            if ($tp[0] > $tmp[1]) {
113
                // Got it, and set next.
114
                $opt[] = $tmp;
115
                $tmp = $tp;
116
            } elseif ($tp[1] > $tmp[1]) {
117
                // Extend end time
118
                $tmp[1] = $tp[1];
119
            }
120
        }
121
        $opt[] = $tmp;
122
123
        return $opt;
124
    }
125
126
    /**
127
     * Computes the difference of time periods
128
     * 
129
     * 1. Compares $timePeriods1 against $timePeriods2 and returns the values in $timePeriods1 that are not present in $timePeriods2.
130
     * 2. e.g. TimePeriodHelper::diff($timePeriods1, $timePeriods2);
131
     * 3. Whether $timePeriods is sorted out will affect the correctness of the results. Please refer to Note 5. Ensure performance by keeping the $timePeriods format correct.
132
     * 
133
     * @param array $timePeriods1
134
     * @param array $timePeriods2
135
     * @param bool $sortOut Whether the input needs to be rearranged. Value: true, false, 'default'. If it is 'default', see getSortOut()
136
     * @return array
137
     */
138
    public static function diff(array $timePeriods1, array $timePeriods2, $sortOut = 'default')
139
    {
140
        /*** Arguments prepare ***/
141
        // Subject or pattern is empty, do nothing
142
        if (empty($timePeriods1) || empty($timePeriods2)) {
143
            return $timePeriods1;
144
        }
145
146
        // Data sorting out
147
        self::dataSortOut($sortOut, $timePeriods1, $timePeriods2);
148
149
        $opt = [];
150
        foreach ($timePeriods1 as $k1 => $ori) {
151
            foreach ($timePeriods2 as $ko => $sub) {
152
                if ($sub[1] <= $ori[0]) {
153
                    // No overlap && Passed: --sub0--sub1--ori0--ori1--
154
                    unset($timePeriods2[$ko]);
155
                    continue;
156
                } elseif ($ori[1] <= $sub[0]) {
157
                    // No overlap: --ori0--ori1--sub0--sub1--
158
                    continue;
159
                } elseif ($sub[0] <= $ori[0] && $ori[1] <= $sub[1]) {
160
                    // Subtract all: --sub0--ori0--ori1--sub1--
161
                    $ori = [];
162
                    break;
163
                } elseif ($ori[0] < $sub[0] && $sub[1] < $ori[1]) {
164
                    // Delete internal: --ori0--sub0--sub1--ori1--
165
                    $opt[] = [$ori[0], $sub[0]];
166
                    $ori = [$sub[1], $ori[1]];
167
                    //} elseif ($sub[0] <= $ori[0] && $sub[1] <= $ori[1]) { // Complete condition
168
                } elseif ($sub[0] <= $ori[0]) { // Equivalent condition
169
                    // Delete overlap: --sub0--ori0--sub1--ori1--
170
                    $ori = [$sub[1], $ori[1]];
171
                    //} elseif ($ori[0] <= $sub[0] && $ori[1] <= $sub[1]) { // Complete condition
172
                    //} elseif ($ori[1] <= $sub[1]) { // Equivalent condition
173
                } else { // Equivalent condition
174
                    // Delete overlap: --ori0--sub0--ori1--sub1--
175
                    $ori = [$ori[0], $sub[0]];
176
                }
177
            }
178
179
            // All No overlap
180
            if (!empty($ori)) {
181
                $opt[] = $ori;
182
            }
183
        }
184
185
        return $opt;
186
    }
187
188
    /**
189
     * Computes the intersection of time periods
190
     * 
191
     * 1. e.g. TimePeriodHelper::intersect($timePeriods1, $timePeriods2);
192
     * 2. Whether $timePeriods is sorted out will affect the correctness of the results. Please refer to Note 5. Ensure performance by keeping the $timePeriods format correct.
193
     * 
194
     * @param array $timePeriods1
195
     * @param array $timePeriods2
196
     * @param bool $sortOut Whether the input needs to be rearranged. Value: true, false, 'default'. If it is 'default', see getSortOut()
197
     * @return array
198
     */
199
    public static function intersect(array $timePeriods1, array $timePeriods2, $sortOut = 'default')
200
    {
201
        // Subject or pattern is empty, do nothing
202
        if (empty($timePeriods1) || empty($timePeriods2)) {
203
            return [];
204
        }
205
206
        // Data sorting out
207
        self::dataSortOut($sortOut, $timePeriods1, $timePeriods2);
208
209
        $opt = [];
210
        foreach ($timePeriods1 as $k1 => $ori) {
211
            foreach ($timePeriods2 as $ko => $sub) {
212
                if ($sub[1] <= $ori[0]) {
213
                    // No overlap && Passed: --sub0--sub1--ori0--ori1--
214
                    unset($timePeriods2[$ko]);
215
                    continue;
216
                } elseif ($ori[1] <= $sub[0]) {
217
                    // No overlap: --ori0--ori1--sub0--sub1--
218
                    continue;
219
                } elseif ($sub[0] <= $ori[0] && $ori[1] <= $sub[1]) {
220
                    // Subtract all: --sub0--ori0--ori1--sub1--
221
                    $opt[] = [$ori[0], $ori[1]];
222
                    break;
223
                } elseif ($ori[0] < $sub[0] && $sub[1] < $ori[1]) {
224
                    // Delete internal: --ori0--sub0--sub1--ori1--
225
                    $opt[] = [$sub[0], $sub[1]];
226
                    $ori = [$sub[1], $ori[1]];
227
                    //} elseif ($sub[0] <= $ori[0] && $sub[1] <= $ori[1]) { // Complete condition
228
                } elseif ($sub[0] <= $ori[0]) { // Equivalent condition
229
                    // Delete overlap: --sub0--ori0--sub1--ori1--
230
                    $opt[] = [$ori[0], $sub[1]];
231
                    $ori = [$sub[1], $ori[1]];
232
                    //} elseif ($ori[0] <= $sub[0] && $ori[1] <= $sub[1]) { // Complete condition
233
                    //} elseif ($ori[1] <= $sub[1]) { // Equivalent condition
234
                } else { // Equivalent condition
235
                    // Delete overlap: --ori0--sub0--ori1--sub1--
236
                    $opt[] = [$sub[0], $ori[1]];
237
                    break;
238
                }
239
            }
240
        }
241
242
        return $opt;
243
    }
244
245
    /**
246
     * Time period is overlap
247
     * 
248
     * 1. Determine if there is overlap between the two time periods
249
     * 2. Only when there is no intersection, no data is needed.
250
     * 3. Logic is similar to intersect.
251
     *  
252
     * @param array $timePeriods1
253
     * @param array $timePeriods2
254
     * @return bool
255
     */
256
    public static function isOverlap(array $timePeriods1, array $timePeriods2)
257
    {
258
        // Subject or pattern is empty, do nothing
259
        if (empty($timePeriods1) || empty($timePeriods2)) {
260
            return false;
261
        }
262
263
        foreach ($timePeriods1 as $k1 => $ori) {
264
            foreach ($timePeriods2 as $ko => $sub) {
265
                if ($sub[1] <= $ori[0]) {
266
                    // No overlap && Passed: --sub0--sub1--ori0--ori1--
267
                    unset($timePeriods2[$ko]);
268
                    continue;
269
                } elseif ($ori[1] <= $sub[0]) {
270
                    // No overlap: --ori0--ori1--sub0--sub1--
271
                    continue;
272
                } elseif ($sub[0] <= $ori[0] && $ori[1] <= $sub[1]) {
273
                    // Subtract all: --sub0--ori0--ori1--sub1--
274
                    return true;
275
                } elseif ($ori[0] < $sub[0] && $sub[1] < $ori[1]) {
276
                    // Delete internal: --ori0--sub0--sub1--ori1--
277
                    return true;
278
                    //} elseif ($sub[0] <= $ori[0] && $sub[1] <= $ori[1]) { // Complete condition
279
                } elseif ($sub[0] <= $ori[0]) { // Equivalent condition
280
                    // Delete overlap: --sub0--ori0--sub1--ori1--
281
                    return true;
282
                    //} elseif ($ori[0] <= $sub[0] && $ori[1] <= $sub[1]) { // Complete condition
283
                    //} elseif ($ori[1] <= $sub[1]) { // Equivalent condition
284
                } else { // Equivalent condition
285
                    // Delete overlap: --ori0--sub0--ori1--sub1--
286
                    return true;
287
                }
288
            }
289
        }
290
291
        return false;
292
    }
293
294
    /**
295
     * The time period is in contact with the specified time (time period)
296
     * 
297
     * @param array $timePeriods
298
     * @param string $sDateTime
299
     * @param string $eDateTime
300
     * @param string $sortOut
301
     * @return array
302
     */
303
    public static function contact(array $timePeriods, $sDateTime, $eDateTime = null, $sortOut = 'default')
304
    {
305
        // Subject is empty, do nothing
306
        if (empty($timePeriods)) {
307
            return [];
308
        }
309
310
        // Set $eDateTime
311
        if (is_null($eDateTime)) {
312
            $eDateTime = $sDateTime;
313
        }
314
315
        // Data sorting out
316
        self::dataSortOut($sortOut, $timePeriods);
317
        if ($sortOut) {
318
            $sDateTime = min($sDateTime, $eDateTime);
319
            $eDateTime = max($sDateTime, $eDateTime);
320
        }
321
322
        // Get Contact time periods
323
        $opt = [];
324
        foreach ($timePeriods as $k => $tp) {
325
            if ($eDateTime <= $tp[0]) {
326
                // No overlap && Passed: --$sDateTime--$eDateTime--$tp0--$tp1--
327
                if ($sDateTime == $tp[0]) {
328
                    // But 
329
                    $opt[] = $tp;
330
                }
331
            } elseif ($tp[1] <= $sDateTime) {
332
                // No overlap: --$tp0--$tp1--$sDateTime--$eDateTime--
333
            } else {
334
                // Overlap
335
                $opt[] = $tp;
336
            }
337
        }
338
339
        return $opt;
340
    }
341
342
    /**
343
     * Time period greater than the specified time
344
     * 
345
     * @param array $timePeriods
346
     * @param string $refDatetime
347
     * @param string $intactTime
348
     * @param string $sortOut
349
     * @return array
350
     */
351
    public static function greaterThan(array $timePeriods, $refDatetime, $intactTime = true, $sortOut = 'default')
352
    {
353
        // Subject is empty, do nothing
354
        if (empty($timePeriods)) {
355
            return [];
356
        }
357
358
        // Data sorting out
359
        self::dataSortOut($sortOut, $timePeriods);
360
361
        // Get Contact time periods
362
        $opt = [];
363
        foreach ($timePeriods as $k => $tp) {
364
            if ($intactTime) {
365
                // Time period is intact
366
                if ($tp[0] >= $refDatetime) {
367
                    $opt[] = $tp;
368
                }
369
            } else {
370
                // Time period not intact
371
                if ($tp[1] > $refDatetime) {
372
                    $opt[] = $tp;
373
                }
374
            }
375
        }
376
377
        return $opt;
378
    }
379
380
    /**
381
     * Time period less than the specified time
382
     * 
383
     * @param array $timePeriods
384
     * @param string $refDatetime
385
     * @param string $intactTime
386
     * @param string $sortOut
387
     * @return array
388
     */
389
    public static function lessThan(array $timePeriods, $refDatetime, $intactTime = true, $sortOut = 'default')
390
    {
391
        // Subject is empty, do nothing
392
        if (empty($timePeriods)) {
393
            return [];
394
        }
395
396
        // Data sorting out
397
        self::dataSortOut($sortOut, $timePeriods);
398
399
        // Get Contact time periods
400
        $opt = [];
401
        foreach ($timePeriods as $k => $tp) {
402
            if ($intactTime) {
403
                // Time period is intact
404
                if ($tp[1] <= $refDatetime) {
405
                    $opt[] = $tp;
406
                }
407
            } else {
408
                // Time period not intact
409
                if ($tp[0] < $refDatetime) {
410
                    $opt[] = $tp;
411
                }
412
            }
413
        }
414
415
        return $opt;
416
    }
417
418
    /**
419
     * Fill time periods
420
     * 
421
     * Leaving only the first start time and the last end time
422
     * 
423
     * @param array $timePeriods
424
     * @return array
425
     */
426
    public static function fill(array $timePeriods)
427
    {
428
        $opt = [];
429
430
        if (isset($timePeriods[0][0])) {
431
            $tmp = array_shift($timePeriods);
432
            $start = $tmp[0];
433
            $end = $tmp[1];
434
            foreach ($timePeriods as $k => $tp) {
435
                $start = min($start, $tp[0]);
436
                $end = max($end, $tp[1]);
437
            }
438
            $opt = [[$start, $end]];
439
        }
440
441
        return $opt;
442
    }
443
444
    /**
445
     * Get gap time periods of multiple sets of time periods
446
     * 
447
     * 1. Whether $timePeriods is sorted out will affect the correctness of the results. Please refer to Note 5. Ensure performance by keeping the $timePeriods format correct.
448
     * 
449
     * @param array $timePeriods
450
     * @param bool $sortOut Whether the input needs to be rearranged. Value: true, false, 'default'. If it is 'default', see getSortOut()
451
     * @return array
452
     */
453
    public static function gap(array $timePeriods, $sortOut = 'default')
454
    {
455
        // Subject is empty, do nothing
456
        if (empty($timePeriods)) {
457
            return [];
458
        }
459
460
        // Data sorting out
461
        self::dataSortOut($sortOut, $timePeriods);
462
463
        $opt = [];
464
        foreach ($timePeriods as $k => $tp) {
465
            if (isset($timePeriods[$k + 1])) {
466
                $opt[] = [$tp[1], $timePeriods[$k + 1][0]];
467
            }
468
        }
469
470
        return $opt;
471
    }
472
473
    /**
474
     * Calculation period total time
475
     *
476
     * 1. You can specify the smallest unit (from setUnit())
477
     * 2. Whether $timePeriods is sorted out will affect the correctness of the results. Please refer to Note 5. Ensure performance by keeping the $timePeriods format correct.
478
     * 3. approximation: chop off
479
     *
480
     * @param array $timePeriods            
481
     * @param int $precision
482
     *            Optional decimal places for the decimal point
483
     * @param bool $sortOut Whether the input needs to be rearranged. Value: true, false, 'default'. If it is 'default', see getSortOut()
484
     * @return number
485
     */
486
    public static function time(array $timePeriods, $precision = 0, $sortOut = 'default')
487
    {
488
        // Subject is empty, do nothing
489
        if (empty($timePeriods)) {
490
            return 0;
491
        }
492
493
        // Data sorting out
494
        self::dataSortOut($sortOut, $timePeriods);
495
496
        // Calculate time
497
        $time = 0;
498
        foreach ($timePeriods as $k => $tp) {
499
            $time += strtotime($tp[1]) - strtotime($tp[0]);
500
        }
501
502
        // Time unit convert
503
        switch (self::getUnit('time')) {
504
            case 'minute':
505
                $time = $time / 60;
506
                break;
507
            case 'hour':
508
                $time = $time / 3600;
509
                break;
510
        }
511
512
        // Precision
513
        if ($precision > 0) {
514
            $pow = pow(10, (int) $precision);
515
            $time = ((int) ($time * $pow)) / $pow;
516
        } else {
517
            $time = (int) ($time);
518
        }
519
520
        return $time;
521
    }
522
523
    /**
524
     * Cut the time period of the specified length of time
525
     *
526
     * 1. You can specify the smallest unit (from setUnit())
527
     * 2. Whether $timePeriods is sorted out will affect the correctness of the results. Please refer to Note 5. Ensure performance by keeping the $timePeriods format correct.
528
     * 
529
     * @param array $timePeriods            
530
     * @param number $time
531
     *            Specified length of time
532
     * @param string $extension
533
     *            If the specified time is long, whether to extend the time period.(default:false)
534
     * @param bool $sortOut Whether the input needs to be rearranged. Value: true, false, 'default'. If it is 'default', see getSortOut()
535
     * @return array
536
     */
537
    public static function cut(array $timePeriods, $time, $extension = false, $sortOut = 'default')
538
    {
539
        // Subject is empty, do nothing
540
        if (empty($timePeriods)) {
541
            return [];
542
        }
543
544
        // Data sorting out
545
        self::dataSortOut($sortOut, $timePeriods);
546
547
        // Convert time by unit
548
        $time = self::time2Second($time);
549
550
        $opt = [];
551
        $timeLen = 0;
552
        foreach ($timePeriods as $k => $tp) {
553
            // Calculation time
554
            $tlen = strtotime($tp[1]) - strtotime($tp[0]);
555
556
            // Judging the length of time
557
            if ($timeLen + $tlen <= $time) {
558
                // Within limits
559
                $opt[] = $tp;
560
                $timeLen = $timeLen + $tlen;
561
            } else {
562
                // Outside the limits
563
                $lastTime = $time - $timeLen;
564
                if ($lastTime > 0) {
565
                    $tps = $tp[0];
566
                    $tpe = self::extendTime($tps, $lastTime);
567
                    if ($tps != $tpe) {
568
                        $opt[] = [$tps, $tpe];
569
                    }
570
                }
571
                $timeLen = $time;
572
                break;
573
            }
574
        }
575
576
        // Extend the last time period
577
        if ($extension && ($timeLen < $time)) {
578
            $eTime = $time - $timeLen;
579
            $eIdx = sizeof($opt) - 1;
580
581
            $tps = $opt[$eIdx][0];
582
            $tpe = self::extendTime($opt[$eIdx][1], $eTime);
583
            if ($tps != $tpe) {
584
                $opt[$eIdx] = [$tps, $tpe];
585
            }
586
        }
587
588
        return $opt;
589
    }
590
591
    /**
592
     * Increase the time period of the specified length of time after the last time period
593
     *
594
     * 1. You can specify the smallest unit (from setUnit())
595
     * 2. Whether $timePeriods is sorted out will affect the correctness of the results. Please refer to Note 5. Ensure performance by keeping the $timePeriods format correct.
596
     * 
597
     * @param array $timePeriods            
598
     * @param number $time
599
     *            Specified length of time (default uint:second)
600
     * @param number $interval
601
     *            Interval with existing time period
602
     * @param bool $sortOut Whether the input needs to be rearranged. Value: true, false, 'default'. If it is 'default', see getSortOut()
603
     * @return array
604
     */
605
    public static function extend(array $timePeriods, $time, $interval = 0, $sortOut = 'default')
606
    {
607
        // Subject is empty, do nothing
608
        if (empty($timePeriods)) {
609
            return [];
610
        }
611
612
        // Data sorting out
613
        self::dataSortOut($sortOut, $timePeriods);
614
615
        // Convert time by unit
616
        $time = self::time2Second($time);
617
        $interval = self::time2Second($interval);
618
619
        // last time period index
620
        $eIdx = sizeof($timePeriods) - 1;
621
622
        if (!$interval) {
623
            // No gap, Directly extend the end time
624
            $timePeriods[$eIdx][1] = self::extendTime($timePeriods[$eIdx][1], $time);
625
        } else {
626
            // Has gap
627
            $tps = self::extendTime($timePeriods[$eIdx][1], $interval);
628
            $tpe = self::extendTime($tps, $time);
629
            if ($tps != $tpe) {
630
                $timePeriods[] = [$tps, $tpe];
631
            }
632
        }
633
634
        return $timePeriods;
635
    }
636
637
    /**
638
     * Shorten the specified length of time from behind
639
     *
640
     * 1. You can specify the smallest unit (from setUnit())
641
     * 2. Whether $timePeriods is sorted out will affect the correctness of the results. Please refer to Note 5. Ensure performance by keeping the $timePeriods format correct.
642
     * 
643
     * @param array $timePeriods            
644
     * @param number $time
645
     *            Specified length of time (default uint:second)
646
     * @param bool $crossperiod
647
     *            Whether to shorten across time
648
     * @param bool $sortOut Whether the input needs to be rearranged. Value: true, false, 'default'. If it is 'default', see getSortOut()
649
     * @return array
650
     */
651
    public static function shorten(array $timePeriods, $time, $crossperiod = true, $sortOut = 'default')
652
    {
653
        // Subject is empty, do nothing
654
        if (empty($timePeriods)) {
655
            return [];
656
        }
657
658
        // Data sorting out
659
        self::dataSortOut($sortOut, $timePeriods);
660
661
        // Convert time by unit
662
        $time = self::time2Second($time);
663
664
        // last time period index
665
        $eIdx = sizeof($timePeriods) - 1;
666
667
        for ($i = $eIdx; $i >= 0; $i--) {
668
            $tps = $timePeriods[$i][0];
669
            $tpe = $timePeriods[$i][1];
670
            $tTime = strtotime($tpe) - strtotime($tps);
671
672
            if ($tTime <= $time) {
673
                // Not enough, unset this timeperiod
674
                unset($timePeriods[$i]);
675
                $time -= $tTime;
676
            } else {
677
                // Enough, shorten end time.
678
                $timePeriods[$i][1] = self::extendTime($timePeriods[$i][0], $tTime - $time);
679
                break;
680
            }
681
682
            // End or No cross-period
683
            if ($time <= 0 || !$crossperiod) {
684
                break;
685
            }
686
        }
687
688
        return $timePeriods;
689
    }
690
691
    /**
692
     * Transform format
693
     * 
694
     * @param array $timePeriods
695
     * @param string $unit Time unit, if default,use class options setting
696
     * @return array
697
     */
698
    public static function format(array $timePeriods, $unit = 'default')
699
    {
700
        foreach ($timePeriods as $k => &$tp) {
701
            $tp[0] = self::timeFormatConv($tp[0], $unit);
702
            $tp[1] = self::timeFormatConv($tp[1], $unit);
703
        }
704
705
        return $timePeriods;
706
    }
707
708
    /**
709
     * Validate time period
710
     * 
711
     * Verify format, size, start/end time
712
     * 
713
     * @param array $timePeriods
714
     * @throws \Exception
715
     * @return bool
716
     */
717
    public static function validate($timePeriods)
718
    {
719
        self::filter($timePeriods, true);
720
        return true;
721
    }
722
723
    /**
724
     * Remove invalid time period
725
     * 
726
     * 1. Verify format, size, start/end time, and remove invalid.
727
     * 2. time carry problem processing, e.g. 2019-01-01 24:00:00 => 2019-01-02 00:00:00
728
     * 
729
     * @param mixed|array $timePeriods
730
     * @param bool $exception Whether an exception is returned when an error occurs.(default false)
731
     * @throws \Exception
732
     * @return array
733
     */
734
    public static function filter($timePeriods, $exception = false)
735
    {
736
        // If not array, return.
737
        if (!is_array($timePeriods)) {
738
            if ($exception)
739
                throw new \Exception('Time periods format error !', 400);
740
            return [];
741
        }
742
743
        foreach ($timePeriods as $k => $tp) {
744
            // filter format, number
745
            if (!is_array($tp) || sizeof($tp) != 2) {
746
                if ($exception)
747
                    throw new \Exception('Time periods format error !', 400);
748
                unset($timePeriods[$k]);
749
                continue;
750
            }
751
            // filter time period
752
            if ($tp[0] >= $tp[1]) {
753
                if ($exception)
754
                    throw new \Exception('Time periods format error !', 400);
755
                unset($timePeriods[$k]);
756
                continue;
757
            }
758
            // filter time format
759
            if (self::getFilterDatetime() && (!self::isDatetime($tp[0]) || !self::isDatetime($tp[1]))) {
760
                if ($exception)
761
                    throw new \Exception('Time periods format error !', 400);
762
                unset($timePeriods[$k]);
763
                continue;
764
            }
765
766
            // Time carry
767
            $timeLen = strlen($tp[0]);
768
            if ($timeLen >= 13) {
769
                if (substr($tp[0], 11, 2) == '24') {
770
                    $timePeriods[$k][0] = self::extendTime($timePeriods[$k][0], 0);
771
                }
772
                if (substr($tp[1], 11, 2) == '24') {
773
                    $timePeriods[$k][1] = self::extendTime($timePeriods[$k][1], 0);
774
                }
775
            }
776
        }
777
778
        return $timePeriods;
779
    }
780
781
782
    /**
783
     * **********************************************
784
     * ************** Options Function **************
785
     * **********************************************
786
     */
787
788
789
    /**
790
     * Specify the minimum unit of calculation
791
     * 
792
     * 1. Scope: Global
793
     * 2. hour,minute,second
794
     * 
795
     * @param string $unit time unit. e.g. hour, minute, second.
796
     * @param string $target Specify function,or all functions
797
     * @throws \Exception
798
     * @return $this
799
     */
800
    public static function setUnit(string $unit, string $target = 'all')
801
    {
802
        /*** Arguments prepare ***/
803
        if (!isset(self::$_options['unitMap'][$unit])) {
804
            throw new \Exception('Error Unit: ' . $unit, 400);
805
        }
806
        // conv unit
807
        $unit = self::$_options['unitMap'][$unit];
808
809
        if ($target != 'all' && !isset(self::$_options['unit'][$target])) {
810
            throw new \Exception('Error Target: ' . $target, 400);
811
        }
812
813
        /* Setting */
814
        if ($target != 'all') {
815
            self::$_options['unit'][$target] = $unit;
816
        } else {
817
            foreach (self::$_options['unit'] as $tar => &$value) {
818
                $value = $unit;
819
            }
820
        }
821
822
        return new static();
823
    }
824
825
    /**
826
     * Get the unit used by the specified function
827
     * 
828
     * @param string $target Specify function's unit
829
     * @throws \Exception
830
     * @return string
831
     */
832
    public static function getUnit(string $target)
833
    {
834
        if (isset(self::$_options['unit'][$target])) {
835
            return self::$_options['unit'][$target];
836
        } else {
837
            throw new \Exception('Error Target: ' . $target, 400);
838
        }
839
    }
840
841
    /**
842
     * If neet filter datetime : Set option
843
     * 
844
     * 1. Scope: Global
845
     * 2. If you do not want to filter the datetime format, set it to false.  
846
     * 3. Maybe the time format is not Y-m-d H:i:s (such as Y-m-d H:i), you need to close it.
847
     * 4. Effect function: filter(), validate()
848
     * 
849
     * @param bool $bool
850
     * @return $this
851
     */
852
    public static function setFilterDatetime($bool)
853
    {
854
        self::$_options['filter']['isDatetime'] = !!$bool;
855
856
        return new static();
857
    }
858
859
    /**
860
     * If neet filter datetime : Get option
861
     * 
862
     * @return bool
863
     */
864
    public static function getFilterDatetime()
865
    {
866
        return self::$_options['filter']['isDatetime'];
867
    }
868
869
    /**
870
     * Auto sort out $timePeriods : Set option
871
     *
872
     * 1. Scope: Global
873
     * 2. Before the function is processed, union() will be used to organize $timePeriods format.
874
     * 
875
     * @param bool $bool default true
876
     * @return $this
877
     */
878
    public static function setSortOut($bool = true)
879
    {
880
        self::$_options['sortOut'] = !!$bool;
881
882
        return new static();
883
    }
884
885
    /**
886
     * Auto sort out $timePeriods : Get option
887
     *
888
     * @return bool
889
     */
890
    public static function getSortOut()
891
    {
892
        return self::$_options['sortOut'];
893
    }
894
895
    /**
896
     * ********************************************
897
     * ************** Tools Function **************
898
     * ********************************************
899
     */
900
901
    /**
902
     * Check datetime fast
903
     * 
904
     * Only check format,no check for reasonableness
905
     * 
906
     * @param string $datetime
907
     * @return boolean
908
     */
909
    public static function isDatetime(string $datetime)
910
    {
911
        return (bool) preg_match('|^[0-9]{4}\-[0-9]{2}\-[0-9]{2}\ [0-9]{2}\:[0-9]{2}\:[0-9]{2}$|', $datetime);
912
    }
913
914
    /**
915
     * Time format convert
916
     * 
917
     * format:Y-m-d H:i:s
918
     * When the length is insufficient, it will add the missing
919
     * 
920
     * @param string $datetime
921
     * @param string $unit Time unit, if default,use self::$_options setting
922
     * @return string
923
     */
924
    public static function timeFormatConv(string $datetime, $unit = 'default')
925
    {
926
        // fill format
927
        $strlen = strlen($datetime);
928
        $datetime .= substr(' 00:00:00', $strlen - 10);
929
930
        $unit = !isset(self::$_options['unitMap'][$unit]) ? self::$_options['unit']['format'] : self::$_options['unitMap'][$unit];
931
        // replace
932
        if ($unit == 'minute') {
933
            $datetime = substr_replace($datetime, "00", 17, 2);
934
        } elseif ($unit == 'hour') {
935
            $datetime = substr_replace($datetime, "00:00", 14, 5);
936
        }
937
938
        return $datetime;
939
    }
940
941
    /**
942
     * Extend time
943
     * 
944
     * @param string $datetime
945
     * @param int $timeLen 
946
     * @return string
947
     */
948
    protected static function extendTime(String $datetime, $timeLen)
949
    {
950
        $tout = date('Y-m-d H:i:s', strtotime($datetime) + $timeLen);
951
        return substr($tout, 0, strlen($datetime));
952
    }
953
954
    /**
955
     * Time Conversion frm unit to second
956
     * 
957
     * @param number $time
958
     * @param string $unit Time unit, if default,use self::$_options setting
959
     * @return int
960
     */
961
    public static function time2Second($time, $unit = 'default')
962
    {
963
        // Git time unit
964
        $unit = !isset(self::$_options['unitMap'][$unit]) ? self::getUnit('time') : self::$_options['unitMap'][$unit];
965
966
        // Convert
967
        switch ($unit) {
968
            case 'minute':
969
                $time = $time * 60;
970
                break;
971
            case 'hour':
972
                $time = $time * 3600;
973
                break;
974
        }
975
976
        return (int) $time;
977
    }
978
979
    /**
980
     * Data sorting out
981
     *
982
     * @param string|bool $sortOut
983
     * @param array $timePeriods1
984
     * @param array|null $timePeriods2
985
     * @return void
986
     */
987
    protected static function dataSortOut(&$sortOut, &$timePeriods1, &$timePeriods2 = null)
988
    {
989
        // Data sorting out
990
        $sortOut = $sortOut === 'default' ? self::getSortOut() : !!$sortOut;
991
        if ($sortOut) {
992
            $timePeriods1 = self::union($timePeriods1);
993
            if (!is_null($timePeriods2)) {
994
                $timePeriods2 = self::union($timePeriods2);
995
            }
996
        }
997
    }
998
}
999