Passed
Push — master ( bf46b0...156469 )
by Mars
01:52
created

TimePeriodHelper::shorten()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 38
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 18
c 1
b 0
f 0
nc 4
nop 4
dl 0
loc 38
rs 9.0444
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
                if ($precision > 0) {
506
                    $pow = pow(10, (int) $precision);
507
                    $time = ((int) ($time / 60 * $pow)) / $pow;
508
                } else {
509
                    $time = (int) ($time / 60);
510
                }
511
                break;
512
            case 'hour':
513
                if ($precision > 0) {
514
                    $pow = pow(10, (int) $precision);
515
                    $time = ((int) ($time / 3600 * $pow)) / $pow;
516
                } else {
517
                    $time = (int) ($time / 3600);
518
                }
519
                break;
520
        }
521
522
        return $time;
523
    }
524
525
    /**
526
     * Cut the time period of the specified length of time
527
     *
528
     * 1. You can specify the smallest unit (from setUnit())
529
     * 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.
530
     * 
531
     * @param array $timePeriods            
532
     * @param number $time
533
     *            Specified length of time
534
     * @param string $extension
535
     *            If the specified time is long, whether to extend the time period.(default:false)
536
     * @param bool $sortOut Whether the input needs to be rearranged. Value: true, false, 'default'. If it is 'default', see getSortOut()
537
     * @return array
538
     */
539
    public static function cut(array $timePeriods, $time, $extension = false, $sortOut = 'default')
540
    {
541
        // Subject is empty, do nothing
542
        if (empty($timePeriods)) {
543
            return [];
544
        }
545
546
        // Data sorting out
547
        self::dataSortOut($sortOut, $timePeriods);
548
549
        // Convert time by unit
550
        $time = self::time2Second($time);
551
552
        $opt = [];
553
        $timeLen = 0;
554
        foreach ($timePeriods as $k => $tp) {
555
            // Calculation time
556
            $tlen = strtotime($tp[1]) - strtotime($tp[0]);
557
558
            // Judging the length of time
559
            if ($timeLen + $tlen <= $time) {
560
                // Within limits
561
                $opt[] = $tp;
562
                $timeLen = $timeLen + $tlen;
563
            } else {
564
                // Outside the limits
565
                $lastTime = $time - $timeLen;
566
                if ($lastTime > 0) {
567
                    $tps = $tp[0];
568
                    $tpe = self::extendTime($tps, $lastTime);
569
                    if ($tps != $tpe) {
570
                        $opt[] = [$tps, $tpe];
571
                    }
572
                }
573
                $timeLen = $time;
574
                break;
575
            }
576
        }
577
578
        // Extend the last time period
579
        if ($extension && ($timeLen < $time)) {
580
            $eTime = $time - $timeLen;
581
            $eIdx = sizeof($opt) - 1;
582
583
            $tps = $opt[$eIdx][0];
584
            $tpe = self::extendTime($opt[$eIdx][1], $eTime);
585
            if ($tps != $tpe) {
586
                $opt[$eIdx] = [$tps, $tpe];
587
            }
588
        }
589
590
        return $opt;
591
    }
592
593
    /**
594
     * Increase the time period of the specified length of time after the last time period
595
     *
596
     * 1. You can specify the smallest unit (from setUnit())
597
     * 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.
598
     * 
599
     * @param array $timePeriods            
600
     * @param number $time
601
     *            Specified length of time (default uint:second)
602
     * @param number $interval
603
     *            Interval with existing time period
604
     * @param bool $sortOut Whether the input needs to be rearranged. Value: true, false, 'default'. If it is 'default', see getSortOut()
605
     * @return array
606
     */
607
    public static function extend(array $timePeriods, $time, $interval = 0, $sortOut = 'default')
608
    {
609
        // Subject is empty, do nothing
610
        if (empty($timePeriods)) {
611
            return [];
612
        }
613
614
        // Data sorting out
615
        self::dataSortOut($sortOut, $timePeriods);
616
617
        // Convert time by unit
618
        $time = self::time2Second($time);
619
        $interval = self::time2Second($interval);
620
621
        // last time period index
622
        $eIdx = sizeof($timePeriods) - 1;
623
624
        if (!$interval) {
625
            // No gap, Directly extend the end time
626
            $timePeriods[$eIdx][1] = self::extendTime($timePeriods[$eIdx][1], $time);
627
        } else {
628
            // Has gap
629
            $tps = self::extendTime($timePeriods[$eIdx][1], $interval);
630
            $tpe = self::extendTime($tps, $time);
631
            if ($tps != $tpe) {
632
                $timePeriods[] = [$tps, $tpe];
633
            }
634
        }
635
636
        return $timePeriods;
637
    }
638
639
    /**
640
     * Shorten the specified length of time from behind
641
     *
642
     * 1. You can specify the smallest unit (from setUnit())
643
     * 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.
644
     * 
645
     * @param array $timePeriods            
646
     * @param number $time
647
     *            Specified length of time (default uint:second)
648
     * @param bool $crossperiod
649
     *            Whether to shorten across time
650
     * @param bool $sortOut Whether the input needs to be rearranged. Value: true, false, 'default'. If it is 'default', see getSortOut()
651
     * @return array
652
     */
653
    public static function shorten(array $timePeriods, $time, $crossperiod = true, $sortOut = 'default')
654
    {
655
        // Subject is empty, do nothing
656
        if (empty($timePeriods)) {
657
            return [];
658
        }
659
660
        // Data sorting out
661
        self::dataSortOut($sortOut, $timePeriods);
662
663
        // Convert time by unit
664
        $time = self::time2Second($time);
665
666
        // last time period index
667
        $eIdx = sizeof($timePeriods) - 1;
668
669
        for ($i = $eIdx; $i >= 0; $i--) {
670
            $tps = $timePeriods[$i][0];
671
            $tpe = $timePeriods[$i][1];
672
            $tTime = strtotime($tpe) - strtotime($tps);
673
674
            if ($tTime <= $time) {
675
                // Not enough, unset this timeperiod
676
                unset($timePeriods[$i]);
677
                $time -= $tTime;
678
            } else {
679
                // Enough, shorten end time.
680
                $timePeriods[$i][1] = self::extendTime($timePeriods[$i][0], $tTime - $time);
681
                break;
682
            }
683
684
            // End or No cross-period
685
            if ($time <= 0 || !$crossperiod) {
686
                break;
687
            }
688
        }
689
690
        return $timePeriods;
691
    }
692
693
    /**
694
     * Transform format
695
     * 
696
     * @param array $timePeriods
697
     * @param string $unit Time unit, if default,use class options setting
698
     * @return array
699
     */
700
    public static function format(array $timePeriods, $unit = 'default')
701
    {
702
        foreach ($timePeriods as $k => &$tp) {
703
            $tp[0] = self::timeFormatConv($tp[0], $unit);
704
            $tp[1] = self::timeFormatConv($tp[1], $unit);
705
        }
706
707
        return $timePeriods;
708
    }
709
710
    /**
711
     * Validate time period
712
     * 
713
     * Verify format, size, start/end time
714
     * 
715
     * @param array $timePeriods
716
     * @throws \Exception
717
     * @return bool
718
     */
719
    public static function validate($timePeriods)
720
    {
721
        self::filter($timePeriods, true);
722
        return true;
723
    }
724
725
    /**
726
     * Remove invalid time period
727
     * 
728
     * 1. Verify format, size, start/end time, and remove invalid.
729
     * 2. time carry problem processing, e.g. 2019-01-01 24:00:00 => 2019-01-02 00:00:00
730
     * 
731
     * @param mixed|array $timePeriods
732
     * @param bool $exception Whether an exception is returned when an error occurs.(default false)
733
     * @throws \Exception
734
     * @return array
735
     */
736
    public static function filter($timePeriods, $exception = false)
737
    {
738
        // If not array, return.
739
        if (!is_array($timePeriods)) {
740
            if ($exception)
741
                throw new \Exception('Time periods format error !', 400);
742
            return [];
743
        }
744
745
        foreach ($timePeriods as $k => $tp) {
746
            // filter format, number
747
            if (!is_array($tp) || sizeof($tp) != 2) {
748
                if ($exception)
749
                    throw new \Exception('Time periods format error !', 400);
750
                unset($timePeriods[$k]);
751
                continue;
752
            }
753
            // filter time period
754
            if ($tp[0] >= $tp[1]) {
755
                if ($exception)
756
                    throw new \Exception('Time periods format error !', 400);
757
                unset($timePeriods[$k]);
758
                continue;
759
            }
760
            // filter time format
761
            if (self::getFilterDatetime() && (!self::isDatetime($tp[0]) || !self::isDatetime($tp[1]))) {
762
                if ($exception)
763
                    throw new \Exception('Time periods format error !', 400);
764
                unset($timePeriods[$k]);
765
                continue;
766
            }
767
768
            // Time carry
769
            $timeLen = strlen($tp[0]);
770
            if ($timeLen >= 13) {
771
                if (substr($tp[0], 11, 2) == '24') {
772
                    $timePeriods[$k][0] = self::extendTime($timePeriods[$k][0], 0);
773
                }
774
                if (substr($tp[1], 11, 2) == '24') {
775
                    $timePeriods[$k][1] = self::extendTime($timePeriods[$k][1], 0);
776
                }
777
            }
778
        }
779
780
        return $timePeriods;
781
    }
782
783
784
    /**
785
     * **********************************************
786
     * ************** Options Function **************
787
     * **********************************************
788
     */
789
790
791
    /**
792
     * Specify the minimum unit of calculation
793
     * 
794
     * 1. Scope: Global
795
     * 2. hour,minute,second
796
     * 
797
     * @param string $unit time unit. e.g. hour, minute, second.
798
     * @param string $target Specify function,or all functions
799
     * @throws \Exception
800
     * @return $this
801
     */
802
    public static function setUnit(string $unit, string $target = 'all')
803
    {
804
        /*** Arguments prepare ***/
805
        if (!isset(self::$_options['unitMap'][$unit])) {
806
            throw new \Exception('Error Unit: ' . $unit, 400);
807
        }
808
        // conv unit
809
        $unit = self::$_options['unitMap'][$unit];
810
811
        if ($target != 'all' && !isset(self::$_options['unit'][$target])) {
812
            throw new \Exception('Error Target: ' . $target, 400);
813
        }
814
815
        /* Setting */
816
        if ($target != 'all') {
817
            self::$_options['unit'][$target] = $unit;
818
        } else {
819
            foreach (self::$_options['unit'] as $tar => &$value) {
820
                $value = $unit;
821
            }
822
        }
823
824
        return new static();
825
    }
826
827
    /**
828
     * Get the unit used by the specified function
829
     * 
830
     * @param string $target Specify function's unit
831
     * @throws \Exception
832
     * @return string
833
     */
834
    public static function getUnit(string $target)
835
    {
836
        if (isset(self::$_options['unit'][$target])) {
837
            return self::$_options['unit'][$target];
838
        } else {
839
            throw new \Exception('Error Target: ' . $target, 400);
840
        }
841
    }
842
843
    /**
844
     * If neet filter datetime : Set option
845
     * 
846
     * 1. Scope: Global
847
     * 2. If you do not want to filter the datetime format, set it to false.  
848
     * 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.
849
     * 4. Effect function: filter(), validate()
850
     * 
851
     * @param bool $bool
852
     * @return $this
853
     */
854
    public static function setFilterDatetime($bool)
855
    {
856
        self::$_options['filter']['isDatetime'] = !!$bool;
857
858
        return new static();
859
    }
860
861
    /**
862
     * If neet filter datetime : Get option
863
     * 
864
     * @return bool
865
     */
866
    public static function getFilterDatetime()
867
    {
868
        return self::$_options['filter']['isDatetime'];
869
    }
870
871
    /**
872
     * Auto sort out $timePeriods : Set option
873
     *
874
     * 1. Scope: Global
875
     * 2. Before the function is processed, union() will be used to organize $timePeriods format.
876
     * 
877
     * @param bool $bool default true
878
     * @return $this
879
     */
880
    public static function setSortOut($bool = true)
881
    {
882
        self::$_options['sortOut'] = !!$bool;
883
884
        return new static();
885
    }
886
887
    /**
888
     * Auto sort out $timePeriods : Get option
889
     *
890
     * @return bool
891
     */
892
    public static function getSortOut()
893
    {
894
        return self::$_options['sortOut'];
895
    }
896
897
    /**
898
     * ********************************************
899
     * ************** Tools Function **************
900
     * ********************************************
901
     */
902
903
    /**
904
     * Check datetime fast
905
     * 
906
     * Only check format,no check for reasonableness
907
     * 
908
     * @param string $datetime
909
     * @return boolean
910
     */
911
    public static function isDatetime(string $datetime)
912
    {
913
        return (bool) preg_match('|^[0-9]{4}\-[0-9]{2}\-[0-9]{2}\ [0-9]{2}\:[0-9]{2}\:[0-9]{2}$|', $datetime);
914
    }
915
916
    /**
917
     * Time format convert
918
     * 
919
     * format:Y-m-d H:i:s
920
     * When the length is insufficient, it will add the missing
921
     * 
922
     * @param string $datetime
923
     * @param string $unit Time unit, if default,use self::$_options setting
924
     * @return string
925
     */
926
    public static function timeFormatConv(string $datetime, $unit = 'default')
927
    {
928
        // fill format
929
        $strlen = strlen($datetime);
930
        $datetime .= substr(' 00:00:00', $strlen - 10);
931
932
        $unit = !isset(self::$_options['unitMap'][$unit]) ? self::$_options['unit']['format'] : self::$_options['unitMap'][$unit];
933
        // replace
934
        if ($unit == 'minute') {
935
            $datetime = substr_replace($datetime, "00", 17, 2);
936
        } elseif ($unit == 'hour') {
937
            $datetime = substr_replace($datetime, "00:00", 14, 5);
938
        }
939
940
        return $datetime;
941
    }
942
943
    /**
944
     * Extend time
945
     * 
946
     * @param string $datetime
947
     * @param int $timeLen 
948
     * @return string
949
     */
950
    protected static function extendTime(String $datetime, $timeLen)
951
    {
952
        $tout = date('Y-m-d H:i:s', strtotime($datetime) + $timeLen);
953
        return substr($tout, 0, strlen($datetime));
954
    }
955
956
    /**
957
     * Time Conversion frm unit to second
958
     * 
959
     * @param number $time
960
     * @param string $unit Time unit, if default,use self::$_options setting
961
     * @return int
962
     */
963
    public static function time2Second($time, $unit = 'default')
964
    {
965
        // Git time unit
966
        $unit = !isset(self::$_options['unitMap'][$unit]) ? self::getUnit('time') : self::$_options['unitMap'][$unit];
967
968
        // Convert
969
        switch ($unit) {
970
            case 'minute':
971
                $time = $time * 60;
972
                break;
973
            case 'hour':
974
                $time = $time * 3600;
975
                break;
976
        }
977
978
        return (int) $time;
979
    }
980
981
    /**
982
     * Data sorting out
983
     *
984
     * @param string|bool $sortOut
985
     * @param array $timePeriods1
986
     * @param array|null $timePeriods2
987
     * @return void
988
     */
989
    protected static function dataSortOut(&$sortOut, &$timePeriods1, &$timePeriods2 = null)
990
    {
991
        // Data sorting out
992
        $sortOut = $sortOut === 'default' ? self::getSortOut() : !!$sortOut;
993
        if ($sortOut) {
994
            $timePeriods1 = self::union($timePeriods1);
995
            if (!is_null($timePeriods2)) {
996
                $timePeriods2 = self::union($timePeriods2);
997
            }
998
        }
999
    }
1000
}
1001