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