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

codeigniter4 / CodeIgniter4 / 25319842193

04 May 2026 12:47PM UTC coverage: 88.276% (+0.03%) from 88.248%
25319842193

Pull #10150

github

web-flow
Merge 95d543756 into 1efb85ba8
Pull Request #10150: feat: add Query Builder whereColumn methods

30 of 30 new or added lines in 1 file covered. (100.0%)

42 existing lines in 2 files now uncovered.

23492 of 26612 relevant lines covered (88.28%)

218.3 hits per line

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

98.77
/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

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

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

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

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

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

62
    // --------------------------------------------------------------------
63
    // Constructors
64
    // --------------------------------------------------------------------
65

66
    /**
67
     * Time constructor.
68
     *
69
     * @param DateTimeZone|string|null $timezone
70
     *
71
     * @throws Exception
72
     */
73
    public function __construct(?string $time = null, $timezone = null, ?string $locale = null)
74
    {
75
        $this->locale = in_array($locale, [null, '', '0'], true) ? Locale::getDefault() : $locale;
7,593✔
76

77
        $time ??= '';
7,593✔
78

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

90
        $timezone       = $timezone ?: date_default_timezone_get();
7,593✔
91
        $this->timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone);
7,593✔
92

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

102
        parent::__construct($time, $this->timezone);
7,593✔
103
    }
104

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

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

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

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

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

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

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

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

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

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

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

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

268
        $timezone ??= 'UTC';
16✔
269

270
        return $time->setTimezone($timezone);
16✔
271
    }
272

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

285
        return new static($date, $timezone, $locale);
103✔
286
    }
287

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

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

320
    // --------------------------------------------------------------------
321
    // For Testing
322
    // --------------------------------------------------------------------
323

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

341
            return;
377✔
342
        }
343

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

351
        static::$testNow = $datetime;
108✔
352
    }
353

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

362
    // --------------------------------------------------------------------
363
    // Getters
364
    // --------------------------------------------------------------------
365

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

524
    // --------------------------------------------------------------------
525
    // Setters
526
    // --------------------------------------------------------------------
527

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

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

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

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

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

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

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

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

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

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

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

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

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

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

655
        ${$name} = $value;
18✔
656

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

669
    /**
670
     * Returns a new instance with the revised timezone.
671
     *
672
     * @param DateTimeZone|string $timezone
673
     *
674
     * @throws Exception
675
     */
676
    public function setTimezone($timezone): static
677
    {
678
        $timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone);
70✔
679
        $dateTime = $this->toDateTime()->setTimezone($timezone);
70✔
680

681
        return static::createFromInstance($dateTime, $this->locale);
70✔
682
    }
683

684
    // --------------------------------------------------------------------
685
    // Add/Subtract
686
    // --------------------------------------------------------------------
687

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

697
        return $time->add(DateInterval::createFromDateString("{$seconds} seconds"));
2✔
698
    }
699

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

709
        return $time->add(DateInterval::createFromDateString("{$minutes} minutes"));
2✔
710
    }
711

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

721
        return $time->add(DateInterval::createFromDateString("{$hours} hours"));
2✔
722
    }
723

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

733
        return $time->add(DateInterval::createFromDateString("{$days} days"));
2✔
734
    }
735

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

745
        return $time->add(DateInterval::createFromDateString("{$months} months"));
4✔
746
    }
747

748
    /**
749
     * Returns a new Time instance with $months calendar months added to the time.
750
     */
751
    public function addCalendarMonths(int $months): static
752
    {
753
        $time = clone $this;
4✔
754

755
        $year  = (int) $time->getYear();
4✔
756
        $month = (int) $time->getMonth();
4✔
757
        $day   = (int) $time->getDay();
4✔
758

759
        // Adjust total months since year 0
760
        $totalMonths = ($year * 12 + $month - 1) + $months;
4✔
761

762
        // Recalculate year and month
763
        $newYear  = intdiv($totalMonths, 12);
4✔
764
        $newMonth = $totalMonths % 12 + 1;
4✔
765

766
        // Get last day of new month
767
        $lastDayOfMonth = cal_days_in_month(CAL_GREGORIAN, $newMonth, $newYear);
4✔
768
        $correctedDay   = min($day, $lastDayOfMonth);
4✔
769

770
        return static::create($newYear, $newMonth, $correctedDay, (int) $this->getHour(), (int) $this->getMinute(), (int) $this->getSecond(), $this->getTimezone(), $this->locale);
4✔
771
    }
772

773
    /**
774
     * Returns a new Time instance with $months calendar months subtracted from the time
775
     */
776
    public function subCalendarMonths(int $months): static
777
    {
778
        return $this->addCalendarMonths(-$months);
2✔
779
    }
780

781
    /**
782
     * Returns a new Time instance with $years added to the time.
783
     *
784
     * @return static
785
     */
786
    public function addYears(int $years)
787
    {
788
        $time = clone $this;
2✔
789

790
        return $time->add(DateInterval::createFromDateString("{$years} years"));
2✔
791
    }
792

793
    /**
794
     * Returns a new Time instance with $seconds subtracted from the time.
795
     *
796
     * @return static
797
     */
798
    public function subSeconds(int $seconds)
799
    {
800
        $time = clone $this;
2✔
801

802
        return $time->sub(DateInterval::createFromDateString("{$seconds} seconds"));
2✔
803
    }
804

805
    /**
806
     * Returns a new Time instance with $minutes subtracted from the time.
807
     *
808
     * @return static
809
     */
810
    public function subMinutes(int $minutes)
811
    {
812
        $time = clone $this;
2✔
813

814
        return $time->sub(DateInterval::createFromDateString("{$minutes} minutes"));
2✔
815
    }
816

817
    /**
818
     * Returns a new Time instance with $hours subtracted from the time.
819
     *
820
     * @return static
821
     */
822
    public function subHours(int $hours)
823
    {
824
        $time = clone $this;
2✔
825

826
        return $time->sub(DateInterval::createFromDateString("{$hours} hours"));
2✔
827
    }
828

829
    /**
830
     * Returns a new Time instance with $days subtracted from the time.
831
     *
832
     * @return static
833
     */
834
    public function subDays(int $days)
835
    {
836
        $time = clone $this;
2✔
837

838
        return $time->sub(DateInterval::createFromDateString("{$days} days"));
2✔
839
    }
840

841
    /**
842
     * Returns a new Time instance with $months subtracted from the time.
843
     *
844
     * @return static
845
     */
846
    public function subMonths(int $months)
847
    {
848
        $time = clone $this;
2✔
849

850
        return $time->sub(DateInterval::createFromDateString("{$months} months"));
2✔
851
    }
852

853
    /**
854
     * Returns a new Time instance with $hours subtracted from the time.
855
     *
856
     * @return static
857
     */
858
    public function subYears(int $years)
859
    {
860
        $time = clone $this;
2✔
861

862
        return $time->sub(DateInterval::createFromDateString("{$years} years"));
2✔
863
    }
864

865
    // --------------------------------------------------------------------
866
    // Formatters
867
    // --------------------------------------------------------------------
868

869
    /**
870
     * Returns the localized value of the date in the format 'Y-m-d H:i:s'
871
     *
872
     * @return false|string
873
     *
874
     * @throws Exception
875
     */
876
    public function toDateTimeString()
877
    {
878
        return $this->toLocalizedString('yyyy-MM-dd HH:mm:ss');
93✔
879
    }
880

881
    /**
882
     * Returns a localized version of the date in Y-m-d format.
883
     *
884
     * @return string
885
     *
886
     * @throws Exception
887
     */
888
    public function toDateString()
889
    {
890
        return $this->toLocalizedString('yyyy-MM-dd');
2✔
891
    }
892

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

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

921
    /**
922
     * Returns the localized value of this instance in $format.
923
     *
924
     * @return false|string
925
     *
926
     * @throws Exception
927
     */
928
    public function toLocalizedString(?string $format = null)
929
    {
930
        $format ??= $this->toStringFormat;
125✔
931

932
        return IntlDateFormatter::formatObject($this->toDateTime(), $format, $this->locale);
125✔
933
    }
934

935
    // --------------------------------------------------------------------
936
    // Comparison
937
    // --------------------------------------------------------------------
938

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

953
        $ourTime = $this->toDateTime()
15✔
954
            ->setTimezone(new DateTimeZone('UTC'))
15✔
955
            ->format('Y-m-d H:i:s.u');
15✔
956

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

960
    /**
961
     * Ensures that the times are identical, taking timezone into account.
962
     *
963
     * @param DateTimeInterface|self|string $testTime
964
     *
965
     * @throws Exception
966
     */
967
    public function sameAs($testTime, ?string $timezone = null): bool
968
    {
969
        $testTime = $this->normalizeTime($testTime, $timezone)->format('Y-m-d H:i:s.u O');
20✔
970

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

973
        return $testTime === $ourTime;
20✔
974
    }
975

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

988
        $testTimestamp = $testTime->getTimestamp();
18✔
989
        $ourTimestamp  = $this->getTimestamp();
18✔
990

991
        if ($ourTimestamp === $testTimestamp) {
18✔
992
            return $this->format('u') < $testTime->format('u');
6✔
993
        }
994

995
        return $ourTimestamp < $testTimestamp;
13✔
996
    }
997

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

1010
        $testTimestamp = $testTime->getTimestamp();
20✔
1011
        $ourTimestamp  = $this->getTimestamp();
20✔
1012

1013
        if ($ourTimestamp === $testTimestamp) {
20✔
1014
            return $this->format('u') > $testTime->format('u');
6✔
1015
        }
1016

1017
        return $ourTimestamp > $testTimestamp;
17✔
1018
    }
1019

1020
    /**
1021
     * Determines if the current instance's time is between two others.
1022
     *
1023
     * If $start is after $end, the arguments are swapped.
1024
     *
1025
     * @param string|null $timezone Used only when $start or $end is a string.
1026
     *
1027
     * @throws Exception
1028
     */
1029
    public function between(DateTimeInterface|string $start, DateTimeInterface|string $end, bool $inclusive = true, ?string $timezone = null): bool
1030
    {
1031
        $start = $this->normalizeTime($start, $timezone);
9✔
1032
        $end   = $this->normalizeTime($end, $timezone);
9✔
1033

1034
        if ($start->isAfter($end)) {
9✔
1035
            [$start, $end] = [$end, $start];
2✔
1036
        }
1037

1038
        if ($inclusive) {
9✔
1039
            return ! $this->isBefore($start) && ! $this->isAfter($end);
7✔
1040
        }
1041

1042
        return $this->isAfter($start) && $this->isBefore($end);
2✔
1043
    }
1044

1045
    /**
1046
     * Returns the earlier of the current instance and the provided time.
1047
     *
1048
     * If null is provided, compares against now in the current timezone.
1049
     *
1050
     * @param string|null $timezone Used only when $time is a string or null.
1051
     *
1052
     * @throws Exception
1053
     */
1054
    public function min(DateTimeInterface|string|null $time = null, ?string $timezone = null): static
1055
    {
1056
        $time = $this->normalizeTime($time, $timezone);
6✔
1057

1058
        return $this->isAfter($time) ? $time : $this;
6✔
1059
    }
1060

1061
    /**
1062
     * Returns the later of the current instance and the provided time.
1063
     *
1064
     * If null is provided, compares against now in the current timezone.
1065
     *
1066
     * @param string|null $timezone Used only when $time is a string or null.
1067
     *
1068
     * @throws Exception
1069
     */
1070
    public function max(DateTimeInterface|string|null $time = null, ?string $timezone = null): static
1071
    {
1072
        $time = $this->normalizeTime($time, $timezone);
6✔
1073

1074
        return $this->isBefore($time) ? $time : $this;
6✔
1075
    }
1076

1077
    /**
1078
     * Determines if the current instance's time is in the past.
1079
     *
1080
     * @throws Exception
1081
     */
1082
    public function isPast(): bool
1083
    {
1084
        return $this->isBefore(static::now($this->timezone));
1✔
1085
    }
1086

1087
    /**
1088
     * Determines if the current instance's time is in the future.
1089
     *
1090
     * @throws Exception
1091
     */
1092
    public function isFuture(): bool
1093
    {
1094
        return $this->isAfter(static::now($this->timezone));
1✔
1095
    }
1096

1097
    // --------------------------------------------------------------------
1098
    // Differences
1099
    // --------------------------------------------------------------------
1100

1101
    /**
1102
     * Returns a text string that is easily readable that describes
1103
     * how long ago, or how long from now, a date is, like:
1104
     *
1105
     *  - 3 weeks ago
1106
     *  - in 4 days
1107
     *  - 6 hours ago
1108
     *
1109
     * @return string
1110
     *
1111
     * @throws Exception
1112
     */
1113
    public function humanize()
1114
    {
1115
        $now  = IntlCalendar::fromDateTime(self::now($this->timezone)->toDateTime());
42✔
1116
        $time = $this->getCalendar()->getTime();
42✔
1117

1118
        $years   = $now->fieldDifference($time, IntlCalendar::FIELD_YEAR);
42✔
1119
        $months  = $now->fieldDifference($time, IntlCalendar::FIELD_MONTH);
42✔
1120
        $days    = $now->fieldDifference($time, IntlCalendar::FIELD_DAY_OF_YEAR);
42✔
1121
        $hours   = $now->fieldDifference($time, IntlCalendar::FIELD_HOUR_OF_DAY);
42✔
1122
        $minutes = $now->fieldDifference($time, IntlCalendar::FIELD_MINUTE);
42✔
1123

1124
        $phrase = null;
42✔
1125

1126
        if ($years !== 0) {
42✔
1127
            $phrase = lang('Time.years', [abs($years)]);
6✔
1128
            $before = $years < 0;
6✔
1129
        } elseif ($months !== 0) {
36✔
1130
            $phrase = lang('Time.months', [abs($months)]);
6✔
1131
            $before = $months < 0;
6✔
1132
        } elseif ($days !== 0 && (abs($days) >= 7)) {
30✔
1133
            $weeks  = ceil($days / 7);
8✔
1134
            $phrase = lang('Time.weeks', [abs($weeks)]);
8✔
1135
            $before = $days < 0;
8✔
1136
        } elseif ($days !== 0) {
22✔
1137
            $before = $days < 0;
10✔
1138

1139
            // Yesterday/Tomorrow special cases
1140
            if (abs($days) === 1) {
10✔
1141
                return $before ? lang('Time.yesterday') : lang('Time.tomorrow');
4✔
1142
            }
1143

1144
            $phrase = lang('Time.days', [abs($days)]);
6✔
1145
        } elseif ($hours !== 0) {
12✔
1146
            $phrase = lang('Time.hours', [abs($hours)]);
4✔
1147
            $before = $hours < 0;
4✔
1148
        } elseif ($minutes !== 0) {
8✔
1149
            $phrase = lang('Time.minutes', [abs($minutes)]);
6✔
1150
            $before = $minutes < 0;
6✔
1151
        } else {
1152
            return lang('Time.now');
2✔
1153
        }
1154

1155
        return $before ? lang('Time.ago', [$phrase]) : lang('Time.inFuture', [$phrase]);
36✔
1156
    }
1157

1158
    /**
1159
     * @param DateTimeInterface|self|string $testTime
1160
     *
1161
     * @return TimeDifference
1162
     *
1163
     * @throws Exception
1164
     */
1165
    public function difference($testTime, ?string $timezone = null)
1166
    {
1167
        $testTime = $this->normalizeTime($testTime, $timezone)->toDateTime();
37✔
1168

1169
        if ($this->timezone->getOffset($this) !== $testTime->getTimezone()->getOffset($this)) {
37✔
UNCOV
1170
            $testTime = $this->getUTCObject($testTime);
×
UNCOV
1171
            $ourTime  = $this->getUTCObject($this);
×
1172
        } else {
1173
            $ourTime = $this->toDateTime();
37✔
1174
        }
1175

1176
        return new TimeDifference($ourTime, $testTime);
37✔
1177
    }
1178

1179
    // --------------------------------------------------------------------
1180
    // Utilities
1181
    // --------------------------------------------------------------------
1182

1183
    /**
1184
     * Returns a Time instance with the timezone converted to UTC.
1185
     *
1186
     * @param DateTimeInterface|self|string $time
1187
     *
1188
     * @return DateTime|static
1189
     *
1190
     * @throws Exception
1191
     */
1192
    public function getUTCObject($time, ?string $timezone = null)
1193
    {
1194
        if ($time instanceof static) {
47✔
1195
            $time = $time->toDateTime();
38✔
1196
        } elseif (is_string($time)) {
9✔
1197
            $timezone = in_array($timezone, [null, '', '0'], true) ? $this->timezone : $timezone;
4✔
1198
            $timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone);
4✔
1199
            $time     = new DateTime($time, $timezone);
4✔
1200
        }
1201

1202
        if ($time instanceof DateTime || $time instanceof DateTimeImmutable) {
47✔
1203
            return $time->setTimezone(new DateTimeZone('UTC'));
47✔
1204
        }
1205

UNCOV
1206
        return $time;
×
1207
    }
1208

1209
    /**
1210
     * Returns a Time instance normalized to the current locale.
1211
     *
1212
     * If $time is a string, it will be parsed using the provided timezone,
1213
     * or the current instance's timezone when omitted. If null is provided,
1214
     * the current time is used in the same timezone.
1215
     *
1216
     * @throws Exception
1217
     */
1218
    private function normalizeTime(DateTimeInterface|string|null $time, ?string $timezone = null): static
1219
    {
1220
        if ($time instanceof DateTimeInterface) {
68✔
1221
            return static::createFromInstance($time, $this->locale);
34✔
1222
        }
1223

1224
        $timezone = in_array($timezone, [null, '', '0'], true) ? $this->timezone : $timezone;
40✔
1225

1226
        if ($time === null) {
40✔
1227
            return static::now($timezone, $this->locale);
4✔
1228
        }
1229

1230
        return new static($time, $timezone, $this->locale);
36✔
1231
    }
1232

1233
    /**
1234
     * Returns the IntlCalendar object used for this object,
1235
     * taking into account the locale, date, etc.
1236
     *
1237
     * Primarily used internally to provide the difference and comparison functions,
1238
     * but available for public consumption if they need it.
1239
     *
1240
     * @return IntlCalendar
1241
     *
1242
     * @throws Exception
1243
     */
1244
    public function getCalendar()
1245
    {
1246
        return IntlCalendar::fromDateTime($this->toDateTime());
42✔
1247
    }
1248

1249
    /**
1250
     * Check a time string to see if it includes a relative date (like 'next Tuesday').
1251
     */
1252
    protected static function hasRelativeKeywords(string $time): bool
1253
    {
1254
        // skip common format with a '-' in it
1255
        if (preg_match('/\d{4}-\d{1,2}-\d{1,2}/', $time) !== 1) {
415✔
1256
            return preg_match(static::$relativePattern, $time) > 0;
254✔
1257
        }
1258

1259
        return false;
264✔
1260
    }
1261

1262
    /**
1263
     * Outputs a short format version of the datetime.
1264
     * The output is NOT localized intentionally.
1265
     */
1266
    public function __toString(): string
1267
    {
1268
        return $this->format('Y-m-d H:i:s');
18✔
1269
    }
1270

1271
    /**
1272
     * Allow for property-type access to any getX method...
1273
     *
1274
     * Note that we cannot use this for any of our setX methods,
1275
     * as they return new Time objects, but the __set ignores
1276
     * return values.
1277
     * See http://php.net/manual/en/language.oop5.overloading.php
1278
     *
1279
     * @param string $name
1280
     *
1281
     * @return array|bool|DateTimeInterface|DateTimeZone|int|IntlCalendar|self|string|null
1282
     */
1283
    public function __get($name)
1284
    {
1285
        $method = 'get' . ucfirst($name);
38✔
1286

1287
        if (method_exists($this, $method)) {
38✔
1288
            return $this->{$method}();
36✔
1289
        }
1290

1291
        return null;
2✔
1292
    }
1293

1294
    /**
1295
     * Allow for property-type checking to any getX method...
1296
     *
1297
     * @param string $name
1298
     */
1299
    public function __isset($name): bool
1300
    {
1301
        $method = 'get' . ucfirst($name);
4✔
1302

1303
        return method_exists($this, $method);
4✔
1304
    }
1305

1306
    /**
1307
     * This is called when we unserialize the Time object.
1308
     *
1309
     * @param array{date: string, timezone: string, timezone_type: int} $data
1310
     */
1311
    public function __unserialize(array $data): void
1312
    {
1313
        parent::__construct($data['date'], new DateTimeZone($data['timezone']));
2✔
1314
    }
1315
}
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

© 2026 Coveralls, Inc