Add a new class Time\Duration with the following stub:
<?php namespace Time; /** * @strict-properties */ final class Duration { public readonly int $seconds; public readonly int $nanoseconds; public readonly bool $negative; /** * Create a duration representing $seconds seconds and $nanoseconds nanoseconds. Neither parameter * may be negative. $nanoseconds must be less than 1_000_000_000 (the number of nanoseconds in a * second). * * This contructor creates a Duration from its “atomic” components. */ public static function fromSeconds(int $seconds, int $nanoseconds = 0): self { } /** * Create a duration representing $nanoseconds nano-seconds. $nanoseconds must not be negative. */ public static function fromNanoseconds(int $nanoseconds): self { } /** * Create a duration representing $microseconds micro-seconds. $microseconds must not be negative. */ public static function fromMicroseconds(int $microseconds): self { } /** * Create a duration representing $milliseconds milli-seconds. $milliseconds must not be negative. */ public static function fromMilliseconds(int $milliseconds): self { } /** * Create a duration representing $minutes minutes. $minutes must not be negative. */ public static function fromMinutes(int $minutes): self { } /** * Create a duration representing $hours hours. $hours must not be negative. */ public static function fromHours(int $hours): self { } /** * Parse a ISO-8601 period. ISO-8601 periods with a date component will be rejected. * The biggest allowed component is H. */ public static function fromIso8601String(string $specification): self { } /** * Negates the duration. * * @return self -$this */ public function negate(): self { } /** * Add the given duration to the duration. * * @return self $this + $duration */ public function add(self $duration): self { } /** * Subtract the given duration from the duration. * * @return self $this - $duration */ public function sub(self $duration): self { } /** * Multiply the length of the duration by the given factor: * * @return self $this * $factor */ public function multiplyBy(int $factor): self { } /** * Divide the length of the duration by the given divisor. * * Fractional nanoseconds will be truncated. * * @return self $this / $factor */ public function divideBy(int $divisor): self { } /** * Returns -1, 0, 1 if $a is less than, equal to, or greater than $b respectively. */ public static function compare(self $a, self $b): int { } }
If this RFC is accepted for PHP 8.6, the polling API introduced in PHP RFC: Polling API for PHP 8.6 will be adjusted to make use of the Time\Duration class instead of separate $timeout and $timeoutMicroseconds parameters:
namespace Io\Poll; final class Context { /** * […] * * @param ?Time\Duration $timeout Timeout. * null means wait indefinitely. * Time\Duration::fromNanoseconds(0) means return immediately (non-blocking poll). * Must be !$timeout->negative * * […] */ public function wait( ?Time\Duration $timeout = null, ?int $maxEvents = null ): array {} }
Note that these examples show possible hypothetical use-cases. The RFC does not propose a change to existing functions, except for the Io\Poll\Context as described previously.
<?php use Time\Duration; $oneSecond = Duration::fromSeconds(1); $halfSecond = $oneSecond->divideBy(2); $onePointFiveSeconds = $oneSecond->add($halfSecond); // Sleep for 1.5 seconds sleep($onePointFiveSeconds); $negativeHour = Duration::fromHours(1)->negate(); $durations = [ $oneSecond, $negativeHour, $onePointFiveSeconds, $halfSecond, ]; usort($durations, Duration::compare(...)); // Will sort to [-1h, 0.5s, 1s, 1.5s] ?>
A proof of concept user-land implementation is as follows. This implementation is non-authoritative and may contain bugs.
<?php namespace Time; /** * @strict-properties */ final class Duration { private function __construct( public readonly int $seconds, public readonly int $nanoseconds, public readonly bool $negative, ) { \assert($seconds >= 0); \assert($nanoseconds >= 0); \assert($nanoseconds < 1_000_000_000); } /** * Create a duration representing $seconds seconds and $nanoseconds nanoseconds. Neither parameter * may be negative. $nanoseconds must be less than 1_000_000_000 (the number of nanoseconds in a * second). * * This contructor creates a Duration from its “atomic” components. */ public static function fromSeconds(int $seconds, int $nanoseconds = 0): self { if ($seconds < 0) { throw new \ValueError('$seconds must not be negative'); } if ($nanoseconds < 0 || $nanoseconds >= 1_000_000_000) { throw new \ValueError('$nanoseconds must be between 0 and 999_999_999'); } return new self($seconds, $nanoseconds, false); } /** * Create a duration representing $nanoseconds nano-seconds. $nanoseconds must not be negative. */ public static function fromNanoseconds(int $nanoseconds): self { $seconds = \intdiv($nanoseconds, 1_000_000_000); $nanoseconds = $nanoseconds % 1_000_000_000; return self::fromSeconds($seconds, $nanoseconds); } /** * Create a duration representing $microseconds micro-seconds. $microseconds must not be negative. */ public static function fromMicroseconds(int $microseconds): self { return self::fromNanoseconds($microseconds * 1_000); } /** * Create a duration representing $milliseconds milli-seconds. $milliseconds must not be negative. */ public static function fromMilliseconds(int $milliseconds): self { return self::fromMicroseconds($milliseconds * 1_000); } /** * Create a duration representing $minutes minutes. $minutes must not be negative. */ public static function fromMinutes(int $minutes): self { return self::fromSeconds($minutes * 60); } /** * Create a duration representing $hours hours. $hours must not be negative. */ public static function fromHours(int $hours): self { return self::fromMinutes($hours * 60); } /** * Parse a ISO-8601 period. ISO-8601 periods with a date component will be rejected. * The biggest allowed component is H. */ public static function fromIso8601String(string $specification): self { /* Omitted */ } /** * Negates the duration. * * @return self -$this */ public function negate(): self { return clone($this, ['negative' => !$this->negative]); } /** * Add the given duration to the duration. * * @return self $this + $duration */ public function add(self $duration): self { /* (+x) + (-y) == (+x) - (+y) */ if (!$this->negative && $duration->negative) { return $this->sub($duration->negate()); } /* (-x) + (+y) == (+y) - (+x) */ if ($this->negative && !$duration->negative) { return $duration->sub($this->negate()); } /* (-x) + (-y) = -((+x) + (+y)) */ if ($this->negative && $duration->negative) { return $this->negate()->add($duration->negate())->negate(); } \assert(!$this->negative); \assert(!$duration->negative); $seconds = $this->seconds + $duration->seconds; $nanoseconds = $this->nanoseconds + $duration->nanoseconds; $seconds += \intdiv($nanoseconds, 1_000_000_000); $nanoseconds = $nanoseconds % 1_000_000_000; return self::fromSeconds($seconds, $nanoseconds); } /** * Subtract the given duration from the duration. * * @return self $this - $duration */ public function sub(self $duration): self { /* (+x) - (-y) == (+x) + (+y) */ if (!$this->negative && $duration->negative) { return $this->add($duration->negate()); } /* (-x) - (+y) == -((+x) + (+y)) */ if ($this->negative && !$duration->negative) { return $this->negate()->add($duration)->negate(); } /* (-x) - (-y) == (-x) + (+y) */ if ($this->negative && $duration->negative) { return $this->add($duration->negate()); } \assert(!$this->negative); \assert(!$duration->negative); if (self::compare($this, $duration) >= 0) { $seconds = $this->seconds - $duration->seconds; $nanoseconds = $this->nanoseconds - $duration->nanoseconds; if ($nanoseconds < 0) { $nanoseconds += 1_000_000_000; $seconds--; } return self::fromSeconds($seconds, $nanoseconds); } else { /* (+x) - (+y) = -((+y) - (+x)) */ return $duration->sub($this)->negate(); } } /** * Multiply the length of the duration by the given factor: * * @return self $this * $factor */ public function multiplyBy(int $factor): self { $seconds = $this->seconds * $factor; $nanoseconds = $this->nanoseconds * $factor; $seconds += \intdiv($nanoseconds, 1_000_000_000); $nanoseconds = $nanoseconds % 1_000_000_000; return clone($this, ['seconds' => $seconds, 'nanoseconds' => $nanoseconds]); } /** * Divide the length of the duration by the given divisor. * * Fractional nanoseconds will be truncated. * * @return self $this / $factor */ public function divideBy(int $divisor): self { $seconds = \intdiv($this->seconds, $divisor); $nanoseconds = $this->nanoseconds + (($this->seconds % $divisor) * 1_000_000_000); $nanoseconds = \intdiv($nanoseconds, $divisor); return clone($this, ['seconds' => $seconds, 'nanoseconds' => $nanoseconds]); } /** * Returns -1, 0, 1 if $a is less than, equal to, or greater than $b respectively. */ public static function compare(self $a, self $b): int { if ($a->negative && !$b->negative) { return -1; } if (!$a->negative && $b->negative) { return 1; } if ($a->negative && $b->negative) { return - ($a->seconds <=> $b->seconds) ?: - ($a->nanoseconds <=> $b->nanoseconds); } if (!$a->negative && !$b->negative) { return ($a->seconds <=> $b->seconds) ?: ($a->nanoseconds <=> $b->nanoseconds); } throw new \Error('unreachable'); } }
This RFC is introducing a single class and starts using the Time for PHP’s standard library. Adding new symbols is not considered a breaking change as per our policy. The naming of the class and namespace is in line with PHP’s naming policy.
A GitHub search for language:php “namespace Time;” symbol:Duration reveals a total of 7 hits. There are two notable hits with:
which both try to solve the same problem in a very similar way and confirming there is an interest in having this API.
Next minor (8.6).
None.
Existing extensions may want to start using this new class where-ever a duration is required.
None.
None.
The introduction of this class and the associated Time namespace is intended to lay the groundwork for a new date and time library in upcoming PHP versions. It is intentionally designed with a minimal, but useful, API that future additions will be able to build on.
Some ideas that have been taken into account when designing the Time\Duration class:
Time\Duration object.Time\Duration, but not the other way around.Primary Vote requiring a 2/3 majority to accept the RFC:
Links to proof of concept PR.
If there is no patch, make it clear who will create a patch, or whether a volunteer to help with implementation is needed.
After the RFC is implemented, this section should contain:
fromSeconds() constructor is an intentional exception, because it works with the two “base units”.