Lines
100.00%
47 / 47
Methods
100.00%
18 / 18
Classes
100.00%
1 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| __construct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| zero | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| microseconds | 100.00% | 3 / 3 | 100.00% | 1 / 1 | 2 | ||
| milliseconds | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| seconds | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| minutes | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| hours | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| parse | 100.00% | 7 / 7 | 100.00% | 1 / 1 | 5 | ||
| requireAtLeastOneMillisecond | 100.00% | 3 / 3 | 100.00% | 1 / 1 | 2 | ||
| toMicroseconds | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| toMilliseconds | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| toSeconds | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| plus | 100.00% | 3 / 3 | 100.00% | 1 / 1 | 2 | ||
| isLongerThan | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| equals | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| formatAsClock | 100.00% | 4 / 4 | 100.00% | 1 / 1 | 2 | ||
| __toString | 100.00% | 10 / 10 | 100.00% | 1 / 1 | 4 | ||
| scale | 100.00% | 6 / 6 | 100.00% | 1 / 1 | 3 | ||
| 11 | final readonly class Duration implements Stringable | |
| 12 | { | |
| 13 | private const array UNITS = ['h' => 3_600_000_000, 'm' => 60_000_000, 's' => 1_000_000, 'ms' => 1_000, 'us' => 1]; | |
| 14 | ||
| 15 | private function __construct(private int $microseconds) {} | |
| 16 | ||
| 17 | public static function zero(): self | |
| 18 | { | |
| 19 | return new self(0); | |
| 20 | } | |
| 21 | ||
| 22 | /** @throws TimeException */ | |
| 23 | public static function microseconds(int $microseconds): self | |
| 24 | { | |
| 25 | if ($microseconds < 0) { | |
| 26 | throw TimeException::createForReason(TimeError::DurationNegative, microseconds: $microseconds); | |
| 27 | } | |
| 28 | ||
| 29 | return new self($microseconds); | |
| 30 | } | |
| 31 | ||
| 32 | /** @throws TimeException */ | |
| 33 | public static function milliseconds(int|float $milliseconds): self | |
| 34 | { | |
| 35 | return self::microseconds(self::scale($milliseconds, self::UNITS['ms'])); | |
| 36 | } | |
| 37 | ||
| 38 | /** @throws TimeException */ | |
| 39 | public static function seconds(int|float $seconds): self | |
| 40 | { | |
| 41 | return self::microseconds(self::scale($seconds, self::UNITS['s'])); | |
| 42 | } | |
| 43 | ||
| 44 | /** @throws TimeException */ | |
| 45 | public static function minutes(int|float $minutes): self | |
| 46 | { | |
| 47 | return self::microseconds(self::scale($minutes, self::UNITS['m'])); | |
| 48 | } | |
| 49 | ||
| 50 | /** @throws TimeException */ | |
| 51 | public static function hours(int|float $hours): self | |
| 52 | { | |
| 53 | return self::microseconds(self::scale($hours, self::UNITS['h'])); | |
| 54 | } | |
| 55 | ||
| 56 | /** @throws TimeException */ | |
| 57 | public static function parse(string $text): self | |
| 58 | { | |
| 59 | $segments = preg_split('/\s+/', trim($text), flags: \PREG_SPLIT_NO_EMPTY); | |
| 60 | $microseconds = 0; | |
| 61 | ||
| 62 | foreach ($segments === false || $segments === [] ? [$text] : $segments as $segment) { | |
| 63 | if (preg_match('/\A(\d+(?:\.\d+)?)(us|ms|s|m|h)\z/', $segment, $match) !== 1) { | |
| 64 | throw TimeException::createForReason(TimeError::DurationUnparsable, text: $text); | |
| 65 | } | |
| 66 | ||
| 67 | $microseconds += self::scale((float) $match[1], self::UNITS[$match[2]]); | |
| 68 | } | |
| 69 | ||
| 70 | return self::microseconds($microseconds); | |
| 71 | } | |
| 72 | ||
| 73 | /** @throws TimeException */ | |
| 74 | public function requireAtLeastOneMillisecond(string $usage): self | |
| 75 | { | |
| 76 | if ($this->microseconds < self::UNITS['ms']) { | |
| 77 | throw TimeException::createForReason(TimeError::DurationNotPositive, usage: $usage); | |
| 78 | } | |
| 79 | ||
| 80 | return $this; | |
| 81 | } | |
| 82 | ||
| 83 | public function toMicroseconds(): int | |
| 84 | { | |
| 85 | return $this->microseconds; | |
| 86 | } | |
| 87 | ||
| 88 | public function toMilliseconds(): int | |
| 89 | { | |
| 90 | return intdiv($this->microseconds, self::UNITS['ms']); | |
| 91 | } | |
| 92 | ||
| 93 | public function toSeconds(): float | |
| 94 | { | |
| 95 | return $this->microseconds / self::UNITS['s']; | |
| 96 | } | |
| 97 | ||
| 98 | /** @throws TimeException */ | |
| 99 | public function plus(self $other): self | |
| 100 | { | |
| 101 | if ($this->microseconds > \PHP_INT_MAX - $other->microseconds) { | |
| 102 | throw TimeException::createForReason(TimeError::DurationTooLarge); | |
| 103 | } | |
| 104 | ||
| 105 | return new self($this->microseconds + $other->microseconds); | |
| 106 | } | |
| 107 | ||
| 108 | public function isLongerThan(self $other): bool | |
| 109 | { | |
| 110 | return $this->microseconds > $other->microseconds; | |
| 111 | } | |
| 112 | ||
| 113 | public function equals(self $other): bool | |
| 114 | { | |
| 115 | return $this->microseconds === $other->microseconds; | |
| 116 | } | |
| 117 | ||
| 118 | public function formatAsClock(): string | |
| 119 | { | |
| 120 | $seconds = intdiv($this->microseconds, self::UNITS['s']); | |
| 121 | $fraction = $this->microseconds % self::UNITS['s']; | |
| 122 | $clock = \sprintf('%02d:%02d:%02d', intdiv($seconds, 3_600), intdiv($seconds % 3_600, 60), $seconds % 60); | |
| 123 | ||
| 124 | return $fraction === 0 ? $clock : $clock . '.' . rtrim(\sprintf('%06d', $fraction), '0'); | |
| 125 | } | |
| 126 | ||
| 127 | public function __toString(): string | |
| 128 | { | |
| 129 | if ($this->microseconds === 0) { | |
| 130 | return '0s'; | |
| 131 | } | |
| 132 | ||
| 133 | $parts = []; | |
| 134 | $remaining = $this->microseconds; | |
| 135 | ||
| 136 | foreach (self::UNITS as $unit => $size) { | |
| 137 | $amount = intdiv($remaining, $size); | |
| 138 | $remaining -= $amount * $size; | |
| 139 | ||
| 140 | if ($amount > 0) { | |
| 141 | $parts[] = $amount . $unit; | |
| 142 | } | |
| 143 | } | |
| 144 | ||
| 145 | return implode(' ', $parts); | |
| 146 | } | |
| 147 | ||
| 148 | /** @throws TimeException */ | |
| 149 | private static function scale(int|float $amount, int $factor): int | |
| 150 | { | |
| 151 | if (!is_finite((float) $amount)) { | |
| 152 | throw TimeException::durationNotFinite((float) $amount); | |
| 153 | } | |
| 154 | ||
| 155 | $scaled = round($amount * $factor); | |
| 156 | ||
| 157 | if (abs($scaled) > \PHP_INT_MAX) { | |
| 158 | throw TimeException::createForReason(TimeError::DurationTooLarge); | |
| 159 | } | |
| 160 | ||
| 161 | return (int) $scaled; | |
| 162 | } | |
| 163 | } |