• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 14533829196

18 Apr 2025 10:35AM UTC coverage: 84.364% (-0.03%) from 84.395%
14533829196

Pull #9528

github

web-flow
Merge 0ebc0711e into 115d37e8a
Pull Request #9528: feat: add Time::addCalendarMonths() function

0 of 9 new or added lines in 1 file covered. (0.0%)

5 existing lines in 1 file now uncovered.

20816 of 24674 relevant lines covered (84.36%)

191.02 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.62
/system/I18n/TimeTrait.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\I18n;
15

16
use CodeIgniter\I18n\Exceptions\I18nException;
17
use DateInterval;
18
use DateTime;
19
use DateTimeImmutable;
20
use DateTimeInterface;
21
use DateTimeZone;
22
use Exception;
23
use IntlCalendar;
24
use IntlDateFormatter;
25
use Locale;
26
use ReturnTypeWillChange;
27

28
/**
29
 * This trait has properties and methods for Time and TimeLegacy.
30
 * When TimeLegacy is removed, this will be in Time.
31
 */
32
trait TimeTrait
33
{
34
    /**
35
     * @var DateTimeZone|string
36
     */
37
    protected $timezone;
38

39
    /**
40
     * @var string
41
     */
42
    protected $locale;
43

44
    /**
45
     * Format to use when displaying datetime through __toString
46
     *
47
     * @var string
48
     */
49
    protected $toStringFormat = 'yyyy-MM-dd HH:mm:ss';
50

51
    /**
52
     * Used to check time string to determine if it is relative time or not....
53
     *
54
     * @var string
55
     */
56
    protected static $relativePattern = '/this|next|last|tomorrow|yesterday|midnight|today|[+-]|first|last|ago/i';
57

58
    /**
59
     * @var DateTimeInterface|static|null
60
     */
61
    protected static $testNow;
62

63
    // --------------------------------------------------------------------
64
    // Constructors
65
    // --------------------------------------------------------------------
66

67
    /**
68
     * Time constructor.
69
     *
70
     * @param DateTimeZone|string|null $timezone
71
     *
72
     * @throws Exception
73
     */
74
    public function __construct(?string $time = null, $timezone = null, ?string $locale = null)
75
    {
76
        $this->locale = $locale !== null && $locale !== '' && $locale !== '0' ? $locale : Locale::getDefault();
6,618✔
77

78
        $time ??= '';
6,618✔
79

80
        // If a test instance has been provided, use it instead.
81
        if ($time === '' && static::$testNow instanceof static) {
6,618✔
82
            if ($timezone !== null) {
84✔
83
                $testNow = static::$testNow->setTimezone($timezone);
44✔
84
                $time    = $testNow->format('Y-m-d H:i:s.u');
44✔
85
            } else {
86
                $timezone = static::$testNow->getTimezone();
40✔
87
                $time     = static::$testNow->format('Y-m-d H:i:s.u');
40✔
88
            }
89
        }
90

91
        $timezone       = $timezone ?: date_default_timezone_get();
6,618✔
92
        $this->timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone);
6,618✔
93

94
        // If the time string was a relative string (i.e. 'next Tuesday')
95
        // then we need to adjust the time going in so that we have a current
96
        // timezone to work with.
97
        if ($time !== '' && static::hasRelativeKeywords($time)) {
6,618✔
98
            $instance = new DateTime('now', $this->timezone);
4✔
99
            $instance->modify($time);
4✔
100
            $time = $instance->format('Y-m-d H:i:s.u');
4✔
101
        }
102

103
        parent::__construct($time, $this->timezone);
6,618✔
104
    }
105

106
    /**
107
     * Returns a new Time instance with the timezone set.
108
     *
109
     * @param DateTimeZone|string|null $timezone
110
     *
111
     * @return static
112
     *
113
     * @throws Exception
114
     */
115
    public static function now($timezone = null, ?string $locale = null)
116
    {
117
        return new static(null, $timezone, $locale);
6,618✔
118
    }
119

120
    /**
121
     * Returns a new Time instance while parsing a datetime string.
122
     *
123
     * Example:
124
     *  $time = Time::parse('first day of December 2008');
125
     *
126
     * @param DateTimeZone|string|null $timezone
127
     *
128
     * @return static
129
     *
130
     * @throws Exception
131
     */
132
    public static function parse(string $datetime, $timezone = null, ?string $locale = null)
133
    {
134
        return new static($datetime, $timezone, $locale);
229✔
135
    }
136

137
    /**
138
     * Return a new time with the time set to midnight.
139
     *
140
     * @param DateTimeZone|string|null $timezone
141
     *
142
     * @return static
143
     *
144
     * @throws Exception
145
     */
146
    public static function today($timezone = null, ?string $locale = null)
147
    {
148
        return new static(date('Y-m-d 00:00:00'), $timezone, $locale);
4✔
149
    }
150

151
    /**
152
     * Returns an instance set to midnight yesterday morning.
153
     *
154
     * @param DateTimeZone|string|null $timezone
155
     *
156
     * @return static
157
     *
158
     * @throws Exception
159
     */
160
    public static function yesterday($timezone = null, ?string $locale = null)
161
    {
162
        return new static(date('Y-m-d 00:00:00', strtotime('-1 day')), $timezone, $locale);
2✔
163
    }
164

165
    /**
166
     * Returns an instance set to midnight tomorrow morning.
167
     *
168
     * @param DateTimeZone|string|null $timezone
169
     *
170
     * @return static
171
     *
172
     * @throws Exception
173
     */
174
    public static function tomorrow($timezone = null, ?string $locale = null)
175
    {
176
        return new static(date('Y-m-d 00:00:00', strtotime('+1 day')), $timezone, $locale);
2✔
177
    }
178

179
    /**
180
     * Returns a new instance based on the year, month and day. If any of those three
181
     * are left empty, will default to the current value.
182
     *
183
     * @param DateTimeZone|string|null $timezone
184
     *
185
     * @return static
186
     *
187
     * @throws Exception
188
     */
189
    public static function createFromDate(?int $year = null, ?int $month = null, ?int $day = null, $timezone = null, ?string $locale = null)
190
    {
191
        return static::create($year, $month, $day, null, null, null, $timezone, $locale);
8✔
192
    }
193

194
    /**
195
     * Returns a new instance with the date set to today, and the time set to the values passed in.
196
     *
197
     * @param DateTimeZone|string|null $timezone
198
     *
199
     * @return static
200
     *
201
     * @throws Exception
202
     */
203
    public static function createFromTime(?int $hour = null, ?int $minutes = null, ?int $seconds = null, $timezone = null, ?string $locale = null)
204
    {
205
        return static::create(null, null, null, $hour, $minutes, $seconds, $timezone, $locale);
6✔
206
    }
207

208
    /**
209
     * Returns a new instance with the date time values individually set.
210
     *
211
     * @param DateTimeZone|string|null $timezone
212
     *
213
     * @return static
214
     *
215
     * @throws Exception
216
     */
217
    public static function create(
218
        ?int $year = null,
219
        ?int $month = null,
220
        ?int $day = null,
221
        ?int $hour = null,
222
        ?int $minutes = null,
223
        ?int $seconds = null,
224
        $timezone = null,
225
        ?string $locale = null,
226
    ) {
227
        $year ??= date('Y');
32✔
228
        $month ??= date('m');
32✔
229
        $day ??= date('d');
32✔
230
        $hour ??= 0;
32✔
231
        $minutes ??= 0;
32✔
232
        $seconds ??= 0;
32✔
233

234
        return new static(date('Y-m-d H:i:s', strtotime("{$year}-{$month}-{$day} {$hour}:{$minutes}:{$seconds}")), $timezone, $locale);
32✔
235
    }
236

237
    /**
238
     * Provides a replacement for DateTime's own createFromFormat function, that provides
239
     * more flexible timeZone handling
240
     *
241
     * @param string                   $format
242
     * @param string                   $datetime
243
     * @param DateTimeZone|string|null $timezone
244
     *
245
     * @return static
246
     *
247
     * @throws Exception
248
     */
249
    #[ReturnTypeWillChange]
250
    public static function createFromFormat($format, $datetime, $timezone = null)
251
    {
252
        if (! $date = parent::createFromFormat($format, $datetime)) {
38✔
253
            throw I18nException::forInvalidFormat($format);
2✔
254
        }
255

256
        return new static($date->format('Y-m-d H:i:s.u'), $timezone);
36✔
257
    }
258

259
    /**
260
     * Returns a new instance with the datetime set based on the provided UNIX timestamp.
261
     *
262
     * @param DateTimeZone|string|null $timezone
263
     *
264
     * @throws Exception
265
     */
266
    public static function createFromTimestamp(float|int $timestamp, $timezone = null, ?string $locale = null): static
267
    {
268
        $time = new static(sprintf('@%.6f', $timestamp), 'UTC', $locale);
16✔
269

270
        $timezone ??= 'UTC';
16✔
271

272
        return $time->setTimezone($timezone);
16✔
273
    }
274

275
    /**
276
     * Takes an instance of DateTimeInterface and returns an instance of Time with it's same values.
277
     *
278
     * @return static
279
     *
280
     * @throws Exception
281
     */
282
    public static function createFromInstance(DateTimeInterface $dateTime, ?string $locale = null)
283
    {
284
        $date     = $dateTime->format('Y-m-d H:i:s.u');
68✔
285
        $timezone = $dateTime->getTimezone();
68✔
286

287
        return new static($date, $timezone, $locale);
68✔
288
    }
289

290
    /**
291
     * Takes an instance of DateTime and returns an instance of Time with it's same values.
292
     *
293
     * @return static
294
     *
295
     * @throws Exception
296
     *
297
     * @deprecated         Use createFromInstance() instead
298
     *
299
     * @codeCoverageIgnore
300
     */
301
    public static function instance(DateTime $dateTime, ?string $locale = null)
302
    {
303
        return static::createFromInstance($dateTime, $locale);
×
304
    }
305

306
    /**
307
     * Converts the current instance to a mutable DateTime object.
308
     *
309
     * @return DateTime
310
     *
311
     * @throws Exception
312
     */
313
    public function toDateTime()
314
    {
315
        return DateTime::createFromFormat(
239✔
316
            'Y-m-d H:i:s.u',
239✔
317
            $this->format('Y-m-d H:i:s.u'),
239✔
318
            $this->getTimezone(),
239✔
319
        );
239✔
320
    }
321

322
    // --------------------------------------------------------------------
323
    // For Testing
324
    // --------------------------------------------------------------------
325

326
    /**
327
     * Creates an instance of Time that will be returned during testing
328
     * when calling 'Time::now()' instead of the current time.
329
     *
330
     * @param DateTimeInterface|self|string|null $datetime
331
     * @param DateTimeZone|string|null           $timezone
332
     *
333
     * @return void
334
     *
335
     * @throws Exception
336
     */
337
    public static function setTestNow($datetime = null, $timezone = null, ?string $locale = null)
338
    {
339
        // Reset the test instance
340
        if ($datetime === null) {
313✔
341
            static::$testNow = null;
313✔
342

343
            return;
313✔
344
        }
345

346
        // Convert to a Time instance
347
        if (is_string($datetime)) {
86✔
348
            $datetime = new static($datetime, $timezone, $locale);
82✔
349
        } elseif ($datetime instanceof DateTimeInterface && ! $datetime instanceof static) {
4✔
350
            $datetime = new static($datetime->format('Y-m-d H:i:s.u'), $timezone);
2✔
351
        }
352

353
        static::$testNow = $datetime;
86✔
354
    }
355

356
    /**
357
     * Returns whether we have a testNow instance saved.
358
     */
359
    public static function hasTestNow(): bool
360
    {
361
        return static::$testNow !== null;
2✔
362
    }
363

364
    // --------------------------------------------------------------------
365
    // Getters
366
    // --------------------------------------------------------------------
367

368
    /**
369
     * Returns the localized Year
370
     *
371
     * @throws Exception
372
     */
373
    public function getYear(): string
374
    {
375
        return $this->toLocalizedString('y');
8✔
376
    }
377

378
    /**
379
     * Returns the localized Month
380
     *
381
     * @throws Exception
382
     */
383
    public function getMonth(): string
384
    {
385
        return $this->toLocalizedString('M');
8✔
386
    }
387

388
    /**
389
     * Return the localized day of the month.
390
     *
391
     * @throws Exception
392
     */
393
    public function getDay(): string
394
    {
395
        return $this->toLocalizedString('d');
4✔
396
    }
397

398
    /**
399
     * Return the localized hour (in 24-hour format).
400
     *
401
     * @throws Exception
402
     */
403
    public function getHour(): string
404
    {
405
        return $this->toLocalizedString('H');
2✔
406
    }
407

408
    /**
409
     * Return the localized minutes in the hour.
410
     *
411
     * @throws Exception
412
     */
413
    public function getMinute(): string
414
    {
415
        return $this->toLocalizedString('m');
2✔
416
    }
417

418
    /**
419
     * Return the localized seconds
420
     *
421
     * @throws Exception
422
     */
423
    public function getSecond(): string
424
    {
425
        return $this->toLocalizedString('s');
2✔
426
    }
427

428
    /**
429
     * Return the index of the day of the week
430
     *
431
     * @throws Exception
432
     */
433
    public function getDayOfWeek(): string
434
    {
435
        return $this->toLocalizedString('c');
2✔
436
    }
437

438
    /**
439
     * Return the index of the day of the year
440
     *
441
     * @throws Exception
442
     */
443
    public function getDayOfYear(): string
444
    {
445
        return $this->toLocalizedString('D');
2✔
446
    }
447

448
    /**
449
     * Return the index of the week in the month
450
     *
451
     * @throws Exception
452
     */
453
    public function getWeekOfMonth(): string
454
    {
455
        return $this->toLocalizedString('W');
2✔
456
    }
457

458
    /**
459
     * Return the index of the week in the year
460
     *
461
     * @throws Exception
462
     */
463
    public function getWeekOfYear(): string
464
    {
465
        return $this->toLocalizedString('w');
2✔
466
    }
467

468
    /**
469
     * Returns the age in years from the date and 'now'
470
     *
471
     * @return int
472
     *
473
     * @throws Exception
474
     */
475
    public function getAge()
476
    {
477
        // future dates have no age
478
        return max(0, $this->difference(static::now())->getYears());
12✔
479
    }
480

481
    /**
482
     * Returns the number of the current quarter for the year.
483
     *
484
     * @throws Exception
485
     */
486
    public function getQuarter(): string
487
    {
488
        return $this->toLocalizedString('Q');
2✔
489
    }
490

491
    /**
492
     * Are we in daylight savings time currently?
493
     */
494
    public function getDst(): bool
495
    {
496
        return $this->format('I') === '1'; // 1 if Daylight Saving Time, 0 otherwise.
4✔
497
    }
498

499
    /**
500
     * Returns boolean whether the passed timezone is the same as
501
     * the local timezone.
502
     */
503
    public function getLocal(): bool
504
    {
505
        $local = date_default_timezone_get();
2✔
506

507
        return $local === $this->timezone->getName();
2✔
508
    }
509

510
    /**
511
     * Returns boolean whether object is in UTC.
512
     */
513
    public function getUtc(): bool
514
    {
515
        return $this->getOffset() === 0;
2✔
516
    }
517

518
    /**
519
     * Returns the name of the current timezone.
520
     */
521
    public function getTimezoneName(): string
522
    {
523
        return $this->timezone->getName();
24✔
524
    }
525

526
    // --------------------------------------------------------------------
527
    // Setters
528
    // --------------------------------------------------------------------
529

530
    /**
531
     * Sets the current year for this instance.
532
     *
533
     * @param int|string $value
534
     *
535
     * @return static
536
     *
537
     * @throws Exception
538
     */
539
    public function setYear($value)
540
    {
541
        return $this->setValue('year', $value);
2✔
542
    }
543

544
    /**
545
     * Sets the month of the year.
546
     *
547
     * @param int|string $value
548
     *
549
     * @return static
550
     *
551
     * @throws Exception
552
     */
553
    public function setMonth($value)
554
    {
555
        if (is_numeric($value) && ($value < 1 || $value > 12)) {
10✔
556
            throw I18nException::forInvalidMonth((string) $value);
4✔
557
        }
558

559
        if (is_string($value) && ! is_numeric($value)) {
6✔
560
            $value = date('m', strtotime("{$value} 1 2017"));
4✔
561
        }
562

563
        return $this->setValue('month', $value);
6✔
564
    }
565

566
    /**
567
     * Sets the day of the month.
568
     *
569
     * @param int|string $value
570
     *
571
     * @return static
572
     *
573
     * @throws Exception
574
     */
575
    public function setDay($value)
576
    {
577
        if ($value < 1 || $value > 31) {
10✔
578
            throw I18nException::forInvalidDay((string) $value);
4✔
579
        }
580

581
        $date    = $this->getYear() . '-' . $this->getMonth();
6✔
582
        $lastDay = date('t', strtotime($date));
6✔
583
        if ($value > $lastDay) {
6✔
584
            throw I18nException::forInvalidOverDay($lastDay, (string) $value);
2✔
585
        }
586

587
        return $this->setValue('day', $value);
4✔
588
    }
589

590
    /**
591
     * Sets the hour of the day (24 hour cycle)
592
     *
593
     * @param int|string $value
594
     *
595
     * @return static
596
     *
597
     * @throws Exception
598
     */
599
    public function setHour($value)
600
    {
601
        if ($value < 0 || $value > 23) {
6✔
602
            throw I18nException::forInvalidHour((string) $value);
4✔
603
        }
604

605
        return $this->setValue('hour', $value);
2✔
606
    }
607

608
    /**
609
     * Sets the minute of the hour
610
     *
611
     * @param int|string $value
612
     *
613
     * @return static
614
     *
615
     * @throws Exception
616
     */
617
    public function setMinute($value)
618
    {
619
        if ($value < 0 || $value > 59) {
6✔
620
            throw I18nException::forInvalidMinutes((string) $value);
4✔
621
        }
622

623
        return $this->setValue('minute', $value);
2✔
624
    }
625

626
    /**
627
     * Sets the second of the minute.
628
     *
629
     * @param int|string $value
630
     *
631
     * @return static
632
     *
633
     * @throws Exception
634
     */
635
    public function setSecond($value)
636
    {
637
        if ($value < 0 || $value > 59) {
6✔
638
            throw I18nException::forInvalidSeconds((string) $value);
4✔
639
        }
640

641
        return $this->setValue('second', $value);
2✔
642
    }
643

644
    /**
645
     * Helper method to do the heavy lifting of the 'setX' methods.
646
     *
647
     * @param int $value
648
     *
649
     * @return static
650
     *
651
     * @throws Exception
652
     */
653
    protected function setValue(string $name, $value)
654
    {
655
        [$year, $month, $day, $hour, $minute, $second] = explode('-', $this->format('Y-n-j-G-i-s'));
18✔
656

657
        ${$name} = $value;
18✔
658

659
        return static::create(
18✔
660
            (int) $year,
18✔
661
            (int) $month,
18✔
662
            (int) $day,
18✔
663
            (int) $hour,
18✔
664
            (int) $minute,
18✔
665
            (int) $second,
18✔
666
            $this->getTimezoneName(),
18✔
667
            $this->locale,
18✔
668
        );
18✔
669
    }
670

671
    /**
672
     * Returns a new instance with the revised timezone.
673
     *
674
     * @param DateTimeZone|string $timezone
675
     *
676
     * @return static
677
     *
678
     * @throws Exception
679
     */
680
    #[ReturnTypeWillChange]
681
    public function setTimezone($timezone)
682
    {
683
        $timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone);
64✔
684

685
        return static::createFromInstance($this->toDateTime()->setTimezone($timezone), $this->locale);
64✔
686
    }
687

688
    // --------------------------------------------------------------------
689
    // Add/Subtract
690
    // --------------------------------------------------------------------
691

692
    /**
693
     * Returns a new Time instance with $seconds added to the time.
694
     *
695
     * @return static
696
     */
697
    public function addSeconds(int $seconds)
698
    {
699
        $time = clone $this;
2✔
700

701
        return $time->add(DateInterval::createFromDateString("{$seconds} seconds"));
2✔
702
    }
703

704
    /**
705
     * Returns a new Time instance with $minutes added to the time.
706
     *
707
     * @return static
708
     */
709
    public function addMinutes(int $minutes)
710
    {
711
        $time = clone $this;
2✔
712

713
        return $time->add(DateInterval::createFromDateString("{$minutes} minutes"));
2✔
714
    }
715

716
    /**
717
     * Returns a new Time instance with $hours added to the time.
718
     *
719
     * @return static
720
     */
721
    public function addHours(int $hours)
722
    {
723
        $time = clone $this;
2✔
724

725
        return $time->add(DateInterval::createFromDateString("{$hours} hours"));
2✔
726
    }
727

728
    /**
729
     * Returns a new Time instance with $days added to the time.
730
     *
731
     * @return static
732
     */
733
    public function addDays(int $days)
734
    {
735
        $time = clone $this;
2✔
736

737
        return $time->add(DateInterval::createFromDateString("{$days} days"));
2✔
738
    }
739

740
    /**
741
     * Returns a new Time instance with $months added to the time.
742
     *
743
     * @return static
744
     */
745
    public function addMonths(int $months)
746
    {
747
        $time = clone $this;
4✔
748

749
        return $time->add(DateInterval::createFromDateString("{$months} months"));
4✔
750
    }
751

752
    /**
753
     * Returns a new Time instance with $months calendar months added to the time.
754
     */
755
    public function addCalendarMonths(int $months): static
756
    {
NEW
757
        $time = clone $this;
×
758

NEW
759
        $year  = (int) $time->getYear();
×
NEW
760
        $month = (int) $time->getMonth() + $months;
×
NEW
761
        $day   = (int) $time->getDay();
×
762

763
        // Adjust year and month for overflow
NEW
764
        $year += intdiv($month - 1, 12);
×
NEW
765
        $month = (($month - 1) % 12) + 1;
×
766

767
        // Find the last valid day of the target month
NEW
768
        $lastDayOfMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year);
×
NEW
769
        $correctedDay   = min($day, $lastDayOfMonth);
×
770

771
        // Return new time instance
NEW
772
        return static::create($year, $month, $correctedDay, (int) $this->getHour(), (int) $this->getMinute(), (int) $this->getSecond(), $this->getTimezone(), $this->locale);
×
773
    }
774

775
    /**
776
     * Returns a new Time instance with $years added to the time.
777
     *
778
     * @return static
779
     */
780
    public function addYears(int $years)
781
    {
782
        $time = clone $this;
2✔
783

784
        return $time->add(DateInterval::createFromDateString("{$years} years"));
2✔
785
    }
786

787
    /**
788
     * Returns a new Time instance with $seconds subtracted from the time.
789
     *
790
     * @return static
791
     */
792
    public function subSeconds(int $seconds)
793
    {
794
        $time = clone $this;
2✔
795

796
        return $time->sub(DateInterval::createFromDateString("{$seconds} seconds"));
2✔
797
    }
798

799
    /**
800
     * Returns a new Time instance with $minutes subtracted from the time.
801
     *
802
     * @return static
803
     */
804
    public function subMinutes(int $minutes)
805
    {
806
        $time = clone $this;
2✔
807

808
        return $time->sub(DateInterval::createFromDateString("{$minutes} minutes"));
2✔
809
    }
810

811
    /**
812
     * Returns a new Time instance with $hours subtracted from the time.
813
     *
814
     * @return static
815
     */
816
    public function subHours(int $hours)
817
    {
818
        $time = clone $this;
2✔
819

820
        return $time->sub(DateInterval::createFromDateString("{$hours} hours"));
2✔
821
    }
822

823
    /**
824
     * Returns a new Time instance with $days subtracted from the time.
825
     *
826
     * @return static
827
     */
828
    public function subDays(int $days)
829
    {
830
        $time = clone $this;
2✔
831

832
        return $time->sub(DateInterval::createFromDateString("{$days} days"));
2✔
833
    }
834

835
    /**
836
     * Returns a new Time instance with $months subtracted from the time.
837
     *
838
     * @return static
839
     */
840
    public function subMonths(int $months)
841
    {
842
        $time = clone $this;
2✔
843

844
        return $time->sub(DateInterval::createFromDateString("{$months} months"));
2✔
845
    }
846

847
    /**
848
     * Returns a new Time instance with $hours subtracted from the time.
849
     *
850
     * @return static
851
     */
852
    public function subYears(int $years)
853
    {
854
        $time = clone $this;
2✔
855

856
        return $time->sub(DateInterval::createFromDateString("{$years} years"));
2✔
857
    }
858

859
    // --------------------------------------------------------------------
860
    // Formatters
861
    // --------------------------------------------------------------------
862

863
    /**
864
     * Returns the localized value of the date in the format 'Y-m-d H:i:s'
865
     *
866
     * @return false|string
867
     *
868
     * @throws Exception
869
     */
870
    public function toDateTimeString()
871
    {
872
        return $this->toLocalizedString('yyyy-MM-dd HH:mm:ss');
89✔
873
    }
874

875
    /**
876
     * Returns a localized version of the date in Y-m-d format.
877
     *
878
     * @return string
879
     *
880
     * @throws Exception
881
     */
882
    public function toDateString()
883
    {
884
        return $this->toLocalizedString('yyyy-MM-dd');
2✔
885
    }
886

887
    /**
888
     * Returns a localized version of the date in nicer date format:
889
     *
890
     *  i.e. Apr 1, 2017
891
     *
892
     * @return string
893
     *
894
     * @throws Exception
895
     */
896
    public function toFormattedDateString()
897
    {
898
        return $this->toLocalizedString('MMM d, yyyy');
2✔
899
    }
900

901
    /**
902
     * Returns a localized version of the time in nicer date format:
903
     *
904
     *  i.e. 13:20:33
905
     *
906
     * @return string
907
     *
908
     * @throws Exception
909
     */
910
    public function toTimeString()
911
    {
912
        return $this->toLocalizedString('HH:mm:ss');
2✔
913
    }
914

915
    /**
916
     * Returns the localized value of this instance in $format.
917
     *
918
     * @return false|string
919
     *
920
     * @throws Exception
921
     */
922
    public function toLocalizedString(?string $format = null)
923
    {
924
        $format ??= $this->toStringFormat;
121✔
925

926
        return IntlDateFormatter::formatObject($this->toDateTime(), $format, $this->locale);
121✔
927
    }
928

929
    // --------------------------------------------------------------------
930
    // Comparison
931
    // --------------------------------------------------------------------
932

933
    /**
934
     * Determines if the datetime passed in is equal to the current instance.
935
     * Equal in this case means that they represent the same moment in time,
936
     * and are not required to be in the same timezone, as both times are
937
     * converted to UTC and compared that way.
938
     *
939
     * @param DateTimeInterface|self|string $testTime
940
     *
941
     * @throws Exception
942
     */
943
    public function equals($testTime, ?string $timezone = null): bool
944
    {
945
        $testTime = $this->getUTCObject($testTime, $timezone);
15✔
946

947
        $ourTime = $this->toDateTime()
15✔
948
            ->setTimezone(new DateTimeZone('UTC'))
15✔
949
            ->format('Y-m-d H:i:s.u');
15✔
950

951
        return $testTime->format('Y-m-d H:i:s.u') === $ourTime;
15✔
952
    }
953

954
    /**
955
     * Ensures that the times are identical, taking timezone into account.
956
     *
957
     * @param DateTimeInterface|self|string $testTime
958
     *
959
     * @throws Exception
960
     */
961
    public function sameAs($testTime, ?string $timezone = null): bool
962
    {
963
        if ($testTime instanceof DateTimeInterface) {
10✔
964
            $testTime = $testTime->format('Y-m-d H:i:s.u O');
6✔
965
        } elseif (is_string($testTime)) {
4✔
966
            $timezone = $timezone !== null && $timezone !== '' && $timezone !== '0' ? $timezone : $this->timezone;
4✔
967
            $timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone);
4✔
968
            $testTime = new DateTime($testTime, $timezone);
4✔
969
            $testTime = $testTime->format('Y-m-d H:i:s.u O');
4✔
970
        }
971

972
        $ourTime = $this->format('Y-m-d H:i:s.u O');
10✔
973

974
        return $testTime === $ourTime;
10✔
975
    }
976

977
    /**
978
     * Determines if the current instance's time is before $testTime,
979
     * after converting to UTC.
980
     *
981
     * @param DateTimeInterface|self|string $testTime
982
     *
983
     * @throws Exception
984
     */
985
    public function isBefore($testTime, ?string $timezone = null): bool
986
    {
987
        $testTime = $this->getUTCObject($testTime, $timezone);
4✔
988

989
        $testTimestamp = $testTime->getTimestamp();
4✔
990
        $ourTimestamp  = $this->getTimestamp();
4✔
991

992
        if ($ourTimestamp === $testTimestamp) {
4✔
993
            return $this->format('u') < $testTime->format('u');
2✔
994
        }
995

996
        return $ourTimestamp < $testTimestamp;
2✔
997
    }
998

999
    /**
1000
     * Determines if the current instance's time is after $testTime,
1001
     * after converting in UTC.
1002
     *
1003
     * @param DateTimeInterface|self|string $testTime
1004
     *
1005
     * @throws Exception
1006
     */
1007
    public function isAfter($testTime, ?string $timezone = null): bool
1008
    {
1009
        $testTime = $this->getUTCObject($testTime, $timezone);
4✔
1010

1011
        $testTimestamp = $testTime->getTimestamp();
4✔
1012
        $ourTimestamp  = $this->getTimestamp();
4✔
1013

1014
        if ($ourTimestamp === $testTimestamp) {
4✔
1015
            return $this->format('u') > $testTime->format('u');
2✔
1016
        }
1017

1018
        return $ourTimestamp > $testTimestamp;
2✔
1019
    }
1020

1021
    // --------------------------------------------------------------------
1022
    // Differences
1023
    // --------------------------------------------------------------------
1024

1025
    /**
1026
     * Returns a text string that is easily readable that describes
1027
     * how long ago, or how long from now, a date is, like:
1028
     *
1029
     *  - 3 weeks ago
1030
     *  - in 4 days
1031
     *  - 6 hours ago
1032
     *
1033
     * @return string
1034
     *
1035
     * @throws Exception
1036
     */
1037
    public function humanize()
1038
    {
1039
        $now  = IntlCalendar::fromDateTime(self::now($this->timezone)->toDateTime());
42✔
1040
        $time = $this->getCalendar()->getTime();
42✔
1041

1042
        $years   = $now->fieldDifference($time, IntlCalendar::FIELD_YEAR);
42✔
1043
        $months  = $now->fieldDifference($time, IntlCalendar::FIELD_MONTH);
42✔
1044
        $days    = $now->fieldDifference($time, IntlCalendar::FIELD_DAY_OF_YEAR);
42✔
1045
        $hours   = $now->fieldDifference($time, IntlCalendar::FIELD_HOUR_OF_DAY);
42✔
1046
        $minutes = $now->fieldDifference($time, IntlCalendar::FIELD_MINUTE);
42✔
1047

1048
        $phrase = null;
42✔
1049

1050
        if ($years !== 0) {
42✔
1051
            $phrase = lang('Time.years', [abs($years)]);
6✔
1052
            $before = $years < 0;
6✔
1053
        } elseif ($months !== 0) {
36✔
1054
            $phrase = lang('Time.months', [abs($months)]);
6✔
1055
            $before = $months < 0;
6✔
1056
        } elseif ($days !== 0 && (abs($days) >= 7)) {
30✔
1057
            $weeks  = ceil($days / 7);
8✔
1058
            $phrase = lang('Time.weeks', [abs($weeks)]);
8✔
1059
            $before = $days < 0;
8✔
1060
        } elseif ($days !== 0) {
22✔
1061
            $before = $days < 0;
10✔
1062

1063
            // Yesterday/Tomorrow special cases
1064
            if (abs($days) === 1) {
10✔
1065
                return $before ? lang('Time.yesterday') : lang('Time.tomorrow');
4✔
1066
            }
1067

1068
            $phrase = lang('Time.days', [abs($days)]);
6✔
1069
        } elseif ($hours !== 0) {
12✔
1070
            $phrase = lang('Time.hours', [abs($hours)]);
4✔
1071
            $before = $hours < 0;
4✔
1072
        } elseif ($minutes !== 0) {
8✔
1073
            $phrase = lang('Time.minutes', [abs($minutes)]);
6✔
1074
            $before = $minutes < 0;
6✔
1075
        } else {
1076
            return lang('Time.now');
2✔
1077
        }
1078

1079
        return $before ? lang('Time.ago', [$phrase]) : lang('Time.inFuture', [$phrase]);
36✔
1080
    }
1081

1082
    /**
1083
     * @param DateTimeInterface|self|string $testTime
1084
     *
1085
     * @return TimeDifference
1086
     *
1087
     * @throws Exception
1088
     */
1089
    public function difference($testTime, ?string $timezone = null)
1090
    {
1091
        if (is_string($testTime)) {
37✔
1092
            $timezone = ($timezone !== null) ? new DateTimeZone($timezone) : $this->timezone;
24✔
1093
            $testTime = new DateTime($testTime, $timezone);
24✔
1094
        } elseif ($testTime instanceof static) {
13✔
1095
            $testTime = $testTime->toDateTime();
13✔
1096
        }
1097

1098
        assert($testTime instanceof DateTime);
1099

1100
        if ($this->timezone->getOffset($this) !== $testTime->getTimezone()->getOffset($this)) {
37✔
UNCOV
1101
            $testTime = $this->getUTCObject($testTime, $timezone);
×
UNCOV
1102
            $ourTime  = $this->getUTCObject($this);
×
1103
        } else {
1104
            $ourTime = $this->toDateTime();
37✔
1105
        }
1106

1107
        return new TimeDifference($ourTime, $testTime);
37✔
1108
    }
1109

1110
    // --------------------------------------------------------------------
1111
    // Utilities
1112
    // --------------------------------------------------------------------
1113

1114
    /**
1115
     * Returns a Time instance with the timezone converted to UTC.
1116
     *
1117
     * @param DateTimeInterface|self|string $time
1118
     *
1119
     * @return DateTime|static
1120
     *
1121
     * @throws Exception
1122
     */
1123
    public function getUTCObject($time, ?string $timezone = null)
1124
    {
1125
        if ($time instanceof static) {
23✔
1126
            $time = $time->toDateTime();
15✔
1127
        } elseif (is_string($time)) {
8✔
1128
            $timezone = $timezone !== null && $timezone !== '' && $timezone !== '0' ? $timezone : $this->timezone;
4✔
1129
            $timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone);
4✔
1130
            $time     = new DateTime($time, $timezone);
4✔
1131
        }
1132

1133
        if ($time instanceof DateTime || $time instanceof DateTimeImmutable) {
23✔
1134
            $time = $time->setTimezone(new DateTimeZone('UTC'));
23✔
1135
        }
1136

1137
        return $time;
23✔
1138
    }
1139

1140
    /**
1141
     * Returns the IntlCalendar object used for this object,
1142
     * taking into account the locale, date, etc.
1143
     *
1144
     * Primarily used internally to provide the difference and comparison functions,
1145
     * but available for public consumption if they need it.
1146
     *
1147
     * @return IntlCalendar
1148
     *
1149
     * @throws Exception
1150
     */
1151
    public function getCalendar()
1152
    {
1153
        return IntlCalendar::fromDateTime($this->toDateTime());
42✔
1154
    }
1155

1156
    /**
1157
     * Check a time string to see if it includes a relative date (like 'next Tuesday').
1158
     */
1159
    protected static function hasRelativeKeywords(string $time): bool
1160
    {
1161
        // skip common format with a '-' in it
1162
        if (preg_match('/\d{4}-\d{1,2}-\d{1,2}/', $time) !== 1) {
368✔
1163
            return preg_match(static::$relativePattern, $time) > 0;
250✔
1164
        }
1165

1166
        return false;
210✔
1167
    }
1168

1169
    /**
1170
     * Outputs a short format version of the datetime.
1171
     * The output is NOT localized intentionally.
1172
     */
1173
    public function __toString(): string
1174
    {
1175
        return $this->format('Y-m-d H:i:s');
18✔
1176
    }
1177

1178
    /**
1179
     * Allow for property-type access to any getX method...
1180
     *
1181
     * Note that we cannot use this for any of our setX methods,
1182
     * as they return new Time objects, but the __set ignores
1183
     * return values.
1184
     * See http://php.net/manual/en/language.oop5.overloading.php
1185
     *
1186
     * @param string $name
1187
     *
1188
     * @return array|bool|DateTimeInterface|DateTimeZone|int|IntlCalendar|self|string|null
1189
     */
1190
    public function __get($name)
1191
    {
1192
        $method = 'get' . ucfirst($name);
38✔
1193

1194
        if (method_exists($this, $method)) {
38✔
1195
            return $this->{$method}();
36✔
1196
        }
1197

1198
        return null;
2✔
1199
    }
1200

1201
    /**
1202
     * Allow for property-type checking to any getX method...
1203
     *
1204
     * @param string $name
1205
     */
1206
    public function __isset($name): bool
1207
    {
1208
        $method = 'get' . ucfirst($name);
4✔
1209

1210
        return method_exists($this, $method);
4✔
1211
    }
1212

1213
    /**
1214
     * This is called when we unserialize the Time object.
1215
     */
1216
    public function __wakeup(): void
1217
    {
1218
        /**
1219
         * Prior to unserialization, this is a string.
1220
         *
1221
         * @var string $timezone
1222
         */
UNCOV
1223
        $timezone = $this->timezone;
×
1224

UNCOV
1225
        $this->timezone = new DateTimeZone($timezone);
×
1226

1227
        // @phpstan-ignore-next-line `$this->date` is a special property for PHP internal use.
UNCOV
1228
        parent::__construct($this->date, $this->timezone);
×
1229
    }
1230
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc