LocalDate::modifyByHours()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
/**
3
 * File was created 11.03.2015 14:51
4
 *
5
 * @author Karsten J. Gerber <[email protected]>
6
 */
7
8
namespace PeekAndPoke\Types;
9
10
/**
11
 * LocalDate is an enhanced date that forces construction with a timezone like Europe/Berlin
12
 *
13
 * The LocalDate is immutable. All modifying methods return a new instance.
14
 *
15
 * It also integrates with Slumber to serialize to json like
16
 *
17
 * { date : "2017-10-10T11:11:11+02:00", tz : "Europe/Berlin" }
18
 *
19
 * So the receiver (e.g. the browser) can reconstruct the date with the correct timezone
20
 *
21
 * @api
22
 *
23
 * @author Karsten J. Gerber <[email protected]>
24
 */
25
class LocalDate
26
{
27
    /** @var \DateTime */
28
    private $date;
29
    /** @var \DateTimeZone */
30
    private $timezone;
31
32
    /**
33
     * @param int                  $timestamp The unix timestamp
34
     * @param \DateTimeZone|string $timezone  The timezone or a string the DateTimeZone c'tor can understand
35
     *
36
     * @return LocalDate
37
     */
38 109
    public static function fromTimestamp($timestamp, $timezone)
39
    {
40 109
        return new LocalDate($timestamp, $timezone);
41
    }
42
43
    /**
44
     * @return LocalDate
45
     */
46 2
    public static function now()
47
    {
48 2
        return self::raw(new \DateTime());
49
    }
50
51
    /**
52
     * @param \DateTime $dateTime
53
     *
54
     * @return LocalDate
55
     */
56 18
    public static function raw(\DateTime $dateTime)
57
    {
58 18
        $tz = $dateTime->getTimezone();
59
60
        // normalize input coming from Javascript or Java etc...
61 18
        if ($tz->getName() === 'Z') {
62 8
            $tz = new \DateTimeZone('Etc/UTC');
63
        }
64
65 18
        return new LocalDate($dateTime, $tz);
66
    }
67
68
    /**
69
     * @param \DateTime|string|float|int $input    The date or a string the DateTime c'tor can understand or a timestamp
70
     * @param \DateTimeZone|string       $timezone The timezone or a string the DateTimeZone c'tor can understand
71
     */
72 226
    public function __construct($input, $timezone)
73
    {
74 226
        if (! $timezone instanceof \DateTimeZone) {
75 198
            $timezone = new \DateTimeZone((string) $timezone);
76
        }
77
78
        /////
79
        // We have to trick PHP a bit here, but why?
80
        //
81
        // Apparently things behave different, when setting a timezone like
82
        // a) 'Europe/Berlin'
83
        // b) '+02:00'
84
        //
85
        // When the date already has a timezone set then
86
        // a) will only set the timezone
87
        // b) will set the timezone and will also change the timestamp by 2 hours
88
        //
89
        // Therefore we create a fresh date, that does not have a timezone yet, set the timestamp, and then apply the
90
        // timezone
91
        //
92
        // See the unit tests for more as well
93
        ////
94 226
        if ($input instanceof \DateTime) {
95
96 62
            $date = (clone $input)->setTimezone($timezone);
97
98 188
        } elseif (is_numeric($input)) {
99
            // Handle numeric input. Special case is floats with micro second precision
100 139
            if (\is_float($input)) {
101 20
                $date = \DateTime::createFromFormat('U.u', sprintf('%.6f', $input));
102 20
                $date->setTimezone($timezone);
103
            } else {
104
                // otherwise we handle things as integer timestamps
105 139
                $date = (new \DateTime())->setTimestamp((int) $input)->setTimezone($timezone);
106
            }
107
108
        } else {
109
            // When we have string input, we immediately use the timezone
110
            // We reconstruct the date time again in order to set the timezone on the inner one
111 136
            $date = (new \DateTime($input, $timezone))->setTimezone($timezone);
112
        }
113
114 226
        $this->date     = $date;
115 226
        $this->timezone = $timezone;
116 226
    }
117
118
    /**
119
     * @return string
120
     */
121 1
    public function __toString()
122
    {
123 1
        return $this->format();
124
    }
125
126
    /**
127
     * @return \DateTime
128
     */
129 85
    public function getDate()
130
    {
131 85
        return clone $this->date;
132
    }
133
134
    /**
135
     * @return int
136
     */
137 125
    public function getTimestamp()
138
    {
139 125
        return $this->date->getTimestamp();
140
    }
141
142
    /**
143
     * @return \DateTimeZone
144
     */
145 174
    public function getTimezone()
146
    {
147 174
        return clone $this->timezone;
148
    }
149
150
    /**
151
     * @return int
152
     */
153 4
    public function getOffset()
154
    {
155 4
        return $this->date->getOffset();
156
    }
157
158
    /**
159
     * @return float
160
     */
161 1
    public function getOffsetInMinutes()
162
    {
163 1
        $offset = $this->getTimezone()->getOffset($this->getDate());
164
165 1
        return $offset / 60;
166
    }
167
168
    /**
169
     * @return float
170
     */
171 1
    public function getOffsetInHours()
172
    {
173 1
        $offset = $this->getTimezone()->getOffset($this->getDate());
174
175 1
        return $offset / 3600;
176
    }
177
178
    /**
179
     * @param string $format
180
     *
181
     * @return string
182
     */
183 210
    public function format($format = 'c')
184
    {
185 210
        return $this->date->format($format);
186
    }
187
188
    /**
189
     * @return LocalDate
190
     */
191 1
    public function getClone()
192
    {
193 1
        return new LocalDate($this->date, $this->timezone);
194
    }
195
196
    /**
197
     * Get the time saving shift on this date in seconds
198
     *
199
     * @return int
200
     */
201 1
    public function getDaylightSavingShift()
202
    {
203 1
        $previousNoon = $this->getStartOfPreviousDay()->modifyByHours(12);
204
205 1
        return $this->getOffset() - $previousNoon->getOffset();
206
    }
207
208
    //// Daylight saving time shift aware methods //////////////////////////////////////////////////////////////////////
209
210
    /**
211
     * Add hours to start of day while also respecting Daylight-saving-time shift
212
     *
213
     * E.g. on 2016-03-27T10:00:00 in Berlin is only 9 hours after the start of the day.
214
     *
215
     * @param $hours
216
     *
217
     * @return LocalDate
218
     */
219 2
    public function getDstStartOfDayPlusHours($hours)
220
    {
221 2
        $startOfDay       = $this->getStartOfDay();
222 2
        $startOfDayOffset = $startOfDay->getOffset();
223
224 2
        $result       = $startOfDay->modifyByHours($hours);
225 2
        $resultOffset = $result->getOffset();
226
227 2
        return $result->modifyBySeconds($startOfDayOffset)->modifyBySeconds(0 - $resultOffset);
228
    }
229
230
    //// Modification methods //////////////////////////////////////////////////////////////////////////////////////////
231
232
    /**
233
     * @return LocalDate
234
     */
235 10
    public function getStartOfDay()
236
    {
237 10
        return $this->modifyTime(0, 0, 0);
238
    }
239
240
    /**
241
     * Get the nearest start of a day, either the current or the next day depending on the time in the day
242
     *
243
     * @return LocalDate
244
     */
245 1
    public function getNearestStartOfDay()
246
    {
247 1
        if ($this->getHours() < 12) {
248 1
            return $this->getStartOfDay();
249
        }
250
251 1
        return $this->getStartOfNextDay();
252
    }
253
254
    /**
255
     * @return LocalDate
256
     */
257 2 View Code Duplication
    public function getStartOfPreviousDay()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
258
    {
259 2
        $dateCloned = clone $this->getDate();
260
261
        // take switches between summer and winter time into account
262 2
        if ($dateCloned->format('H') > 21) {
263 1
            $dateCloned->modify('-27 hours');
264
        } else {
265 2
            $dateCloned->modify('-24 hours');
266
        }
267
268 2
        $temp = new LocalDate($dateCloned, $this->timezone);
269
270 2
        return $temp->getStartOfDay();
271
    }
272
273
    /**
274
     * @return LocalDate
275
     */
276 2 View Code Duplication
    public function getStartOfNextDay()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
277
    {
278 2
        $dateCloned = clone $this->getDate();
279
280
        // take switches between summer and winter time into account
281 2
        if ($dateCloned->format('H') < 3) {
282 1
            $dateCloned->modify('+27 hours');
283
        } else {
284 2
            $dateCloned->modify('+24 hours');
285
        }
286
287 2
        $temp = new LocalDate($dateCloned, $this->timezone);
288
289 2
        return $temp->getStartOfDay();
290
    }
291
292
    /**
293
     * @return LocalDate
294
     */
295 1
    public function getEndOfDay()
296
    {
297 1
        return $this->getStartOfDay()->modifyByDays(1);
298
    }
299
300
    /**
301
     * @param int $hour
302
     * @param int $minute
303
     * @param int $second
304
     *
305
     * @return LocalDate
306
     */
307 10
    public function modifyTime($hour, $minute, $second)
308
    {
309 10
        $hour   = (int) $hour;
310 10
        $minute = (int) $minute;
311 10
        $second = (int) $second;
312
313 10
        $clonedDate = clone $this->date;
314
315 10
        $clonedDate->setTime((int) $hour, (int) $minute, (int) $second);
316
317 10
        return new LocalDate($clonedDate, $this->timezone);
318
    }
319
320
    /**
321
     * @param int $numSeconds
322
     *
323
     * @return LocalDate
324
     */
325 94
    public function modifyBySeconds($numSeconds)
326
    {
327 94
        return self::fromTimestamp(
328 94
            $this->getTimestamp() + (int) $numSeconds,
329 94
            $this->getTimezone()
330
        );
331
    }
332
333
    /**
334
     * @param float $numMinutes
335
     *
336
     * @return LocalDate
337
     */
338 19
    public function modifyByMinutes($numMinutes)
339
    {
340 19
        return $this->modifyBySeconds(((double) $numMinutes) * 60);
341
    }
342
343
    /**
344
     * @param float $numHours
345
     *
346
     * @return LocalDate
347
     */
348 24
    public function modifyByHours($numHours)
349
    {
350 24
        return $this->modifyBySeconds(((double) $numHours) * 3600);
351
    }
352
353
    /**
354
     * @param float $numDays
355
     *
356
     * @return LocalDate
357
     */
358 32
    public function modifyByDays($numDays)
359
    {
360 32
        return $this->modifyBySeconds(((double) $numDays) * 86400);
361
    }
362
363
    /**
364
     * @param int $numDays
365
     *
366
     * @return LocalDate
367
     */
368 3
    public function modifyByDaysDaylightSavingAware($numDays)
369
    {
370 3
        $numDays   = (int) $numDays;
371 3
        $timestamp = $this->getTimestamp() + (86400 * $numDays);
372
373 3
        $newDate = self::fromTimestamp($timestamp, $this->timezone);
374
375
        // calculate the datetime savings offsets
376 3
        $timezoneInitialDateOffset = $this->timezone->getOffset($this->date);
377 3
        $timezoneNewDateOffset     = $this->timezone->getOffset($newDate->date);
378
379
        // fix the new date if it's needed
380 3
        if ($timezoneInitialDateOffset !== $timezoneNewDateOffset) {
381 3
            $newDate = self::fromTimestamp($timestamp + ($timezoneInitialDateOffset - $timezoneNewDateOffset), $this->timezone);
382
        }
383
384 3
        return $newDate;
385
    }
386
387
    /**
388
     * @param \DateInterval|string $interval
389
     *
390
     * @deprecated use addInterval or subInterval
391
     *
392
     * @return LocalDate
393
     *
394
     * @throws \Exception When the given param is not a valid date interval
395
     */
396
    public function modifyByInterval($interval)
397
    {
398
        return $this->addInterval($interval);
399
    }
400
401
    /**
402
     * @param \DateInterval|string $interval
403
     *
404
     * @return LocalDate
405
     *
406
     * @throws \Exception When the given param is not a valid date interval
407
     */
408 10 View Code Duplication
    public function addInterval($interval)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
409
    {
410 10
        if (! $interval instanceof \DateInterval) {
411 10
            $interval = new \DateInterval($interval);
412
        }
413
414 10
        $dateCloned = clone $this->date;
415 10
        $dateCloned->add($interval);
416
417 10
        return new LocalDate($dateCloned, $this->timezone);
418
    }
419
420
    /**
421
     * @param \DateInterval|string $interval
422
     *
423
     * @return LocalDate
424
     *
425
     * @throws \Exception When the given param is not a valid date interval
426
     */
427 10 View Code Duplication
    public function subInterval($interval)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
428
    {
429 10
        if (! $interval instanceof \DateInterval) {
430 10
            $interval = new \DateInterval($interval);
431
        }
432
433 10
        $dateCloned = clone $this->date;
434 10
        $dateCloned->sub($interval);
435
436 10
        return new LocalDate($dateCloned, $this->timezone);
437
    }
438
439
    /**
440
     * @param $minutesInterval
441
     *
442
     * @return LocalDate
443
     */
444 1
    public function alignToMinutesInterval($minutesInterval)
445
    {
446 1
        if ($minutesInterval <= 0) {
447 1
            return $this;
448
        }
449
450
        // This is a work-around for the day-light-saving shift days
451
        // If we would use minutesIntoDay and then add those to startOfDay, we loose one hour.
452
        // Example would be '2015-03-29 11:20' with tz 'Europe/Berlin' would result in '2015-03-29 10:00'
453 1
        $minutesIntoDay = ((int) $this->date->format('H') * 60) + ((int) $this->date->format('i'));
454
        // cut off partial intervals
455 1
        $corrected = ((int) ($minutesIntoDay / $minutesInterval)) * $minutesInterval;
456
457 1
        return $this->getStartOfDay()->modifyByMinutes($corrected);
458
    }
459
460
    /**
461
     * @return int
462
     */
463 1
    public function getMinutesIntoDay()
464
    {
465 1
        $day  = $this->getStartOfDay();
466 1
        $diff = $this->getTimestamp() - $day->getTimestamp();
467
468 1
        return (int) ($diff / 60);
469
    }
470
471
    /**
472
     * Get the hours portion of the current time
473
     *
474
     * @return int
475
     */
476 1
    public function getHours()
477
    {
478 1
        return (int) $this->format('H');
479
    }
480
481
    /**
482
     * Get the weekday with Sun = 0, Mon = 1, ... Sat = 6
483
     *
484
     * @return int
485
     */
486 1
    public function getWeekday()
487
    {
488 1
        $day = (int) $this->format('N');
489
490 1
        if ($day === 7) {
491 1
            return 0;
492
        }
493
494 1
        return $day;
495
    }
496
497
    ////  COMPARISON METHODS  //////////////////////////////////////////////////////////////////////////////////////////
498
499
    /**
500
     * Returns the data that is earlier, either the current or the other
501
     *
502
     * @param LocalDate|\DateTime $other
503
     *
504
     * @return LocalDate
505
     */
506 1
    public function min($other)
507
    {
508 1
        $other = $this->ensure($other);
509
510 1
        return $this->isBefore($other) ? $this : $other;
511
    }
512
513
    /**
514
     * Returns the data that is later, either the current or the other
515
     *
516
     * @param LocalDate|\DateTime $other
517
     *
518
     * @return LocalDate
519
     */
520 1
    public function max($other)
521
    {
522 1
        $other = $this->ensure($other);
523
524 1
        return $this->isAfter($other) ? $this : $other;
525
    }
526
527
    /**
528
     * Get the number of seconds between this and the other.
529
     *
530
     * The result will be negative when this is after the other
531
     *
532
     * @param LocalDate|\DateTime $other
533
     *
534
     * @return float
535
     */
536 82
    public function diffInSeconds($other)
537
    {
538 82
        $other = $this->ensure($other);
539
540 82
        return ($other->getTimestamp() - $this->getTimestamp());
541
    }
542
543
    /**
544
     * Get the number of minutes between this and the other.
545
     *
546
     * The result will be negative when this is after the other
547
     *
548
     * @param LocalDate|\DateTime $other
549
     *
550
     * @return float
551
     */
552 18
    public function diffInMinutes($other)
553
    {
554 18
        $other = $this->ensure($other);
555
556 18
        return $this->diffInSeconds($other) / 60;
557
    }
558
559
    /**
560
     * Get the number of minutes between this and the other.
561
     *
562
     * The result will be negative when this is after the other
563
     *
564
     * @param LocalDate|\DateTime $other
565
     *
566
     * @return float
567
     */
568 18
    public function diffInHours($other)
569
    {
570 18
        $other = $this->ensure($other);
571
572 18
        return $this->diffInSeconds($other) / 3600;
573
    }
574
575
    /**
576
     * Get the number of minutes between this and the other.
577
     *
578
     * The result will be negative when this is after the other
579
     *
580
     * @param LocalDate|\DateTime $other
581
     *
582
     * @return float
583
     */
584 28
    public function diffInDays($other)
585
    {
586 28
        $other = $this->ensure($other);
587
588 28
        return $this->diffInSeconds($other) / 86400;
589
    }
590
591
    /**
592
     * @param LocalDate|\DateTime $other
593
     *
594
     * @return bool
595
     */
596 6
    public function isBefore($other)
597
    {
598 6
        $other = $this->ensure($other);
599
600 6
        return $this->date < $other->date;
601
    }
602
603
    /**
604
     * @param LocalDate|\DateTime $other
605
     *
606
     * @return bool
607
     */
608 1
    public function isBeforeOrEqual($other)
609
    {
610 1
        $other = $this->ensure($other);
611
612 1
        return $this->date <= $other->date;
613
    }
614
615
    /**
616
     * @param LocalDate|\DateTime $other
617
     *
618
     * @return bool
619
     */
620 7
    public function isEqual($other)
621
    {
622 7
        $other = $this->ensure($other);
623
624 7
        return $this->date->getTimestamp() === $other->date->getTimestamp();
625
    }
626
627
    /**
628
     * @param LocalDate|\DateTime $other
629
     *
630
     * @return bool
631
     */
632 1
    public function isAfter($other)
633
    {
634 1
        $other = $this->ensure($other);
635
636 1
        return $this->date > $other->date;
637
    }
638
639
    /**
640
     * @param LocalDate|\DateTime $other
641
     *
642
     * @return bool
643
     */
644 1
    public function isAfterOrEqual($other)
645
    {
646 1
        $other = $this->ensure($other);
647
648 1
        return $this->date >= $other->date;
649
    }
650
651
    ////  PRIVATE HELPER  //////////////////////////////////////////////////////////////////////////////////////////////
652
653
    /**
654
     * @param mixed $input
655
     *
656
     * @return LocalDate
657
     */
658 98
    private function ensure($input)
659
    {
660 98
        if ($input instanceof self) {
661 97
            return $input;
662
        }
663
664 3
        if ($input instanceof \DateTime) {
665 2
            return new LocalDate($input, $this->timezone);
666
        }
667
668 1
        return new LocalDate($input, $this->timezone);
669
    }
670
}
671