Completed
Push — master ( 622f8e...671128 )
by Karsten
06:08
created

LocalDate::__construct()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 42
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 42
c 0
b 0
f 0
ccs 20
cts 20
cp 1
rs 8.5806
cc 4
eloc 18
nc 6
nop 2
crap 4
1
<?php
2
/**
3
 * File was created 11.03.2015 14:51
4
 *
5
 * @author Karsten J. Gerber <[email protected]>
6
 */
7
namespace PeekAndPoke\Horizons\DateAndTime;
8
9
/**
10
 * Date
11
 *
12
 * @author Karsten J. Gerber <[email protected]>
13
 */
14
class LocalDate
15
{
16
    /** @var \DateTime */
17
    private $date;
18
    /** @var \DateTimeZone */
19
    private $timezone;
20
21
    /**
22
     * @param int                  $timestamp The unix timestamp
23
     * @param \DateTimeZone|string $timezone  The timezone or a string the DateTimeZone c'tor can understand
24
     *
25
     * @return LocalDate
26
     */
27 92
    public static function fromTimestamp($timestamp, $timezone)
28
    {
29 92
        return new LocalDate($timestamp, $timezone);
30
    }
31
32
    /**
33
     * @return LocalDate
34
     */
35 1
    public static function now()
36
    {
37 1
        return self::raw(new \DateTime());
38
    }
39
40
    /**
41
     * @param \DateTime $dateTime
42
     * @return LocalDate
43
     */
44 11
    public static function raw(\DateTime $dateTime)
45
    {
46 11
        $tz = $dateTime->getTimezone();
47
48
        // normalize input coming from Javascript or Java etc...
49 11
        if ($tz->getName() === 'Z') {
50 2
            $tz = new \DateTimeZone('Etc/UTC');
51 2
        }
52
53 11
        return new LocalDate($dateTime, $tz);
54
    }
55
56
    /**
57
     * @param \DateTime|string|float $input    The date or a string the DateTime c'tor can understand or a timestamp
58
     * @param \DateTimeZone|string   $timezone The timezone or a string the DateTimeZone c'tor can understand
59
     */
60 190
    public function __construct($input, $timezone)
61
    {
62 190
        if (! $timezone instanceof \DateTimeZone) {
63 179
            $timezone = new \DateTimeZone((string) $timezone);
64 179
        }
65
66
        /////
67
        // We have to trick PHP a bit here, but why?
68
        //
69
        // Apparently things behave different, when setting a timezone like
70
        // a) 'Europe/Berlin'
71
        // b) '+02:00'
72
        //
73
        // When the date already has a timezone set then
74
        // a) will only set the timezone
75
        // b) will set the timezone and will also change the timestamp by 2 hours
76
        //
77
        // Therefore we create a fresh date, that does not have a timezone yet, set the timestamp, and then apply the
78
        // timezone
79
        //
80
        // See the unit tests for more as well
81
        ////
82 190
        if ($input instanceof \DateTime) {
83 60
            $date = (new \DateTime())
84 60
                ->setTimestamp($input->getTimestamp())
85 60
                ->setTimezone($timezone);
86 190
        } elseif (is_numeric($input)) {
87 101
            $date = (new \DateTime())
88 101
                ->setTimestamp($input)
89 101
                ->setTimezone($timezone);
90 101
        } else {
91
            // when we have string input, we immediately use the timezone
92 145
            $tmp = new \DateTime($input, $timezone);
93
            // we reconstruct the date time again in order to set the timezone on the inner one
94 145
            $date = (new \DateTime())
95 145
                ->setTimestamp($tmp->getTimestamp())
96 145
                ->setTimezone($timezone);
97
        }
98
99 190
        $this->date     = $date;
100 190
        $this->timezone = $timezone;
101 190
    }
102
103
    /**
104
     * @return string
105
     */
106 1
    public function __toString()
107
    {
108 1
        return $this->format();
109
    }
110
111
    /**
112
     * @return \DateTime
113
     */
114 57
    public function getDate()
115
    {
116 57
        return clone $this->date;
117
    }
118
119
    /**
120
     * @return int
121
     */
122 103
    public function getTimestamp()
123
    {
124 103
        return $this->date->getTimestamp();
125
    }
126
127
    /**
128
     * @return \DateTimeZone
129
     */
130 138
    public function getTimezone()
131
    {
132 138
        return clone $this->timezone;
133
    }
134
135
    /**
136
     * @return int
137
     */
138 2
    public function getOffset()
139
    {
140 2
        return $this->date->getOffset();
141
    }
142
143
    /**
144
     * @return float
145
     */
146 1
    public function getOffsetInMinutes()
147
    {
148 1
        $offset = $this->getTimezone()->getOffset($this->getDate());
149
150 1
        return $offset / 60;
151
    }
152
153
    /**
154
     * @return float
155
     */
156 1
    public function getOffsetInHours()
157
    {
158 1
        $offset = $this->getTimezone()->getOffset($this->getDate());
159
160 1
        return $offset / 3600;
161
    }
162
163
    /**
164
     * @param string $format
165
     *
166
     * @return string
167
     */
168 160
    public function format($format = 'c')
169
    {
170 160
        return $this->date->format($format);
171
    }
172
173
    /**
174
     * @return LocalDate
175
     */
176 1
    public function getClone()
177
    {
178 1
        return new LocalDate($this->date, $this->timezone);
179
    }
180
181
    /**
182
     * Get the time saving shift on this date in seconds
183
     *
184
     * @return int
185
     */
186 1
    public function getDaylightSavingShift()
187
    {
188 1
        $previousNoon = $this->getStartOfPreviousDay()->modifyByHours(12);
189
190 1
        return  $this->getOffset() - $previousNoon->getOffset();
191
    }
192
193
    //// Daylight saving time shift aware methods //////////////////////////////////////////////////////////////////////
194
195
    /**
196
     * Add hours to start of day while also respecting Daylight-saving-time shift
197
     *
198
     * E.g. on 2016-03-27T10:00:00 in Berlin is only 9 hours after the start of the day.
199
     *
200
     * @param $hours
201
     *
202
     * @return LocalDate
203
     */
204
    public function getDstStartOfDayPlusHours ($hours)
205
    {
206
        $startOfDay = $this->getStartOfDay();
207
        $startOfDayOffset = $startOfDay->getOffset();
208
209
        $result = $startOfDay->modifyByHours($hours);
210
        $resultOffset = $result->getOffset();
211
212
        return $result->modifyBySeconds($startOfDayOffset)->modifyBySeconds(0 - $resultOffset);
213
    }
214
215
    //// Modification methods //////////////////////////////////////////////////////////////////////////////////////////
216
217
    /**
218
     * @return LocalDate
219
     */
220 18
    public function getStartOfDay()
221
    {
222 18
        return $this->modifyTime(0, 0, 0);
223
    }
224
225
    /**
226
     * Get the nearest start of a day, either the current or the next day depending on the time in the day
227
     *
228
     * @return LocalDate
229
     */
230 1
    public function getNearestStartOfDay()
231
    {
232 1
        if ($this->getHours() < 12) {
233 1
            return $this->getStartOfDay();
234
        }
235
236 1
        return $this->getStartOfNextDay();
237
    }
238
239
    /**
240
     * @return LocalDate
241
     */
242 2 View Code Duplication
    public function getStartOfPreviousDay()
243
    {
244 2
        $dateCloned = clone $this->getDate();
245
246
        // take switches between summer and winter time into account
247 2
        if ($dateCloned->format('H') > 21) {
248 1
            $dateCloned->modify('-27 hours');
249 1
        } else {
250 2
            $dateCloned->modify('-24 hours');
251
        }
252
253 2
        $temp = new LocalDate($dateCloned, $this->timezone);
254
255 2
        return $temp->getStartOfDay();
256
    }
257
258
    /**
259
     * @return LocalDate
260
     */
261 2 View Code Duplication
    public function getStartOfNextDay()
262
    {
263 2
        $dateCloned = clone $this->getDate();
264
265
        // take switches between summer and winter time into account
266 2
        if ($dateCloned->format('H') < 3) {
267 1
            $dateCloned->modify('+27 hours');
268 1
        } else {
269 2
            $dateCloned->modify('+24 hours');
270
        }
271
272 2
        $temp = new LocalDate($dateCloned, $this->timezone);
273
274 2
        return $temp->getStartOfDay();
275
    }
276
277
    /**
278
     * @return LocalDate
279
     */
280 1
    public function getEndOfDay()
281
    {
282 1
        return $this->getStartOfDay()->modifyByDays(1);
283
    }
284
285
    /**
286
     * @param int   $hour
287
     * @param int   $minute
288
     * @param int   $second
289
     *
290
     * @return LocalDate
291
     */
292 18
    public function modifyTime($hour, $minute, $second)
293
    {
294 18
        $hour   = (int) $hour;
295 18
        $minute = (int) $minute;
296 18
        $second = (int) $second;
297
298 18
        $clonedDate = clone $this->date;
299
300 18
        $clonedDate->setTime((int) $hour, (int) $minute, (int) $second);
301
302 18
        return new LocalDate($clonedDate, $this->timezone);
303
    }
304
305
    /**
306
     * @param int $numSeconds
307
     *
308
     * @return LocalDate
309
     */
310 85
    public function modifyBySeconds($numSeconds)
311
    {
312 85
        return LocalDate::fromTimestamp(
313 85
            $this->getTimestamp() + (int) $numSeconds,
314 85
            $this->getTimezone()
315 85
        );
316
    }
317
318
    /**
319
     * @param float $numMinutes
320
     *
321
     * @return LocalDate
322
     */
323 19
    public function modifyByMinutes($numMinutes)
324
    {
325 19
        return $this->modifyBySeconds(((double) $numMinutes) * 60);
326
    }
327
328
    /**
329
     * @param float $numHours
330
     *
331
     * @return LocalDate
332
     */
333 19
    public function modifyByHours($numHours)
334
    {
335 19
        return $this->modifyBySeconds(((double) $numHours) * 3600);
336
    }
337
338
    /**
339
     * @param float $numDays
340
     *
341
     * @return LocalDate
342
     */
343 29
    public function modifyByDays($numDays)
344
    {
345 29
        return $this->modifyBySeconds(((double) $numDays) * 86400);
346
    }
347
348
    /**
349
     * @param int $numDays
350
     *
351
     * @return LocalDate
352
     */
353
    public function modifyByDaysDaylightSavingAware($numDays)
354
    {
355
        $numDays   = (int) $numDays;
356
        $timestamp = $this->getTimestamp() + (86400 * $numDays);
357
358
        $newDate = LocalDate::fromTimestamp($timestamp, $this->timezone);
359
360
        // calculate the datetime savings offsets
361
        $timezoneInitialDateOffset = $this->timezone->getOffset($this->date);
362
        $timezoneNewDateOffset = $this->timezone->getOffset($newDate->date);
363
364
        // fix the new date if it's needed
365
        if ($timezoneInitialDateOffset !== $timezoneNewDateOffset) {
366
            $newDate = LocalDate::fromTimestamp($timestamp + ($timezoneInitialDateOffset - $timezoneNewDateOffset), $this->timezone);
367
        }
368
369
        return $newDate;
370
    }
371
372
    /**
373
     * @param \DateInterval|string $interval
374
     *
375
     * @deprecated use addInterval or subInterval
376
     *
377
     * @return LocalDate
378
     */
379
    public function modifyByInterval($interval)
380
    {
381
        return $this->addInterval($interval);
382
    }
383
384
    /**
385
     * @param \DateInterval|string $interval
386
     *
387
     * @return LocalDate
388
     */
389 10 View Code Duplication
    public function addInterval($interval)
390
    {
391 10
        if (!$interval instanceof \DateInterval) {
392 10
            $interval = new \DateInterval($interval);
393 10
        }
394
395 10
        $dateCloned = clone $this->date;
396 10
        $dateCloned->add($interval);
397
398 10
        return new LocalDate($dateCloned, $this->timezone);
399
    }
400
401
    /**
402
     * @param \DateInterval|string $interval
403
     *
404
     * @return LocalDate
405
     */
406 10 View Code Duplication
    public function subInterval($interval)
407
    {
408 10
        if (!$interval instanceof \DateInterval) {
409 10
            $interval = new \DateInterval($interval);
410 10
        }
411
412 10
        $dateCloned = clone $this->date;
413 10
        $dateCloned->sub($interval);
414
415 10
        return new LocalDate($dateCloned, $this->timezone);
416
    }
417
418
    /**
419
     * @param $minutesInterval
420
     *
421
     * @return LocalDate
422
     */
423 1
    public function alignToMinutesInterval($minutesInterval)
424
    {
425 1
        if ($minutesInterval <= 0) {
426 1
            return $this;
427
        }
428
429
        // This is a work-around for the day-light-saving shift days
430
        // If we would use minutesIntoDay and then add those to startOfDay, we loose one hour.
431
        // Example would be '2015-03-29 11:20' with tz 'Europe/Berlin' would result in '2015-03-29 10:00'
432 1
        $minutesIntoDay = ((int)$this->date->format('H') * 60) + ((int)$this->date->format('i'));
433
        // cut off partial intervals
434 1
        $corrected = ((int)($minutesIntoDay / $minutesInterval)) * $minutesInterval;
435
436 1
        return $this->getStartOfDay()->modifyByMinutes($corrected);
437
    }
438
439
    /**
440
     * @return int
441
     */
442 1
    public function getMinutesIntoDay()
443
    {
444 1
        $day  = $this->getStartOfDay();
445 1
        $diff = $this->getTimestamp() - $day->getTimestamp();
446
447 1
        return (int) ($diff / 60);
448
    }
449
450
    /**
451
     * Get the hours portion of the current time
452
     *
453
     * @return int
454
     */
455 1
    public function getHours()
456
    {
457 1
        return (int) $this->format('H');
458
    }
459
460
    /**
461
     * Get the weekday with Sun = 0, Mon = 1, ... Sat = 6
462
     *
463
     * @return int
464
     */
465 1
    public function getWeekday()
466
    {
467 1
        $day = (int) $this->format('N');
468
469 1
        if ($day === 7) {
470 1
            return 0;
471
        }
472
473 1
        return $day;
474
    }
475
476
    ////  COMPARISON METHODS  //////////////////////////////////////////////////////////////////////////////////////////
477
478
    /**
479
     * Returns the data that is earlier, either the current or the other
480
     *
481
     * @param LocalDate|\DateTime $other
482
     *
483
     * @return LocalDate
484
     */
485 1
    public function min($other)
486
    {
487 1
        $other = $this->ensure($other);
488
489 1
        return $this->isBefore($other) ? $this : $other;
490
    }
491
492
    /**
493
     * Returns the data that is later, either the current or the other
494
     *
495
     * @param LocalDate|\DateTime $other
496
     *
497
     * @return LocalDate
498
     */
499 1
    public function max($other)
500
    {
501 1
        $other = $this->ensure($other);
502
503 1
        return $this->isAfter($other) ? $this : $other;
504
    }
505
506
    /**
507
     * Get the number of seconds between this and the other.
508
     *
509
     * The result will be negative when this is after the other
510
     *
511
     * @param LocalDate|\DateTime $other
512
     *
513
     * @return float
514
     */
515 82
    public function diffInSeconds($other)
516
    {
517 82
        $other = $this->ensure($other);
518
519 82
        return ($other->getTimestamp() - $this->getTimestamp());
520
    }
521
522
    /**
523
     * Get the number of minutes between this and the other.
524
     *
525
     * The result will be negative when this is after the other
526
     *
527
     * @param LocalDate|\DateTime $other
528
     *
529
     * @return float
530
     */
531 18
    public function diffInMinutes($other)
532
    {
533 18
        $other = $this->ensure($other);
534
535 18
        return $this->diffInSeconds($other) / 60;
536
    }
537
538
    /**
539
     * Get the number of minutes between this and the other.
540
     *
541
     * The result will be negative when this is after the other
542
     *
543
     * @param LocalDate|\DateTime $other
544
     *
545
     * @return float
546
     */
547 18
    public function diffInHours($other)
548
    {
549 18
        $other = $this->ensure($other);
550
551 18
        return $this->diffInSeconds($other) / 3600;
552
    }
553
554
    /**
555
     * Get the number of minutes between this and the other.
556
     *
557
     * The result will be negative when this is after the other
558
     *
559
     * @param LocalDate|\DateTime $other
560
     *
561
     * @return float
562
     */
563 28
    public function diffInDays($other)
564
    {
565 28
        $other = $this->ensure($other);
566
567 28
        return $this->diffInSeconds($other) / 86400;
568
    }
569
570
    /**
571
     * @param LocalDate|\DateTime $other
572
     *
573
     * @return bool
574
     */
575 1
    public function isBefore($other)
576
    {
577 1
        $other = $this->ensure($other);
578
579 1
        return $this->date < $other->date;
580
    }
581
582
    /**
583
     * @param LocalDate|\DateTime $other
584
     *
585
     * @return bool
586
     */
587 1
    public function isBeforeOrEqual($other)
588
    {
589 1
        $other = $this->ensure($other);
590
591 1
        return $this->date <= $other->date;
592
    }
593
594
    /**
595
     * @param LocalDate|\DateTime $other
596
     *
597
     * @return bool
598
     */
599 10
    public function isEqual($other)
600
    {
601 10
        $other = $this->ensure($other);
602
603 10
        return $this->date->getTimestamp() === $other->date->getTimestamp();
604
    }
605
606
    /**
607
     * @param LocalDate|\DateTime $other
608
     *
609
     * @return bool
610
     */
611 1
    public function isAfter($other)
612
    {
613 1
        $other = $this->ensure($other);
614
615 1
        return $this->date > $other->date;
616
    }
617
618
    /**
619
     * @param LocalDate|\DateTime $other
620
     *
621
     * @return bool
622
     */
623 1
    public function isAfterOrEqual($other)
624
    {
625 1
        $other = $this->ensure($other);
626
627 1
        return $this->date >= $other->date;
628
    }
629
630
    ////  PRIVATE HELPER  //////////////////////////////////////////////////////////////////////////////////////////////
631
632
    /**
633
     * @param mixed $input
634
     *
635
     * @return LocalDate
636
     */
637 96
    private function ensure($input)
638
    {
639 96
        if ($input instanceof LocalDate) {
640 96
            return $input;
641
        }
642
643 2
        if ($input instanceof \DateTime) {
644 2
            return new LocalDate($input, $this->timezone);
645
        }
646
647
        return new LocalDate($input, $this->timezone);
648
    }
649
}
650