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

azjezz / psl / 22587556011

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

push

github

azjezz
feat(datetime): add `Period`, `Interval`, `TemporalAmountInterface`, and fix bugs

Signed-off-by: azjezz <azjezz@protonmail.com>

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

99.32
/src/Psl/DateTime/Duration.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Psl\DateTime;
6

7
use DateInterval;
8
use Override;
9
use Psl\Comparison;
10
use Psl\Math;
11
use Psl\Str;
12

13
/**
14
 * Defines a representation of a time duration with specific hours, minutes, seconds,
15
 * and nanoseconds.
16
 *
17
 * All instances are normalized as follows:
18
 *
19
 * - all non-zero parts (hours, minutes, seconds, nanoseconds) will have the same sign
20
 * - minutes, seconds will be between -59 and 59
21
 * - nanoseconds will be between -999999999 and 999999999 (less than 1 second)
22
 *
23
 * For example, Duration::hours(2, -183) normalizes to "-1 hour(s), -3 minute(s)".
24
 *
25
 * @implements Comparison\Comparable<Duration>
26
 *
27
 * @immutable
28
 */
29
final readonly class Duration implements TemporalAmountInterface, Comparison\Comparable
30
{
31
    /**
32
     * Initializes a new instance of Duration with specified hours, minutes, seconds, and
33
     * nanoseconds.
34
     *
35
     * @param int $hours
36
     * @param int<-59, 59> $minutes
37
     * @param int<-59, 59> $seconds
38
     * @param int<-999999999, 999999999> $nanoseconds
39
     *
40
     * @pure
41
     */
42
    private function __construct(
43
        private int $hours,
44
        private int $minutes,
45
        private int $seconds,
46
        private int $nanoseconds,
47
    ) {}
264✔
48

49
    /**
50
     * Returns an instance representing the specified number of hours (and
51
     * optionally minutes, seconds, nanoseconds). Due to normalization, the
52
     * actual values in the returned instance may differ from the provided ones.
53
     *
54
     * @pure
55
     */
56
    public static function fromParts(int $hours, int $minutes = 0, int $seconds = 0, int $nanoseconds = 0): self
57
    {
58
        // This is where the normalization happens.
59
        $s =
262✔
60
            (SECONDS_PER_HOUR * $hours)
262✔
61
            + (SECONDS_PER_MINUTE * $minutes)
262✔
62
            + $seconds
262✔
63
            + (int) ($nanoseconds / NANOSECONDS_PER_SECOND);
262✔
64
        $ns = $nanoseconds % NANOSECONDS_PER_SECOND;
262✔
65
        if ($s < 0 && $ns > 0) {
262✔
66
            ++$s;
3✔
67
            $ns -= NANOSECONDS_PER_SECOND;
3✔
68
        } elseif ($s > 0 && $ns < 0) {
261✔
69
            --$s;
9✔
70
            $ns += NANOSECONDS_PER_SECOND;
9✔
71
        }
72

73
        $m = (int) ($s / 60);
262✔
74
        $s %= 60;
262✔
75
        $h = (int) ($m / 60);
262✔
76
        $m %= 60;
262✔
77
        return new self($h, $m, $s, $ns);
262✔
78
    }
79

80
    /**
81
     * Returns an instance representing the specified number of hours.
82
     *
83
     * @pure
84
     */
85
    public static function hours(int $hours): self
86
    {
87
        return self::fromParts($hours);
7✔
88
    }
89

90
    /**
91
     * Returns an instance representing the specified number of minutes. Due to
92
     * normalization, the actual value in the returned instance may differ from
93
     * the provided one, and the resulting instance may contain larger units.
94
     *
95
     * For example, `Duration::minutes(63)` normalizes to "1 hour(s), 3 minute(s)".
96
     *
97
     * @pure
98
     */
99
    public static function minutes(int $minutes): self
100
    {
101
        return self::fromParts(0, $minutes);
6✔
102
    }
103

104
    /**
105
     * Returns an instance representing the specified number of seconds. Due to
106
     * normalization, the actual value in the returned instance may differ from
107
     * the provided one, and the resulting instance may contain larger units.
108
     *
109
     * For example, `Duration::seconds(63)` normalizes to "1 minute(s), 3 second(s)".
110
     *
111
     * @pure
112
     */
113
    public static function seconds(int $seconds): self
114
    {
115
        return self::fromParts(0, 0, $seconds);
39✔
116
    }
117

118
    /**
119
     * Returns an instance representing the specified number of milliseconds (ms).
120
     * The value is converted and stored as nanoseconds, since that is the only
121
     * unit smaller than a second that we support. Due to normalization, the
122
     * resulting instance may contain larger units.
123
     *
124
     * For example, `Duration::milliseconds(8042)` normalizes to "8 second(s), 42000000 nanosecond(s)".
125
     *
126
     * @pure
127
     */
128
    public static function milliseconds(int $milliseconds): self
129
    {
130
        return self::fromParts(0, 0, 0, NANOSECONDS_PER_MILLISECOND * $milliseconds);
103✔
131
    }
132

133
    /**
134
     * Returns an instance representing the specified number of microseconds (us).
135
     * The value is converted and stored as nanoseconds, since that is the only
136
     * unit smaller than a second that we support. Due to normalization, the
137
     * resulting instance may contain larger units.
138
     *
139
     * For example, `Duration::microseconds(8000042)` normalizes to "8 second(s), 42000 nanosecond(s)".
140
     *
141
     * @pure
142
     */
143
    public static function microseconds(int $microseconds): self
144
    {
145
        return self::fromParts(0, 0, 0, NANOSECONDS_PER_MICROSECOND * $microseconds);
4✔
146
    }
147

148
    /**
149
     * Returns an instance representing the specified number of nanoseconds (ns).
150
     * Due to normalization, the resulting instance may contain larger units.
151
     *
152
     * For example, `Duration::nanoseconds(8000000042)` normalizes to "8 second(s), 42 nanosecond(s)".
153
     *
154
     * @pure
155
     */
156
    public static function nanoseconds(int $nanoseconds): self
157
    {
158
        return self::fromParts(0, 0, 0, $nanoseconds);
7✔
159
    }
160

161
    /**
162
     * Returns an instance with all parts equal to 0.
163
     *
164
     * @pure
165
     */
166
    public static function zero(): self
167
    {
168
        return new self(0, 0, 0, 0);
9✔
169
    }
170

171
    /**
172
     * Compiles and returns the duration's components (hours, minutes, seconds, nanoseconds) in an
173
     * array, in descending order of significance.
174
     *
175
     * @return array{int, int, int, int}
176
     *
177
     * @psalm-mutation-free
178
     */
179
    public function getParts(): array
180
    {
181
        return [$this->hours, $this->minutes, $this->seconds, $this->nanoseconds];
52✔
182
    }
183

184
    /**
185
     * Returns the "hours" part of this time duration.
186
     *
187
     * @psalm-mutation-free
188
     */
189
    public function getHours(): int
190
    {
191
        return $this->hours;
6✔
192
    }
193

194
    /**
195
     * Returns the "minutes" part of this time duration.
196
     *
197
     * @psalm-mutation-free
198
     */
199
    public function getMinutes(): int
200
    {
201
        return $this->minutes;
6✔
202
    }
203

204
    /**
205
     * Returns the "seconds" part of this time duration.
206
     *
207
     * @psalm-mutation-free
208
     */
209
    public function getSeconds(): int
210
    {
211
        return $this->seconds;
5✔
212
    }
213

214
    /**
215
     * Returns the "nanoseconds" part of this time duration.
216
     *
217
     * @psalm-mutation-free
218
     */
219
    public function getNanoseconds(): int
220
    {
221
        return $this->nanoseconds;
6✔
222
    }
223

224
    /**
225
     * Computes, and returns the total duration of the instance in hours as a floating-point number,
226
     * including any fractional parts.
227
     *
228
     * @psalm-mutation-free
229
     */
230
    public function getTotalHours(): float
231
    {
232
        return (
11✔
233
            $this->hours
11✔
234
            + ($this->minutes / MINUTES_PER_HOUR)
11✔
235
            + ($this->seconds / SECONDS_PER_HOUR)
11✔
236
            + ($this->nanoseconds / (SECONDS_PER_HOUR * NANOSECONDS_PER_SECOND))
11✔
237
        );
11✔
238
    }
239

240
    /**
241
     * Computes, and returns the total duration of the instance in minutes as a floating-point number,
242
     * including any fractional parts.
243
     *
244
     * @psalm-mutation-free
245
     */
246
    public function getTotalMinutes(): float
247
    {
248
        return (
9✔
249
            ($this->hours * MINUTES_PER_HOUR)
9✔
250
            + $this->minutes
9✔
251
            + ($this->seconds / SECONDS_PER_MINUTE)
9✔
252
            + ($this->nanoseconds / (SECONDS_PER_MINUTE * NANOSECONDS_PER_SECOND))
9✔
253
        );
9✔
254
    }
255

256
    /**
257
     * Computes, and returns the total duration of the instance in seconds as a floating-point number,
258
     * including any fractional parts.
259
     *
260
     * @psalm-mutation-free
261
     */
262
    public function getTotalSeconds(): float
263
    {
264
        return (
121✔
265
            $this->seconds
121✔
266
            + ($this->minutes * SECONDS_PER_MINUTE)
121✔
267
            + ($this->hours * SECONDS_PER_HOUR)
121✔
268
            + ($this->nanoseconds / NANOSECONDS_PER_SECOND)
121✔
269
        );
121✔
270
    }
271

272
    /**
273
     * Computes, and returns the total duration of the instance in milliseconds as a floating-point number,
274
     * including any fractional parts.
275
     *
276
     * @psalm-mutation-free
277
     */
278
    public function getTotalMilliseconds(): float
279
    {
280
        return (
10✔
281
            ($this->hours * SECONDS_PER_HOUR * MILLISECONDS_PER_SECOND)
10✔
282
            + ($this->minutes * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND)
10✔
283
            + ($this->seconds * MILLISECONDS_PER_SECOND)
10✔
284
            + ($this->nanoseconds / NANOSECONDS_PER_MILLISECOND)
10✔
285
        );
10✔
286
    }
287

288
    /**
289
     * Computes, and returns the total duration of the instance in microseconds as a floating-point number,
290
     * including any fractional parts.
291
     *
292
     * @psalm-mutation-free
293
     */
294
    public function getTotalMicroseconds(): float
295
    {
296
        return (
9✔
297
            ($this->hours * SECONDS_PER_HOUR * MICROSECONDS_PER_SECOND)
9✔
298
            + ($this->minutes * SECONDS_PER_MINUTE * MICROSECONDS_PER_SECOND)
9✔
299
            + ($this->seconds * MICROSECONDS_PER_SECOND)
9✔
300
            + ($this->nanoseconds / NANOSECONDS_PER_MICROSECOND)
9✔
301
        );
9✔
302
    }
303

304
    /**
305
     * Determines whether the instance represents a zero duration.
306
     *
307
     * @psalm-mutation-free
308
     */
309
    public function isZero(): bool
310
    {
311
        return 0 === $this->hours && 0 === $this->minutes && 0 === $this->seconds && 0 === $this->nanoseconds;
16✔
312
    }
313

314
    /**
315
     * Checks if the duration is positive, implying that all non-zero components are positive.
316
     *
317
     * Due to normalization, it is guaranteed that a positive time duration will
318
     * have all of its parts (hours, minutes, seconds, nanoseconds) positive or
319
     * equal to 0.
320
     *
321
     * Note that this method returns false if all parts are equal to 0.
322
     *
323
     * @psalm-mutation-free
324
     */
325
    public function isPositive(): bool
326
    {
327
        return $this->hours > 0 || $this->minutes > 0 || $this->seconds > 0 || $this->nanoseconds > 0;
10✔
328
    }
329

330
    /**
331
     * Checks if the duration is negative, implying that all non-zero components are negative.
332
     *
333
     * Due to normalization, it is guaranteed that a negative time duration will
334
     * have all of its parts (hours, minutes, seconds, nanoseconds) negative or
335
     * equal to 0.
336
     *
337
     * Note that this method returns false if all parts are equal to 0.
338
     *
339
     * @psalm-mutation-free
340
     */
341
    public function isNegative(): bool
342
    {
343
        return $this->hours < 0 || $this->minutes < 0 || $this->seconds < 0 || $this->nanoseconds < 0;
6✔
344
    }
345

346
    /**
347
     * Returns a new instance with the "hours" part changed to the specified
348
     * value.
349
     *
350
     * Note that due to normalization, the actual value in the returned
351
     * instance may differ, and this may affect other parts of the returned
352
     * instance too.
353
     *
354
     * For example, `Duration::hours(2, 30)->withHours(-1)` is equivalent to
355
     * `Duration::hours(-1, 30)` which normalizes to "-30 minute(s)".
356
     *
357
     * @psalm-mutation-free
358
     */
359
    public function withHours(int $hours): self
360
    {
361
        return self::fromParts($hours, $this->minutes, $this->seconds, $this->nanoseconds);
1✔
362
    }
363

364
    /**
365
     * Returns a new instance with the "minutes" part changed to the specified
366
     * value.
367
     *
368
     * Note that due to normalization, the actual value in the returned
369
     * instance may differ, and this may affect other parts of the returned
370
     * instance too.
371
     *
372
     * For example, `Duration::minutes(2, 30)->withMinutes(-1)` is equivalent to
373
     * `Duration::minutes(-1, 30)` which normalizes to "-30 second(s)".
374
     *
375
     * @psalm-mutation-free
376
     */
377
    public function withMinutes(int $minutes): self
378
    {
379
        return self::fromParts($this->hours, $minutes, $this->seconds, $this->nanoseconds);
1✔
380
    }
381

382
    /**
383
     * Returns a new instance with the "seconds" part changed to the specified
384
     * value.
385
     *
386
     * Note that due to normalization, the actual value in the returned
387
     * instance may differ, and this may affect other parts of the returned
388
     * instance too.
389
     *
390
     * For example, `Duration::minutes(2, 30)->withSeconds(-30)` is equivalent
391
     * to `Duration::minutes(2, -30)` which normalizes to "1 minute(s), 30 second(s)".
392
     *
393
     * @psalm-mutation-free
394
     */
395
    public function withSeconds(int $seconds): self
396
    {
397
        return self::fromParts($this->hours, $this->minutes, $seconds, $this->nanoseconds);
1✔
398
    }
399

400
    /**
401
     * Returns a new instance with the "nanoseconds" part changed to the specified
402
     * value.
403
     *
404
     * Note that due to normalization, the actual value in the returned
405
     * instance may differ, and this may affect other parts of the returned
406
     * instance too.
407
     *
408
     * For example, `Duration::seconds(2)->withNanoseconds(-1)` is equivalent
409
     * to `Duration::seconds(2, -1)` which normalizes to "1 second(s), 999999999 nanosecond(s)".
410
     *
411
     * @psalm-mutation-free
412
     */
413
    public function withNanoseconds(int $nanoseconds): self
414
    {
415
        return self::fromParts($this->hours, $this->minutes, $this->seconds, $nanoseconds);
1✔
416
    }
417

418
    /**
419
     * Implements a comparison between this duration and another, based on their duration.
420
     *
421
     * @param Duration $other
422
     *
423
     * @psalm-mutation-free
424
     */
425
    #[Override]
426
    public function compare(mixed $other): Comparison\Order
427
    {
428
        if ($this->hours !== $other->hours) {
11✔
429
            return Comparison\Order::from($this->hours <=> $other->hours);
1✔
430
        }
431

432
        if ($this->minutes !== $other->minutes) {
11✔
433
            return Comparison\Order::from($this->minutes <=> $other->minutes);
2✔
434
        }
435

436
        if ($this->seconds !== $other->seconds) {
9✔
437
            return Comparison\Order::from($this->seconds <=> $other->seconds);
3✔
438
        }
439

440
        return Comparison\Order::from($this->nanoseconds <=> $other->nanoseconds);
8✔
441
    }
442

443
    /**
444
     * Evaluates whether this duration is equivalent to another, considering all time components.
445
     *
446
     * @param TemporalAmountInterface $other
447
     *
448
     * @psalm-mutation-free
449
     */
450
    #[Override]
451
    public function equals(mixed $other): bool
452
    {
453
        if (!$other instanceof Duration) {
8✔
NEW
454
            return false;
×
455
        }
456

457
        return $this->compare($other) === Comparison\Order::Equal;
8✔
458
    }
459

460
    /**
461
     * Determines if this duration is shorter than another.
462
     *
463
     * @psalm-mutation-free
464
     */
465
    public function shorter(self $other): bool
466
    {
467
        return $this->compare($other) === Comparison\Order::Less;
7✔
468
    }
469

470
    /**
471
     * Determines if this duration is shorter than, or equivalent to another.
472
     *
473
     * @psalm-mutation-free
474
     */
475
    public function shorterOrEqual(self $other): bool
476
    {
477
        return $this->compare($other) !== Comparison\Order::Greater;
6✔
478
    }
479

480
    /**
481
     * Determines if this duration is longer than another.
482
     *
483
     * @psalm-mutation-free
484
     */
485
    public function longer(self $other): bool
486
    {
487
        return $this->compare($other) === Comparison\Order::Greater;
6✔
488
    }
489

490
    /**
491
     * Determines if this duration is longer than, or equivalent to another.
492
     *
493
     * @psalm-mutation-free
494
     */
495
    public function longerOrEqual(self $other): bool
496
    {
497
        return $this->compare($other) !== Comparison\Order::Less;
6✔
498
    }
499

500
    /**
501
     * Returns true if this instance represents a time duration longer than $a but
502
     * shorter than $b, or vice-versa (shorter than $a but longer than $b), or if
503
     * this instance is equal to $a and/or $b. Returns false if this instance is
504
     * shorter/longer than both.
505
     *
506
     * @psalm-mutation-free
507
     */
508
    public function betweenInclusive(self $a, self $b): bool
509
    {
510
        $ca = $this->compare($a);
7✔
511
        $cb = $this->compare($b);
7✔
512

513
        return $ca === Comparison\Order::Equal || $ca !== $cb;
7✔
514
    }
515

516
    /**
517
     * Returns true if this instance represents a time duration longer than $a but
518
     * shorter than $b, or vice-versa (shorter than $a but longer than $b).
519
     * Returns false if this instance is equal to $a and/or $b, or shorter/longer
520
     * than both.
521
     *
522
     * @psalm-mutation-free
523
     */
524
    public function betweenExclusive(self $a, self $b): bool
525
    {
526
        $ca = $this->compare($a);
7✔
527
        $cb = $this->compare($b);
7✔
528

529
        return $ca !== Comparison\Order::Equal && $cb !== Comparison\Order::Equal && $ca !== $cb;
7✔
530
    }
531

532
    /**
533
     * Returns a new instance, converting a positive/negative duration to the
534
     * opposite (negative/positive) duration of equal length. The resulting
535
     * instance has all parts equivalent to the current instance's parts
536
     * multiplied by -1.
537
     *
538
     * @psalm-mutation-free
539
     */
540
    public function invert(): self
541
    {
542
        if ($this->isZero()) {
3✔
543
            return $this;
2✔
544
        }
545

546
        return new self(-$this->hours, -$this->minutes, -$this->seconds, -$this->nanoseconds);
2✔
547
    }
548

549
    /**
550
     * Returns a new instance representing the sum of this instance and the
551
     * provided `$other` instance. Note that time duration can be negative, so
552
     * the resulting instance is not guaranteed to be shorter/longer than either
553
     * of the inputs.
554
     *
555
     * This operation is commutative: `$a->plus($b) === $b->plus($a)`
556
     *
557
     * @psalm-mutation-free
558
     */
559
    public function plus(self $other): self
560
    {
561
        if ($other->isZero()) {
5✔
562
            return $this;
2✔
563
        }
564

565
        if ($this->isZero()) {
4✔
566
            return $other;
2✔
567
        }
568

569
        return self::fromParts(
3✔
570
            $this->hours + $other->hours,
3✔
571
            $this->minutes + $other->minutes,
3✔
572
            $this->seconds + $other->seconds,
3✔
573
            $this->nanoseconds + $other->nanoseconds,
3✔
574
        );
3✔
575
    }
576

577
    /**
578
     * Returns a new instance representing the difference between this instance
579
     * and the provided `$other` instance (i.e. `$other` subtracted from `$this`).
580
     * Note that time duration can be negative, so the resulting instance is not
581
     * guaranteed to be shorter/longer than either of the inputs.
582
     *
583
     * This operation is not commutative: `$a->minus($b) !== $b->minus($a)`
584
     * But: `$a->minus($b) === $b->minus($a)->invert()`
585
     *
586
     * @psalm-mutation-free
587
     */
588
    public function minus(self $other): self
589
    {
590
        if ($other->isZero()) {
5✔
591
            return $this;
2✔
592
        }
593

594
        if ($this->isZero()) {
4✔
595
            return $other->invert();
2✔
596
        }
597

598
        return self::fromParts(
3✔
599
            $this->hours - $other->hours,
3✔
600
            $this->minutes - $other->minutes,
3✔
601
            $this->seconds - $other->seconds,
3✔
602
            $this->nanoseconds - $other->nanoseconds,
3✔
603
        );
3✔
604
    }
605

606
    /**
607
     * Returns an ISO 8601 duration string representing this duration.
608
     *
609
     * Examples: "PT5H30M", "PT10.5S", "PT0S", "-PT1H30M".
610
     * Fractional seconds are used when nanoseconds are present.
611
     *
612
     * @psalm-mutation-free
613
     */
614
    public function toIso8601(): string
615
    {
616
        return Internal\format_iso8601_duration($this->hours, $this->minutes, $this->seconds, $this->nanoseconds);
14✔
617
    }
618

619
    /**
620
     * Parses an ISO 8601 duration string into a Duration.
621
     *
622
     * Accepts formats like "PT5H30M", "PT10.5S", "-PT1H", "PT0S".
623
     * Only time components (H, M, S after T) are accepted. Date components (Y, M, D before T)
624
     * will cause a {@see Exception\ParserException}.
625
     *
626
     * @throws Exception\ParserException If the string is not a valid ISO 8601 duration.
627
     *
628
     * @pure
629
     */
630
    public static function fromIso8601(string $value): self
631
    {
632
        [$hours, $minutes, $seconds, $nanoseconds] = Internal\parse_iso8601_duration($value);
19✔
633

634
        return self::fromParts($hours, $minutes, $seconds, $nanoseconds);
14✔
635
    }
636

637
    /**
638
     * Adds this duration to the given temporal, returning the result.
639
     *
640
     * @psalm-mutation-free
641
     */
642
    public function addTo(TemporalInterface $temporal): TemporalInterface
643
    {
644
        return $temporal->plus($this);
2✔
645
    }
646

647
    /**
648
     * Subtracts this duration from the given temporal, returning the result.
649
     *
650
     * @psalm-mutation-free
651
     */
652
    public function subtractFrom(TemporalInterface $temporal): TemporalInterface
653
    {
654
        return $temporal->minus($this);
1✔
655
    }
656

657
    /**
658
     * Returns the time duration as string, useful e.g. for debugging. This is not
659
     * meant to be a comprehensive way to format time durations for user-facing
660
     * output.
661
     *
662
     * @param int<0, max> $max_decimals
663
     *
664
     * @psalm-mutation-free
665
     */
666
    public function toString(int $max_decimals = 3): string
667
    {
668
        $decimal_part = '';
18✔
669
        if ($max_decimals > 0) {
18✔
670
            $decimal_part = (string) Math\abs($this->nanoseconds);
17✔
671
            $decimal_part = Str\pad_left($decimal_part, 9, '0');
17✔
672
            $decimal_part = Str\slice($decimal_part, 0, $max_decimals);
17✔
673
            $decimal_part = Str\trim_right($decimal_part, '0');
17✔
674
        }
675

676
        if ('' !== $decimal_part) {
18✔
677
            $decimal_part = '.' . $decimal_part;
6✔
678
        }
679

680
        $sec_sign = $this->seconds < 0 || $this->nanoseconds < 0 ? '-' : '';
18✔
681
        $sec = Math\abs($this->seconds);
18✔
682

683
        $containsHours = 0 !== $this->hours;
18✔
684
        $containsMinutes = 0 !== $this->minutes;
18✔
685
        $concatenatedSeconds = $sec_sign . (string) $sec . $decimal_part;
18✔
686
        $containsSeconds = '0' !== $concatenatedSeconds;
18✔
687

688
        /** @var list<string> $output */
689
        $output = [];
18✔
690
        if ($containsHours) {
18✔
691
            $output[] = (string) $this->hours . ' hour(s)';
9✔
692
        }
693

694
        if ($containsMinutes || $containsHours && $containsSeconds) {
18✔
695
            $output[] = (string) $this->minutes . ' minute(s)';
10✔
696
        }
697

698
        if ($containsSeconds) {
18✔
699
            $output[] = $concatenatedSeconds . ' second(s)';
13✔
700
        }
701

702
        return [] === $output ? '0 second(s)' : Str\join($output, ', ');
18✔
703
    }
704

705
    /**
706
     * Returns a string representation of the time duration.
707
     *
708
     * @psalm-mutation-free
709
     */
710
    #[Override]
711
    public function __toString(): string
712
    {
713
        return $this->toString();
1✔
714
    }
715

716
    /**
717
     * Converts this {@see Duration} to a PHP {@see DateInterval}.
718
     *
719
     * Note: nanosecond precision is truncated to microseconds.
720
     *
721
     * @return DateInterval
722
     *
723
     * @psalm-mutation-free
724
     */
725
    #[Override]
726
    public function toStdlib(): DateInterval
727
    {
728
        $total_seconds = (int) $this->getTotalSeconds();
4✔
729

730
        return DateInterval::createFromDateString($total_seconds . ' seconds');
4✔
731
    }
732

733
    /**
734
     * Returns data which can be serialized by json_encode().
735
     *
736
     * @return array{hours: int, minutes: int, seconds: int, nanoseconds: int}
737
     *
738
     * @psalm-mutation-free
739
     */
740
    #[Override]
741
    public function jsonSerialize(): array
742
    {
743
        return [
1✔
744
            'hours' => $this->hours,
1✔
745
            'minutes' => $this->minutes,
1✔
746
            'seconds' => $this->seconds,
1✔
747
            'nanoseconds' => $this->nanoseconds,
1✔
748
        ];
1✔
749
    }
750
}
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