Re: [RFC] Duration class
| From: | Tim Düsterhus | Date: | Tue, 07 Jul 2026 14:19:02 +0000 |
| Subject: | Re: [RFC] Duration class | ||
| References: | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | Groups: | php.internals |
| Request: | Send a blank email to internals+get-131793@lists.php.net to get a copy of this message | ||
Hi
On 2026-07-04 11:09, Marc B. wrote:
This is a simplified example what I mean: // Initial version final class Duration {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.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.
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. SinceFor both of these, theWhy would you create a duration directly from a point-in-time ?falsevariant 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 thathrtime()*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.
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).
No, this behaves differently from theI'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:The same applies to the nanoseconds argument of your fromSeconds() Duration::fromSeconds($seconds)->add(Duration::fromNanoseconds($nanos))Duration::fromSeconds($seconds) ->add(Duration::fromMilliseconds($millis)) ->add(Duration::fromMicroseconds($micros)) ->add(Duration::fromNanoseconds($nanos));will work and do the right thing.
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.
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üsterhusFor the common case of “seconds + fractional seconds”, theThe 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.fromSeconds()constructor works (and going from fractional milliseconds to fractional nanoseconds is a*1_000_000operation that should be easy and obvious enough).