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

azjezz / psl / 22587626309

02 Mar 2026 05:27PM UTC coverage: 97.853% (-0.06%) from 97.914%
22587626309

Pull #595

github

web-flow
Merge ec27e9146 into bc317895e
Pull Request #595: feat(datetime): add `Period`, `Interval`, `TemporalAmountInterface`, and fix bugs

315 of 327 new or added lines in 11 files covered. (96.33%)

2 existing lines in 1 file now uncovered.

8704 of 8895 relevant lines covered (97.85%)

40.6 hits per line

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

96.13
/src/Psl/DateTime/DateTimeConvenienceMethodsTrait.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Psl\DateTime;
6

7
use Psl\Locale\Locale;
8
use Psl\Math;
9

10
/**
11
 * @require-implements DateTimeInterface
12
 *
13
 * @psalm-immutable
14
 */
15
trait DateTimeConvenienceMethodsTrait
16
{
17
    use TemporalConvenienceMethodsTrait {
18
        toRfc3339 as private toRfc3339Impl;
19
    }
20

21
    /**
22
     * Checks if this {@see DateTimeInterface} instance is equal to the given {@see DateTimeInterface} instance including the timezone.
23
     *
24
     * @param DateTimeInterface $other The {@see DateTimeInterface} instance to compare with.
25
     *
26
     * @return bool True if equal including timezone, false otherwise.
27
     *
28
     * @psalm-mutation-free
29
     */
30
    public function equalsIncludingTimezone(DateTimeInterface $other): bool
31
    {
32
        return $this->equals($other) && $this->getTimezone() === $other->getTimezone();
1✔
33
    }
34

35
    /**
36
     * Obtains the timezone offset as a {@see Duration} object.
37
     *
38
     * This method effectively returns the offset from UTC for the timezone of this instance at the specific date and time it represents.
39
     *
40
     * It is equivalent to executing `$dt->getTimezone()->getOffset($dt)`, which calculates the offset for the timezone of this instance.
41
     *
42
     * @return Duration The offset from UTC as a Duration.
43
     *
44
     * @psalm-mutation-free
45
     */
46
    public function getTimezoneOffset(): Duration
47
    {
48
        return $this->getTimezone()->getOffset($this);
1✔
49
    }
50

51
    /**
52
     * Determines whether this instance is currently in daylight saving time.
53
     *
54
     * This method checks if the date and time represented by this instance fall within the daylight saving time period of its timezone.
55
     *
56
     * It is equivalent to `!$dt->getTimezone()->getDaylightSavingTimeOffset($dt)->isZero()`, indicating whether there is a non-zero DST offset.
57
     *
58
     * @return bool True if in daylight saving time, false otherwise.
59
     *
60
     * @psalm-mutation-free
61
     */
62
    public function isDaylightSavingTime(): bool
63
    {
64
        return !$this->getTimezone()->getDaylightSavingTimeOffset($this)->isZero();
1✔
65
    }
66

67
    /**
68
     * Converts the {@see DateTimeInterface} instance to the specified timezone.
69
     *
70
     * @param Timezone $timezone The timezone to convert to.
71
     *
72
     * @psalm-mutation-free
73
     */
74
    public function convertToTimezone(Timezone $timezone): static
75
    {
76
        return static::fromTimestamp($this->getTimestamp(), $timezone);
1✔
77
    }
78

79
    /**
80
     * Returns a new instance with the specified year.
81
     *
82
     * @throws Exception\UnexpectedValueException If the provided year do not align with calendar expectations.
83
     *
84
     * @psalm-mutation-free
85
     */
86
    public function withYear(int $year): static
87
    {
88
        return $this->withDate($year, $this->getMonth(), $this->getDay());
1✔
89
    }
90

91
    /**
92
     * Returns a new instance with the specified month.
93
     *
94
     * @param Month|int<1, 12> $month
95
     *
96
     * @throws Exception\UnexpectedValueException If the provided month do not align with calendar expectations.
97
     *
98
     * @psalm-mutation-free
99
     */
100
    public function withMonth(Month|int $month): static
101
    {
102
        return $this->withDate($this->getYear(), $month, $this->getDay());
1✔
103
    }
104

105
    /**
106
     * Returns a new instance with the specified day.
107
     *
108
     * @param int<1, 31> $day
109
     *
110
     * @throws Exception\UnexpectedValueException If the provided day do not align with calendar expectations.
111
     *
112
     * @psalm-mutation-free
113
     */
114
    public function withDay(int $day): static
115
    {
116
        return $this->withDate($this->getYear(), $this->getMonth(), $day);
1✔
117
    }
118

119
    /**
120
     * Returns a new instance with the specified hours.
121
     *
122
     * @param int<0, 23> $hours
123
     *
124
     * @throws Exception\UnexpectedValueException If the provided hours do not align with calendar expectations.
125
     *
126
     * @psalm-mutation-free
127
     */
128
    public function withHours(int $hours): static
129
    {
130
        return $this->withTime($hours, $this->getMinutes(), $this->getSeconds(), $this->getNanoseconds());
1✔
131
    }
132

133
    /**
134
     * Returns a new instance with the specified minutes.
135
     *
136
     * @param int<0, 59> $minutes
137
     *
138
     * @throws Exception\UnexpectedValueException If the provided minutes do not align with calendar expectations.
139
     *
140
     * @psalm-mutation-free
141
     */
142
    public function withMinutes(int $minutes): static
143
    {
144
        return $this->withTime($this->getHours(), $minutes, $this->getSeconds(), $this->getNanoseconds());
1✔
145
    }
146

147
    /**
148
     * Returns a new instance with the specified seconds.
149
     *
150
     * @param int<0, 59> $seconds
151
     *
152
     * @throws Exception\UnexpectedValueException If the provided seconds do not align with calendar expectations.
153
     *
154
     * @psalm-mutation-free
155
     */
156
    public function withSeconds(int $seconds): static
157
    {
158
        return $this->withTime($this->getHours(), $this->getMinutes(), $seconds, $this->getNanoseconds());
1✔
159
    }
160

161
    /**
162
     * Returns a new instance with the specified nanoseconds.
163
     *
164
     * @param int<0, 999999999> $nanoseconds
165
     *
166
     * @throws Exception\UnexpectedValueException If the provided nanoseconds do not align with calendar expectations.
167
     *
168
     * @psalm-mutation-free
169
     */
170
    public function withNanoseconds(int $nanoseconds): static
171
    {
172
        return $this->withTime($this->getHours(), $this->getMinutes(), $this->getSeconds(), $nanoseconds);
1✔
173
    }
174

175
    /**
176
     * Returns a new instance representing the start of the same day (00:00:00.000000000).
177
     *
178
     * @psalm-mutation-free
179
     */
180
    public function atStartOfDay(): static
181
    {
182
        return $this->withTime(0, 0, 0, 0);
5✔
183
    }
184

185
    /**
186
     * Returns a new instance representing the end of the same day (23:59:59.999999999).
187
     *
188
     * @psalm-mutation-free
189
     */
190
    public function atEndOfDay(): static
191
    {
192
        return $this->withTime(23, 59, 59, 999_999_999);
3✔
193
    }
194

195
    /**
196
     * Returns a new instance representing the start of the current month (1st day at 00:00:00.000000000).
197
     *
198
     * @psalm-mutation-free
199
     */
200
    public function atStartOfMonth(): static
201
    {
202
        return $this->withDate($this->getYear(), $this->getMonth(), 1)->withTime(0, 0, 0, 0);
1✔
203
    }
204

205
    /**
206
     * Returns a new instance representing the end of the current month (last day at 23:59:59.999999999).
207
     *
208
     * @psalm-mutation-free
209
     */
210
    public function atEndOfMonth(): static
211
    {
212
        $monthEnum = Month::from($this->getMonth());
3✔
213
        $lastDay = $monthEnum->getDaysForYear($this->getYear());
3✔
214

215
        return $this->withDate($this->getYear(), $this->getMonth(), $lastDay)->withTime(23, 59, 59, 999_999_999);
3✔
216
    }
217

218
    /**
219
     * Returns a new instance representing the start of the current year (January 1st at 00:00:00.000000000).
220
     *
221
     * @psalm-mutation-free
222
     */
223
    public function atStartOfYear(): static
224
    {
225
        return $this->withDate($this->getYear(), 1, 1)->withTime(0, 0, 0, 0);
1✔
226
    }
227

228
    /**
229
     * Returns a new instance representing the end of the current year (December 31st at 23:59:59.999999999).
230
     *
231
     * @psalm-mutation-free
232
     */
233
    public function atEndOfYear(): static
234
    {
235
        return $this->withDate($this->getYear(), 12, 31)->withTime(23, 59, 59, 999_999_999);
1✔
236
    }
237

238
    /**
239
     * Returns a new instance representing the start of the current ISO week (Monday at 00:00:00.000000000).
240
     *
241
     * @throws Exception\UnderflowException If the operation results in an arithmetic underflow.
242
     * @throws Exception\OverflowException If the operation results in an arithmetic overflow.
243
     *
244
     * @psalm-mutation-free
245
     */
246
    public function atStartOfWeek(): static
247
    {
248
        $daysBack = $this->getWeekday()->value - Weekday::Monday->value;
3✔
249

250
        return $this->minusDays($daysBack)->atStartOfDay();
3✔
251
    }
252

253
    /**
254
     * Returns a new instance representing the end of the current ISO week (Sunday at 23:59:59.999999999).
255
     *
256
     * @throws Exception\UnderflowException If the operation results in an arithmetic underflow.
257
     * @throws Exception\OverflowException If the operation results in an arithmetic overflow.
258
     *
259
     * @psalm-mutation-free
260
     */
261
    public function atEndOfWeek(): static
262
    {
263
        $daysForward = Weekday::Sunday->value - $this->getWeekday()->value;
2✔
264

265
        return $this->plusDays($daysForward)->atEndOfDay();
2✔
266
    }
267

268
    /**
269
     * Returns the date (year, month, day).
270
     *
271
     * @return array{int, int<1, 12>, int<1, 31>} The date.
272
     *
273
     * @psalm-mutation-free
274
     */
275
    public function getDate(): array
276
    {
277
        return [$this->getYear(), $this->getMonth(), $this->getDay()];
4✔
278
    }
279

280
    /**
281
     * Returns the time (hours, minutes, seconds, nanoseconds).
282
     *
283
     * @return array{
284
     *     int<0, 23>,
285
     *     int<0, 59>,
286
     *     int<0, 59>,
287
     *     int<0, 999999999>,
288
     * }
289
     *
290
     * @psalm-mutation-free
291
     */
292
    public function getTime(): array
293
    {
294
        return [
4✔
295
            $this->getHours(),
4✔
296
            $this->getMinutes(),
4✔
297
            $this->getSeconds(),
4✔
298
            $this->getNanoseconds(),
4✔
299
        ];
4✔
300
    }
301

302
    /**
303
     * Returns the {@see DateTimeInterface} parts (year, month, day, hours, minutes, seconds, nanoseconds).
304
     *
305
     * @return array{
306
     *     int,
307
     *     int<1, 12>,
308
     *     int<1, 31>,
309
     *     int<0, 23>,
310
     *     int<0, 59>,
311
     *     int<0, 59>,
312
     *     int<0, 999999999>,
313
     * }
314
     *
315
     * @psalm-mutation-free
316
     */
317
    public function getParts(): array
318
    {
319
        return [
1✔
320
            $this->getYear(),
1✔
321
            $this->getMonth(),
1✔
322
            $this->getDay(),
1✔
323
            $this->getHours(),
1✔
324
            $this->getMinutes(),
1✔
325
            $this->getSeconds(),
1✔
326
            $this->getNanoseconds(),
1✔
327
        ];
1✔
328
    }
329

330
    /**
331
     * Retrieves the era of the date represented by this DateTime instance.
332
     *
333
     * This method returns an instance of the `Era` enum, which indicates whether the date
334
     * falls in the Anno Domini (AD) or Before Christ (BC) era. The era is determined based on the year
335
     * of the date this object represents, with years designated as BC being negative
336
     * and years in AD being positive.
337
     *
338
     * @psalm-mutation-free
339
     */
340
    public function getEra(): Era
341
    {
342
        return Era::fromYear($this->getYear());
1✔
343
    }
344

345
    /**
346
     * Returns the century number for the year stored in this object.
347
     *
348
     * @psalm-mutation-free
349
     */
350
    public function getCentury(): int
351
    {
352
        return (int) ceil($this->getYear() / 100);
1✔
353
    }
354

355
    /**
356
     * Returns the short format of the year (last 2 digits).
357
     *
358
     * @return int<-99, 99> The short format of the year.
359
     *
360
     * @psalm-mutation-free
361
     */
362
    public function getYearShort(): int
363
    {
364
        /** @var int<-99, 99> */
365
        return (int) $this->format(pattern: 'yy', locale: Locale::EnglishUnitedKingdom);
1✔
366
    }
367

368
    /**
369
     * Returns the month as an instance of the {@see Month} enum.
370
     *
371
     * This method converts the numeric representation of the month into its corresponding
372
     * case in the {@see Month} enum, providing a type-safe way to work with months.
373
     *
374
     * @return Month The month as an enum case.
375
     *
376
     * @psalm-mutation-free
377
     */
378
    public function getMonthEnum(): Month
379
    {
380
        return Month::from($this->getMonth());
2✔
381
    }
382

383
    /**
384
     * Returns the hours using the 12-hour format (1 to 12) along with the meridiem indicator.
385
     *
386
     * @return array{int<1, 12>, Meridiem} The hours and meridiem indicator.
387
     *
388
     * @psalm-mutation-free
389
     */
390
    public function getTwelveHours(): array
391
    {
392
        $hours = $this->getHours();
8✔
393
        $twelve_hours = $hours % 12;
8✔
394
        if (0 === $twelve_hours) {
8✔
395
            $twelve_hours = 12;
2✔
396
        }
397

398
        return [$twelve_hours, $hours < 12 ? Meridiem::AnteMeridiem : Meridiem::PostMeridiem];
8✔
399
    }
400

401
    /**
402
     * Retrieves the ISO-8601 year and week number corresponding to the date.
403
     *
404
     * This method returns an array consisting of two integers: the first represents the year, and the second
405
     * represents the week number according to ISO-8601 standards, which ranges from 1 to 53. The week numbering
406
     * follows the ISO-8601 specification, where a week starts on a Monday and the first week of the year is the
407
     * one that contains at least four days of the new year.
408
     *
409
     * Due to the ISO-8601 week numbering rules, the returned year might not always match the Gregorian year
410
     * obtained from `$this->getYear()`. Specifically:
411
     *
412
     *  - The first few days of January might belong to the last week of the previous year if they fall before
413
     *      the first Thursday of January.
414
     *
415
     *  - Conversely, the last days of December might be part of the first week of the following year if they
416
     *      extend beyond the last Thursday of December.
417
     *
418
     * Examples:
419
     *  - For the date 2020-01-01, it returns [2020, 1], indicating the first week of 2020.
420
     *  - For the date 2021-01-01, it returns [2020, 53], showing that this day is part of the last week of 2020
421
     *      according to ISO-8601.
422
     *
423
     * @return array{int, int<1, 53>}
424
     *
425
     * @psalm-mutation-free
426
     */
427
    public function getISOWeekNumber(): array
428
    {
429
        /** @var int<1, 53> $week */
430
        $week = (int) $this->format(pattern: 'w', locale: Locale::EnglishUnitedKingdom);
1✔
431

432
        $year = (int) $this->format(pattern: 'Y', locale: Locale::EnglishUnitedKingdom);
1✔
433

434
        return [$year, $week];
1✔
435
    }
436

437
    /**
438
     * Gets the weekday of the date.
439
     *
440
     * @return Weekday The weekday.
441
     *
442
     * @psalm-mutation-free
443
     */
444
    public function getWeekday(): Weekday
445
    {
446
        return Weekday::from((int) $this->format(pattern: 'e', locale: Locale::EnglishUnitedKingdom));
7✔
447
    }
448

449
    /**
450
     * Returns the day of the year (1–366).
451
     *
452
     * @return int<1, 366>
453
     *
454
     * @psalm-mutation-free
455
     */
456
    public function getDayOfYear(): int
457
    {
458
        /** @var int<1, 366> */
459
        return (int) $this->format(pattern: 'D', locale: Locale::EnglishUnitedKingdom);
1✔
460
    }
461

462
    /**
463
     * Checks if the year is a leap year.
464
     *
465
     * @psalm-mutation-free
466
     */
467
    public function isLeapYear(): bool
468
    {
469
        return namespace\is_leap_year($this->getYear());
1✔
470
    }
471

472
    /**
473
     * Adds the specified years to this date-time object, returning a new instance with the added years.
474
     *
475
     * @throws Exception\UnexpectedValueException If adding the years results in an arithmetic issue.
476
     *
477
     * @psalm-mutation-free
478
     */
479
    public function plusYears(int $years): static
480
    {
481
        return $this->plusMonths($years * MONTHS_PER_YEAR);
2✔
482
    }
483

484
    /**
485
     * Subtracts the specified years from this date-time object, returning a new instance with the subtracted years.
486
     *
487
     * @throws Exception\UnexpectedValueException If subtracting the years results in an arithmetic issue.
488
     *
489
     * @psalm-mutation-free
490
     */
491
    public function minusYears(int $years): static
492
    {
493
        return $this->minusMonths($years * MONTHS_PER_YEAR);
2✔
494
    }
495

496
    /**
497
     * Adds the specified months to this date-time object, returning a new instance with the added months.
498
     *
499
     * @throws Exception\UnexpectedValueException If adding the months results in an arithmetic issue.
500
     *
501
     * @psalm-mutation-free
502
     */
503
    public function plusMonths(int $months): static
504
    {
505
        if (0 === $months) {
10✔
506
            return $this;
2✔
507
        }
508

509
        if ($months < 1) {
9✔
510
            return $this->minusMonths(-$months);
3✔
511
        }
512

513
        $plus_years = Math\div($months, MONTHS_PER_YEAR);
7✔
514
        $months_left = $months - ($plus_years * MONTHS_PER_YEAR);
7✔
515
        $target_month = $this->getMonth() + $months_left;
7✔
516

517
        if ($target_month > MONTHS_PER_YEAR) {
7✔
518
            $plus_years++;
3✔
519
            $target_month -= MONTHS_PER_YEAR;
3✔
520
        }
521

522
        $target_month_enum = Month::from($target_month);
7✔
523

524
        return $this->withDate(
7✔
525
            $target_year = $this->getYear() + $plus_years,
7✔
526
            $target_month_enum->value,
7✔
527
            Math\minva($this->getDay(), $target_month_enum->getDaysForYear($target_year)),
7✔
528
        );
7✔
529
    }
530

531
    /**
532
     * Subtracts the specified months from this date-time object, returning a new instance with the subtracted months.
533
     *
534
     * @throws Exception\UnexpectedValueException If subtracting the months results in an arithmetic issue.
535
     *
536
     * @psalm-mutation-free
537
     */
538
    public function minusMonths(int $months): static
539
    {
540
        if (0 === $months) {
10✔
541
            return $this;
2✔
542
        }
543

544
        if ($months < 1) {
9✔
545
            return $this->plusMonths(-$months);
3✔
546
        }
547

548
        $minus_years = Math\div($months, MONTHS_PER_YEAR);
7✔
549
        $months_left = $months - ($minus_years * MONTHS_PER_YEAR);
7✔
550
        $target_month = $this->getMonth() - $months_left;
7✔
551

552
        if ($target_month <= 0) {
7✔
553
            $minus_years++;
4✔
554
            $target_month = MONTHS_PER_YEAR - Math\abs($target_month);
4✔
555
        }
556

557
        $target_month_enum = Month::from($target_month);
7✔
558

559
        return $this->withDate(
7✔
560
            $target_year = $this->getYear() - $minus_years,
7✔
561
            $target_month_enum->value,
7✔
562
            Math\minva($this->getDay(), $target_month_enum->getDaysForYear($target_year)),
7✔
563
        );
7✔
564
    }
565

566
    /**
567
     * Adds the specified weeks to this date-time object, returning a new instance with the added weeks.
568
     *
569
     * @throws Exception\UnderflowException If adding the weeks results in an arithmetic underflow.
570
     * @throws Exception\OverflowException If adding the weeks results in an arithmetic overflow.
571
     *
572
     * @psalm-mutation-free
573
     */
574
    public function plusWeeks(int $weeks): static
575
    {
576
        return $this->plusDays($weeks * DAYS_PER_WEEK);
1✔
577
    }
578

579
    /**
580
     * Subtracts the specified weeks from this date-time object, returning a new instance with the subtracted weeks.
581
     *
582
     * @throws Exception\UnderflowException If subtracting the weeks results in an arithmetic underflow.
583
     * @throws Exception\OverflowException If subtracting the weeks results in an arithmetic overflow.
584
     *
585
     * @psalm-mutation-free
586
     */
587
    public function minusWeeks(int $weeks): static
588
    {
589
        return $this->minusDays($weeks * DAYS_PER_WEEK);
1✔
590
    }
591

592
    /**
593
     * Adds the specified days to this date-time object, returning a new instance with the added days.
594
     *
595
     * @throws Exception\UnderflowException If adding the days results in an arithmetic underflow.
596
     * @throws Exception\OverflowException If adding the days results in an arithmetic overflow.
597
     *
598
     * @psalm-mutation-free
599
     */
600
    public function plusDays(int $days): static
601
    {
602
        return static::fromTimestamp($this->getTimestamp()->plusSeconds($days * SECONDS_PER_DAY), $this->getTimezone());
4✔
603
    }
604

605
    /**
606
     * Subtracts the specified days from this date-time object, returning a new instance with the subtracted days.
607
     *
608
     * @throws Exception\UnderflowException If subtracting the days results in an arithmetic underflow.
609
     * @throws Exception\OverflowException If subtracting the days results in an arithmetic overflow.
610
     *
611
     * @psalm-mutation-free
612
     */
613
    public function minusDays(int $days): static
614
    {
615
        return static::fromTimestamp(
5✔
616
            $this->getTimestamp()->minusSeconds($days * SECONDS_PER_DAY),
5✔
617
            $this->getTimezone(),
5✔
618
        );
5✔
619
    }
620

621
    /**
622
     * Adds the specified temporal amount to this date-time object, returning a new instance.
623
     *
624
     * Supports both {@see Duration} (exact time) and {@see Period} (calendar-aware arithmetic,
625
     * e.g. adding 1 month to January 31 yields February 28/29).
626
     *
627
     * @throws Exception\UnderflowException If the operation results in an arithmetic underflow.
628
     * @throws Exception\OverflowException If the operation results in an arithmetic overflow.
629
     *
630
     * @psalm-mutation-free
631
     */
632
    public function plus(TemporalAmountInterface $amount): static
633
    {
634
        if ($amount instanceof Duration) {
5✔
635
            return static::fromTimestamp($this->getTimestamp()->plus($amount), $this->getTimezone());
2✔
636
        }
637

638
        return $this->applyCalendarOffset($amount, 1);
3✔
639
    }
640

641
    /**
642
     * Subtracts the specified temporal amount from this date-time object, returning a new instance.
643
     *
644
     * Supports both {@see Duration} (exact time) and {@see Period} (calendar-aware arithmetic).
645
     *
646
     * @throws Exception\UnderflowException If the operation results in an arithmetic underflow.
647
     * @throws Exception\OverflowException If the operation results in an arithmetic overflow.
648
     *
649
     * @psalm-mutation-free
650
     */
651
    public function minus(TemporalAmountInterface $amount): static
652
    {
653
        if ($amount instanceof Duration) {
3✔
654
            return static::fromTimestamp($this->getTimestamp()->minus($amount), $this->getTimezone());
1✔
655
        }
656

657
        return $this->applyCalendarOffset($amount, -1);
2✔
658
    }
659

660
    /**
661
     * Applies a calendar-aware offset (Period) in a single pass.
662
     *
663
     * @param 1|-1 $sign 1 for addition, -1 for subtraction.
664
     *
665
     * @psalm-mutation-free
666
     */
667
    private function applyCalendarOffset(Period $period, int $sign): static
668
    {
669
        $monthsToAdd = $sign * (($period->getYears() * MONTHS_PER_YEAR) + $period->getMonths());
5✔
670
        $extraSeconds = $sign * ($period->getDays() * SECONDS_PER_DAY);
5✔
671

672
        $hasMonths = 0 !== $monthsToAdd;
5✔
673
        $hasOffset = 0 !== $extraSeconds;
5✔
674

675
        if (!$hasMonths && !$hasOffset) {
5✔
NEW
676
            return $this;
×
677
        }
678

679
        $year = $this->getYear();
5✔
680
        $month = $this->getMonth();
5✔
681
        $day = $this->getDay();
5✔
682
        if ($hasMonths) {
5✔
683
            $totalMonths = ($year * MONTHS_PER_YEAR) + $month - 1 + $monthsToAdd;
5✔
684
            $year = Math\div($totalMonths, MONTHS_PER_YEAR);
5✔
685
            $month = $totalMonths % MONTHS_PER_YEAR;
5✔
686
            if ($month < 0) {
5✔
NEW
687
                $year--;
×
NEW
688
                $month += MONTHS_PER_YEAR;
×
689
            }
690

691
            $month += 1;
5✔
692
            $day = Math\minva($day, Month::from($month)->getDaysForYear($year));
5✔
693
        }
694

695
        if ($hasMonths) {
5✔
696
            $calendar = Internal\create_intl_calendar_from_date_time(
5✔
697
                $this->getTimezone(),
5✔
698
                $year,
5✔
699
                $month,
5✔
700
                $day,
5✔
701
                $this->getHours(),
5✔
702
                $this->getMinutes(),
5✔
703
                $this->getSeconds(),
5✔
704
            );
5✔
705
            $baseSeconds = (int) ($calendar->getTime() / MILLISECONDS_PER_SECOND);
5✔
706
            $baseNanoseconds = $this->getNanoseconds();
5✔
707
        } else {
NEW
708
            $ts = $this->getTimestamp();
×
NEW
709
            $baseSeconds = $ts->getSeconds();
×
NEW
710
            $baseNanoseconds = $ts->getNanoseconds();
×
711
        }
712

713
        return static::fromTimestamp(
5✔
714
            Timestamp::fromParts($baseSeconds + $extraSeconds, $baseNanoseconds),
5✔
715
            $this->getTimezone(),
5✔
716
        );
5✔
717
    }
718

719
    /**
720
     * Formats this {@see DateTimeInterface} instance based on a specific pattern, with optional customization for timezone and locale.
721
     *
722
     * This method allows for detailed customization of the output string by specifying a format pattern. If no pattern is provided,
723
     * a default, implementation-specific pattern will be used. Additionally, the method supports specifying a timezone and locale
724
     * for further customization of the formatted output. If these are not provided, system defaults will be used.
725
     *
726
     * Example usage:
727
     *
728
     * ```php
729
     * $formatted = $temporal->format('yyyy-MM-dd HH:mm:ss', $timezone, $locale);
730
     * ```
731
     *
732
     * @param null|FormatPattern|string $pattern Optional custom format pattern for the date and time. If null, uses a default pattern.
733
     * @param null|Timezone $timezone Optional timezone for formatting. If null, uses the current timezone.
734
     * @param null|Locale $locale Optional locale for formatting. If null, uses the system's default locale.
735
     *
736
     * @return string The formatted date and time string, according to the specified pattern, timezone, and locale.
737
     *
738
     * @see https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax
739
     * @see Locale::default()
740
     *
741
     * @psalm-mutation-free
742
     */
743
    public function format(
744
        null|FormatPattern|string $pattern = null,
745
        null|Timezone $timezone = null,
746
        null|Locale $locale = null,
747
    ): string {
748
        $timestamp = $this->getTimestamp();
14✔
749

750
        return Internal\create_intl_date_formatter(
14✔
751
            null,
14✔
752
            null,
14✔
753
            $pattern,
14✔
754
            $timezone ?? $this->getTimezone(),
14✔
755
            $locale,
14✔
756
        )->format($timestamp->getSeconds() + ($timestamp->getNanoseconds() / NANOSECONDS_PER_SECOND));
14✔
757
    }
758

759
    /**
760
     * Formats this {@see DateTimeInterface} instance to a string based on the RFC 3339 format, with additional
761
     * options for second fractions and timezone representation.
762
     *
763
     * The RFC 3339 format is widely adopted in web and network protocols for its unambiguous representation of date, time,
764
     * and timezone information. This method not only ensures universal readability but also the precise specification
765
     * of time across various systems, being compliant with both RFC 3339 and ISO 8601 standards.
766
     *
767
     * Example usage:
768
     *
769
     * ```php
770
     * // Default formatting
771
     * $rfc_formatted_string = $datetime->toRfc3339();
772
     * // Customized formatting with milliseconds and 'Z' for UTC
773
     * $rfc_formatted_string_with_milliseconds_and_z = $datetime->toRfc3339(SecondsStyle::Milliseconds, true);
774
     * ```
775
     *
776
     * @param null|SecondsStyle $seconds_style Optional parameter to specify the seconds formatting style. Automatically
777
     *                                         selected based on precision if null.
778
     * @param bool $use_z Determines the representation of UTC timezone. True to use 'Z', false to use the standard offset format.
779
     *
780
     * @return string The formatted string of the {@see DateTimeInterface} instance, adhering to the RFC 3339 and compatible with ISO 8601 formats.
781
     *
782
     * @see https://datatracker.ietf.org/doc/html/rfc3339
783
     *
784
     * @psalm-mutation-free
785
     */
786
    public function toRfc3339(null|SecondsStyle $seconds_style = null, bool $use_z = false): string
787
    {
788
        return Internal\format_rfc3339($this->getTimestamp(), $seconds_style, $use_z, $this->getTimezone());
1✔
789
    }
790

791
    /**
792
     * Provides a string representation of this {@see TemporalInterface} instance, formatted according to specified styles for date and time,
793
     * and optionally adjusted for a specific timezone and locale.
794
     *
795
     * This method offers a higher-level abstraction for formatting, allowing users to specify styles for date and time separately
796
     * rather than a custom pattern. If no styles are provided, default styles will be used.
797
     *
798
     * Additionally, the timezone and locale can be specified for locale-sensitive formatting.
799
     *
800
     * Example usage:
801
     *
802
     * ```php
803
     * $string_representation = $temporal->toString(FormatDateStyle::Long, FormatTimeStyle::Short, $timezone, $locale);
804
     * ```
805
     *
806
     * @param null|DateStyle $date_style Optional style for the date portion of the output. If null, a default style is used.
807
     * @param null|TimeStyle $time_style Optional style for the time portion of the output. If null, a default style is used.
808
     * @param null|Timezone $timezone Optional timezone for formatting. If null, uses the current timezone.
809
     * @param null|Locale $locale Optional locale for formatting. If null, uses the system's default locale.
810
     *
811
     * @return string The string representation of the date and time, formatted according to the specified styles, timezone, and locale.
812
     *
813
     * @see DateStyle::default()
814
     * @see TimeStyle::default()
815
     * @see Locale::default()
816
     *
817
     * @psalm-mutation-free
818
     */
819
    public function toString(
820
        null|DateStyle $date_style = null,
821
        null|TimeStyle $time_style = null,
822
        null|Timezone $timezone = null,
823
        null|Locale $locale = null,
824
    ): string {
825
        $timestamp = $this->getTimestamp();
3✔
826

827
        return Internal\create_intl_date_formatter(
3✔
828
            $date_style,
3✔
829
            $time_style,
3✔
830
            null,
3✔
831
            $timezone ?? $this->getTimezone(),
3✔
832
            $locale,
3✔
833
        )->format($timestamp->getSeconds());
3✔
834
    }
835
}
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