Lines 100.00% 28 / 28
Functions and Methods 100.00% 16 / 16
Classes and Traits 100.00% 1 / 1
Name Lines Functions and Methods CRAP Classes and Traits
Instant 100.00% 28 / 28 100.00% 16 / 16 21 100.00% 1 / 1
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 fromEpochMicroseconds 100.00% 1 / 1 100.00% 1 / 1 1
 fromDateTime 100.00% 1 / 1 100.00% 1 / 1 1
 fromIso 100.00% 5 / 5 100.00% 1 / 1 3
 tryFromIso 100.00% 5 / 5 100.00% 1 / 1 4
 toEpochMicroseconds 100.00% 1 / 1 100.00% 1 / 1 1
 toDateTime 100.00% 5 / 5 100.00% 1 / 1 1
 toIso8601 100.00% 1 / 1 100.00% 1 / 1 1
 plus 100.00% 1 / 1 100.00% 1 / 1 1
 minus 100.00% 1 / 1 100.00% 1 / 1 1
 elapsedSince 100.00% 1 / 1 100.00% 1 / 1 1
 isBefore 100.00% 1 / 1 100.00% 1 / 1 1
 isAfter 100.00% 1 / 1 100.00% 1 / 1 1
 equals 100.00% 1 / 1 100.00% 1 / 1 1
 __toString 100.00% 1 / 1 100.00% 1 / 1 1
 floorDiv 100.00% 1 / 1 100.00% 1 / 1 1
1<?php
2
3declare(strict_types=1);
4
5namespace Stewart\Contracts\Time;
6
7use DateTimeImmutable;
8use DateTimeInterface;
9use DateTimeZone;
10use Exception;
11use Stewart\Contracts\Exception\TimeError;
12use Stewart\Contracts\Exception\TimeException;
13use Stringable;
14
15final readonly class Instant implements Stringable
16{
17    private const int MICROS_PER_SECOND = 1_000_000;
18    private const string ISO_SHAPE = '/\A\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}(:?\d{2})?)?\z/i';
19
20    private function __construct(private int $epochMicroseconds) {}
21
22    public static function fromEpochMicroseconds(int $epochMicroseconds): self
23    {
24        return new self($epochMicroseconds);
25    }
26
27    public static function fromDateTime(DateTimeInterface $moment): self
28    {
29        return new self($moment->getTimestamp() * self::MICROS_PER_SECOND + (int) $moment->format('u'));
30    }
31
32    /** @throws TimeException */
33    public static function fromIso(string $text): self
34    {
35        if (preg_match(self::ISO_SHAPE, $text) !== 1) {
36            throw TimeException::createForReason(TimeError::InstantUnparsable, text: $text);
37        }
38
39        try {
40            return self::fromDateTime(new DateTimeImmutable($text, new DateTimeZone('UTC')));
41        } catch (Exception) {
42            throw TimeException::createForReason(TimeError::InstantUnparsable, text: $text);
43        }
44    }
45
46    public static function tryFromIso(?string $text): ?self
47    {
48        if ($text === null || $text === '') {
49            return null;
50        }
51
52        try {
53            return self::fromIso($text);
54        } catch (TimeException) {
55            return null;
56        }
57    }
58
59    public function toEpochMicroseconds(): int
60    {
61        return $this->epochMicroseconds;
62    }
63
64    public function toDateTime(?DateTimeZone $zone = null): DateTimeImmutable
65    {
66        $seconds = self::floorDiv($this->epochMicroseconds, self::MICROS_PER_SECOND);
67        $fraction = $this->epochMicroseconds - $seconds * self::MICROS_PER_SECOND;
68
69        $utc = DateTimeImmutable::createFromFormat('U.u', \sprintf('%d.%06d', $seconds, $fraction), new DateTimeZone('UTC'));
70        \assert($utc !== false);
71
72        return $utc->setTimezone($zone ?? new DateTimeZone(date_default_timezone_get()));
73    }
74
75    public function toIso8601(): string
76    {
77        return $this->toDateTime(new DateTimeZone('UTC'))->format('Y-m-d\TH:i:s.u\Z');
78    }
79
80    public function plus(Duration $duration): self
81    {
82        return new self($this->epochMicroseconds + $duration->toMicroseconds());
83    }
84
85    public function minus(Duration $duration): self
86    {
87        return new self($this->epochMicroseconds - $duration->toMicroseconds());
88    }
89
90    public function elapsedSince(self $earlier): Duration
91    {
92        return Duration::microseconds(max(0, $this->epochMicroseconds - $earlier->epochMicroseconds));
93    }
94
95    public function isBefore(self $other): bool
96    {
97        return $this->epochMicroseconds < $other->epochMicroseconds;
98    }
99
100    public function isAfter(self $other): bool
101    {
102        return $this->epochMicroseconds > $other->epochMicroseconds;
103    }
104
105    public function equals(self $other): bool
106    {
107        return $this->epochMicroseconds === $other->epochMicroseconds;
108    }
109
110    public function __toString(): string
111    {
112        return $this->toIso8601();
113    }
114
115    private static function floorDiv(int $dividend, int $divisor): int
116    {
117        return intdiv($dividend - (($dividend % $divisor) + $divisor) % $divisor, $divisor);
118    }
119}