Re: [RFC] Duration class

From: Date: Sat, 04 Jul 2026 09:09:01 +0000
Subject: Re: [RFC] Duration class
References: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16  Groups: php.internals 
Request: Send a blank email to internals+get-131755@lists.php.net to get a copy of this message
Hi On 03.07.26 23:51, Tim Düsterhus wrote:
Hi On 7/3/26 22:17, Marc B. wrote:
The fromSeconds constructor breaks the flow of all other constructors as this is not only about seconds. Also, this directly exposes how it's handled internally to the API which makes it hard to extend later on (maybe we want to support picoseconds on 10 years).
“Nanosecond precision” is an integral part of the design of the class and folks will build assumptions around this. Changing the precision later will be something between hard to impossible, even if the fromSeconds() constructor wouldn't take a fractional part. This is a property shared between all the other languages we looked at (except for Swift as mentioned by John Bafford): Everyone models their Durations with nanosecond precision and documents it as such.
This is a simplified example what I mean: // Initial version final class Duration {
    public readonly $totalSeconds;
    public int $milliseconds { get => (int)($this->microseconds / 1_000); } // 0 - 999
    public readonly $microseconds; // 0 - 999_999
    public static function from(/* ... */ int $seconds=0, int $milliseconds=0, int $microseconds=0)
} // Next version - introduce nanoseconds final class Duration {
    public readonly int $totalSeconds;
    public int $milliseconds { get => (int)($this->nanoseconds / 1_000_000); } // 0 - 999
    public int $microseconds { get => (int)($this->nanoseconds / 1_000); } // 0 - 999_999
    public readonly int $nanoseconds; // 0 - 999_999_999
    public static function from(/* ... */ int $seconds=0, int $milliseconds=0, int $microseconds=0, int $nanoseconds=0)
} // Next version - introduce picoseconds final class Duration {
    public readonly int $totalSeconds;
    public int $milliseconds { get => (int)($this->picoseconds / 1_000_000_000); } // 0 - 999
    public int $microseconds { get => (int)($this->picoseconds / 1_000_000); } // 0 - 999_999
    public int $nanoseconds { get => (int)($this->picoseconds / 1_000); } // 0 - 999_999_999
    public readonly int $picoseconds; // 0 - 999_999_999_999
    public static function from(/* ... */ int $seconds=0, int $milliseconds=0, int $microseconds=0, int $nanoseconds=0, int $picoseconds=0)
} Introducing nanoseconds and later on picoseconds in that example isn't a big deal anymore.
PHP should make things simpler - not more complex. There are plenty of different duration systems out there a such values could come from as input to a PHP application. The current Duration API forces you to manually correctly calculate the value before passing it the constructor. E.g.: * time measurement using microtime(true) * using hrtime(true) returns float on 32bit
For both of these, the false variant returns the value as a fixed-point decimal of seconds + fractional seconds, matching the “seconds with a fractional part” design of this RFC. For hrtime() specifically, you can even do the following:
    $duration = Duration::fromSeconds(...hrtime());
which will do the right thing (except for the fact that hrtime() *technically* returns an Instant [1], not a Duration). And of course the new date and time API would not be complete without also providing access to a high-precision (monotonic) clock in a way that cleanly interoperates with the new API. [1] An instant with an unknown origin.
Why would you create a duration directly from a point-in-time ?
I like the idea of a general "from" constructor - but not restricted to seconds + nanos but ALL supported units
This one could still be provided in future scope, but given my previous replies, you can probably assume that I don't consider it a good idea.
$seconds = 1; $millis = 1_000; $micros = 1_000_000; $nanos = 1_000_000_000; Duration::from(seconds: $seconds, milliseconds: $millis, microseconds: $micros, nanoseconds: $nanos); // 4 seconds - yes please
I'd argue that if I have the use case of constructing a duration from a number of seconds *and* multiple different fractions of a second that each could overflow into larger scales, I should likely stop and consider whether what I'm doing is a good idea. But even then:
    Duration::fromSeconds($seconds)
        ->add(Duration::fromMilliseconds($millis))
        ->add(Duration::fromMicroseconds($micros))
        ->add(Duration::fromNanoseconds($nanos));
will work and do the right thing.
The same applies to the nanoseconds argument of your fromSeconds() Duration::fromSeconds($seconds)->add(Duration::fromNanoseconds($nanos))
For the common case of “seconds + fractional seconds”, the fromSeconds() constructor works (and going from fractional milliseconds to fractional nanoseconds is a *1_000_000 operation that should be easy and obvious enough).
The hole point is to not need to manually calculate before and manually make sure it's in the expected range of the limited Duration constructor. // not tested and I still might miss a case - but this should give a mental modal what you need to manually handle for such a simple function function durationFromInput(int $seconds, int $microseconds): Duration {
    if ($microseconds > 999_999 || $microseconds < -999_999) {
        $seconds += intdiv($microseconds, 1_000_000);
        $microseconds = $microseconds % 1_000_000;
    }
    if ($microseconds < 0) {
        $seconds -= 1;
        $microseconds = 1_000_000 - $microseconds;
    }
    $negative = $seconds < 0;
    if ($negative) {
        $seconds *= -1;
    }
    if (is_float($seconds)) {
        throw new RangeError('Integer overflow');
    }
    $duration = Duration::fromSeconds($seconds, $microseconds * 1_000);
    if ($negative) {
        $duration = $duration::negate();
    }
    return $duration;
}
Best regards Tim Düsterhus


« previous php.internals (#131755) next »