Hi TIm,

On Sat, Jul 4, 2026 at 12:14 AM Tim Düsterhus <[email protected]> wrote:
>
> Hi
>
> On 7/3/26 18:29, Pierre Joye wrote:
> > I like that new Duration, good new APIs. I was following the
> > discussion and one thing keeps coming to my mind as an API mistake:
> >
> >    public static function fromSeconds(int $seconds, int $nanoseconds = 0)
> >
> > Let me break down my thinking:
> >
> > fromSeconds implies I'm setting a duration in seconds. But there are
> > two arguments — the second is nanoseconds, and milliseconds isn't
> > representable at all. The name promises one unit and the signature
> > quietly provides another.
>
> Please see https://news-web.php.net/php.internals/131419; and the fourth
> paragraph in the “design considerations”.
>
> The $nanoseconds argument in the `fromSeconds()` constructor is
> intentionally limited to “the number of nanoseconds in a second” which
> means that the constructor can be reasoned about taking (fractional)
> seconds - the base unit of Durations - as a fixed point decimal. I do
> not consider this a break in the promise.

I do consider it breaks the promise. Additionally, it forces the user
to do mental calculation and remember an arbitrary bound of
999,999,999 that doesn't exist in any of the APIs you cited as
inspiration. Java's Duration.ofSeconds(long, long) explicitly
documents carrying excess nanoseconds into seconds with no cap,
Duration.ofSeconds(2, 1_000_000_001) produces the same value as
Duration.ofSeconds(3, 1). Rust's Duration::new does the same. Temporal
doesn't even carry by default. Temporal.Duration.from({ hours: 200,
minutes: 17 }) is valid and preserves both values as given. None of
the three reject or restrict a sub-unit's magnitude the way this
proposal does.

> > That's correct, but I don't think it applies here. The fromSeconds
> > issue is a naming defect regardless of language: the method name
> > promises "seconds" and the signature quietly also takes nanoseconds.
>
> As mentioned above, the $nanoseconds parameter is intentionally limited,
> making the constructor consistent with the other constructors in that
> the “unit that is mentioned in the name” may overflow into the larger
> units to allow construction with the desired precision in a way that
> makes sense for the given use case.
>
> Extending this pattern to e.g. `fromMinutes()` would result in
> `fromMinutes($minutes, $seconds = 0)` with $seconds being limited to 59.
>
> However in the other direction, `fromMilliseconds($milliseconds,
> $microseconds = 0)`, with $microseconds <= 999 would feel weird, because
> one wouldn’t say 5 milliseconds and 500 microseconds, when they can just
> move the comma and say 5500 microseconds.
>
> The seconds boundary is where the units shift from a “metric” factor of
> 1000 to a factor of 60. Thus the special handling.
>
> > PHP already supports named arguments, giving us the same clarity as
> > Temporal object literal without borrowing any JS syntax:
> >
> >    Duration::from(seconds: -30, nanoseconds: -500);
> >    Duration::from(seconds: -30, nanoseconds: 500); // error
> >    Duration::from(seconds: 30, nanoseconds: 500).negate();
> >
> > A single ::from() with named arguments would be unambiguous, easy to
> > remember, and remove the need for the whole family of from<Unit>
>
> A design goal of the constructors was that they are injective functions,
> i.e. there must be at most one way of creating a Duration with a
> specific length from any given constructor. This is in order to make it
> easy to reason about the resulting value, without needing to perform
> (mental) calculations.

If injectivity were the real goal, none of Java, Rust, or Temporal
would be usable APIs by that standard, they're explicitly
non-injective by design, and that hasn't stopped them from being the
reference implementations you're citing as inspiration.
Precision-preserving carry is the friendlier default; rejecting valid
input to protect a property the cited prior art doesn't itself protect
is what actually creates the mental overhead it is trying to avoid.

I also think having the from() would be an easy win from day 1, could
even allow string if no named argument for the ISO 8601 input.

cheers,
-- 
Pierre

@pierrejoye

Reply via email to