Hi

On 2026-07-04 11:09, Marc B. wrote:
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.

It would still be, because you might unknowingly lose precision when someone gives you a Duration object with nanoseconds filled in, but you can only deal with microseconds, because your code was written before nanoseconds were supported.. Rounding down / truncating is the wrong behavior in many situations. See the “Passing Duration objects to APIs unable to handle nanosecond precision” section in the RFC.

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 acknowledged in the parentheses that this modeling is not strictly correct from a semantic perspective, but it is meaningful:

As Morgan explained, an instant (“point in time”) is effectively an origin + a Duration. Since `hrtime()` returns an instant retrieved from a monotonic clock, the origin is unknown to you and the only information you know is the duration. Thus you can represent this as just the Duration without losing any information. The only legal operation is calculating the difference, since for that the origin cancels out ((origin + duration1) - (origin + duration2)) is just (duration1 - duration2).

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))

No, this behaves differently from the `fromSeconds()` constructor due to possible overflow in the `$nanos` component. The `fromSeconds()` constructor takes a fixed-point decimal which is a single semantic value and not appropriately represented by an explicit addition operation.

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.

As mentioned in the previous paragraph: Each constructor takes a single “atomic value”. If you have an overflow in the microseconds, then it's no longer a single semantic value, but two values. It's the difference between “I have 1.5 seconds” and “I have 1 second and 500_000 microseconds” where the “and” is the addition operation.

When I'm presented a web form as a user and it asks for a Duration with two fields “seconds” and “microseconds”, then I would fully expect a 0 <= microseconds < 1_000_000 validation happening there, and not a silent overflow into the seconds field that I explicitly provided. Similarly when asked for a date with separate year, month, day inputs, I would expect 2026-06-31 to be rejected as invalid instead of silently adjusting it to 2026-07-01, because as a user semantically I'm thinking about entering a single semantic value.

Best regards
Tim Düsterhus

Reply via email to