[PHP-DEV] [RFC] Duration class

Hey Tim,

On 30.06.26 19:04, Tim Düsterhus wrote:

To remain consistent with `bcdivmod()`, the function should return a 2-tuple (i.e. `array{0: int, 1: Duration}`). This makes destructuring very convenient:

\[$fullDurations, $remainder\] = $oneHour\->divideInto\($sevenMinutes\);

see php.internals: Re: [RFC] [Discussion] Add bcdivmod to BCMath for the previous discussion.

I don't plan to add `divideInto()` to the initial set of methods in PHP 8.6, but would be open to it if anyone feels having it right away is useful or necessary. Please speak up in that case. And please also do if you disagree with the divideBy() + divideInto() pair.

Best regards
Tim Düsterhus

This is a new OOP API, maybe we could prioritise DX over consistency with `bcdivmod()`? Can we please return a value object like "DurationDivision"?

final readonly class DurationDivision
{
     public int $factor; // or "quotient"
     public Duration $remainder;
}

$divisionResult = $oneHour->divideInto($sevenMinutes);

$divisionResult->factor // unlike in destructuring, fully optional
$divisionResult->remainder->seconds

Although you call the destructuring very convenient, I personally think it makes a more awkward API. Would love and prefer to see an VO here!

Nits:
- while not incorrect, I find `Time` for the namespace not ideal. As others proposed, I'd also rather go with "Temporal".
- in the "Proposal" section example of the RFC text the `readonly` are redundantly on the class and properties; in the later example you only have it on the properties.

--

Cheers
Nick

Hi

Am 2026-06-23 15:04, schrieb Tim Düsterhus:

Both Derick and I were receptive to the `Duration::sum()` suggestion.

One big benefit I am personally seeing is that it would allow to rename the `negate()`, `multiplyBy()`, and `divideBy()` methods to use “past tense” (is that the correct term?) to indicate that they return a copy, without requiring awkward naming like `added()`. `->sub()` is also technically redundant due to the existence of negative Durations, thus there is no need for a mirrored pair like multiplication / division, since the information can be encoded in the Duration object itself.

To avoid readers making assumptions, can you spell out your arguments against replacing `->add()` and `->sub()` by a unified `::sum()` method?

To close this out ourselves: While having a static `sum()` method works for `Duration`s, since the Duration with addition forms an abelian group, this doesn't work for the other future-scope classes where addition doesn't form an (abelian) group. More specifically: Adding two Instants together is not a meaningful operation. Adding a Duration to an Instant however is. Using something like:

     $epoch = Instant::fromUnixtime(0);
     $onePointFiveHoursLater = Instant::sum($epoch, Duration::fromMinutes(30), Duration::fromHours(1));

would be very weird. Instead `->add()` is the obvious operation here:

     $epoch = Instant::fromUnixtime(0);
     $onePointFiveHoursLater = $epoch
         ->add(Duration::fromMinutes(30))
         ->add(Duration::fromHours(1));

notably this would also work then:

     $epoch = Instant::fromUnixtime(0);
     $onePointFiveHoursLater = $epoch
         ->add(
             Duration::fromMinutes(30)
                ->add(Duration::fromHours(1)
         );

making the Instant + Duration -> Instant operation a “right group action”.

We will thus keep instance methods for “addition” and “subtraction”. If someone has a suggestion regarding naming (particularly with regard to negate() vs negated() and multiplyBy() and multipliedBy()), I'll be happy to discuss that. Java uses plus, minus, multipliedBy, dividedBy and negated. Golang is just using an integer (meaning you just use the operators). Rust’s std::time::Duration uses add, sub, mul, div (with saturating and checked variants), but also overloads operators. Rust’s chrono::TimeDelta is similar to std::time::Duration, but doesn't have the saturating variants. JavaScript’s Temporal uses add, sub, negated and doesn't support multiplication and division.

Please keep the `divide**By**()` (Duration / int -> Duration) vs `divide**Into**()` (Duration / Duration -> (int, Duration)) suggestion in mind when having opinions about the naming. See: php.internals: Re: [RFC] Duration class

Best regards
Tim Düsterhus

On Wed, Jul 1, 2026, at 12:22 PM, Tim Düsterhus wrote:

Hi

Am 2026-06-23 15:04, schrieb Tim Düsterhus:

Both Derick and I were receptive to the `Duration::sum()` suggestion.

One big benefit I am personally seeing is that it would allow to rename
the `negate()`, `multiplyBy()`, and `divideBy()` methods to use “past
tense” (is that the correct term?) to indicate that they return a copy,
without requiring awkward naming like `added()`. `->sub()` is also
technically redundant due to the existence of negative Durations, thus
there is no need for a mirrored pair like multiplication / division,
since the information can be encoded in the Duration object itself.

To avoid readers making assumptions, can you spell out your arguments
against replacing `->add()` and `->sub()` by a unified `::sum()`
method?

To close this out ourselves: While having a static `sum()` method works
for `Duration`s, since the Duration with addition forms an abelian
group, this doesn't work for the other future-scope classes where
addition doesn't form an (abelian) group. More specifically: Adding two
Instants together is not a meaningful operation. Adding a Duration to an
Instant however is. Using something like:

     $epoch = Instant::fromUnixtime(0);
     $onePointFiveHoursLater = Instant::sum($epoch,
Duration::fromMinutes(30), Duration::fromHours(1));

would be very weird. Instead `->add()` is the obvious operation here:

     $epoch = Instant::fromUnixtime(0);
     $onePointFiveHoursLater = $epoch
         ->add(Duration::fromMinutes(30))
         ->add(Duration::fromHours(1));

notably this would also work then:

     $epoch = Instant::fromUnixtime(0);
     $onePointFiveHoursLater = $epoch
         ->add(
             Duration::fromMinutes(30)
                ->add(Duration::fromHours(1)
         );

making the Instant + Duration -> Instant operation a “right group
action”.

How about that. Forgetting to respond to this thread meant you gave a good response for me. Thanks, Tim. :slight_smile:

We will thus keep instance methods for “addition” and “subtraction”. If
someone has a suggestion regarding naming (particularly with regard to
negate() vs negated() and multiplyBy() and multipliedBy()), I'll be
happy to discuss that. Java uses plus, minus, multipliedBy, dividedBy
and negated. Golang is just using an integer (meaning you just use the
operators). Rust’s std::time::Duration uses add, sub, mul, div (with
saturating and checked variants), but also overloads operators. Rust’s
chrono::TimeDelta is similar to std::time::Duration, but doesn't have
the saturating variants. JavaScript’s Temporal uses add, sub, negated
and doesn't support multiplication and division.

Please keep the `divide**By**()` (Duration / int -> Duration) vs
`divide**Into**()` (Duration / Duration -> (int, Duration)) suggestion
in mind when having opinions about the naming. See:
php.internals: Re: [RFC] Duration class

negated() would be consistent with the "ed means return new" pattern found in many languages, so I would strongly recommend following that pattern.

For the others, my inclination is to go short (add, sub) for easier typing and a more compact result, but that's not a make-or-break issue for me.

--Larry Garfield

On 2026-07-02 05:22, Tim Düsterhus wrote:

To close this out ourselves: While having a static `sum()` method works for `Duration`s, since the Duration with addition forms an abelian group, this doesn't work for the other future-scope classes where addition doesn't form an (abelian) group. More specifically: Adding two Instants together is not a meaningful operation. Adding a Duration to an Instant however is.

If you want the appropriate mathematical structure, the timeline models a one-dimensional real affine space, in which Durations are vectors and Instants are points; it's a vector space that has forgotten its origin (the choice of epoch is arbitrary and not a distinguished point of the space - as evidenced by how many of them there are).

Hi

Am 2026-07-02 03:53, schrieb Morgan:

If you want the appropriate mathematical structure, the timeline models a one-dimensional real affine space, in which Durations are vectors and Instants are points; it's a vector space that has forgotten its origin (the choice of epoch is arbitrary and not a distinguished point of the space - as evidenced by how many of them there are).

Thank you. I genuinely enjoyed being outnerded here :slight_smile:

Any suggestions regarding naming from your side?

Best regards
Tim Düsterhus

Hi

On 2026-07-01 20:18, Larry Garfield wrote:

We will thus keep instance methods for “addition” and “subtraction”. If
someone has a suggestion regarding naming (particularly with regard to
negate() vs negated() and multiplyBy() and multipliedBy()), I'll be
happy to discuss that. Java uses plus, minus, multipliedBy, dividedBy
and negated. Golang is just using an integer (meaning you just use the
operators). Rust’s std::time::Duration uses add, sub, mul, div (with
saturating and checked variants), but also overloads operators. Rust’s
chrono::TimeDelta is similar to std::time::Duration, but doesn't have
the saturating variants. JavaScript’s Temporal uses add, sub, negated
and doesn't support multiplication and division.

Please keep the `divide**By**()` (Duration / int -> Duration) vs
`divide**Into**()` (Duration / Duration -> (int, Duration)) suggestion
in mind when having opinions about the naming. See:
php.internals: Re: [RFC] Duration class

negated() would be consistent with the "ed means return new" pattern found in many languages, so I would strongly recommend following that pattern.

Yes. The only issue I'm having with that is that it doesn't trivially transfer to addition and subtraction, which technically would be an inconsistency in the API. Maybe that's okay, particularly given that both Java and JavaScript have the -ed for everything exception addition and subtraction, but I nevertheless want to have the discussion.

For the others, my inclination is to go short (add, sub) for easier typing and a more compact result, but that's not a make-or-break issue for me.

Having slept over it, I would prefer the full `subtract()`:

Our naming policy mentions to that “Abbreviations and acronyms as well as initialisms SHOULD be avoided wherever possible” (policies/coding-standards-and-naming.rst at main · php/policies · GitHub).

------------

To further complicate matters, the relatively new BcMath\Number class goes with add, sub, mul, div. Given that we need two division operations here, we’ll need the suffix and `divBy` and `divInto` don‘t really roll off the tongue well, particularly the latter :expressionless:

Best regards
Tim Düsterhus

On Thu, Jul 2, 2026, at 1:30 PM, Tim Düsterhus wrote:

negated() would be consistent with the "ed means return new" pattern
found in many languages, so I would strongly recommend following that
pattern.

Yes. The only issue I'm having with that is that it doesn't trivially
transfer to addition and subtraction, which technically would be an
inconsistency in the API. Maybe that's okay, particularly given that
both Java and JavaScript have the -ed for everything exception addition
and subtraction, but I nevertheless want to have the discussion.

From what I've seen in my collections research, the -ed suffix is used in cases where it's not obvious from the method name. (For some definition of obvious.) For instance, append() tends to be an operate-in-place, and concat() produces-new. But neither have an -ed on them. Things like sort[ed]() or reverse[d]() do, and often have both versions on the same object.

So I think a reasonable guideline is "if it's self-evident, don't bother; if it's confusing, use -ed for the return-new immutable one." For some squishy definition of self-evident. In this case, it feels self-evident to me that add() will return-new, as that's how I am used to addition working. incrementBy() I would expected to modify-in-place. negate() is potentially confusing as it could go either way, so I would recommend the -ed suffix.

--Larry Garfield

On 2026-07-03 03:27, Tim Düsterhus wrote:

Any suggestions regarding naming from your side?

Best regards
Tim Düsterhus

"translate" would be too abstract, obviously.

Have "offset" or "shift" been suggested?

"offset" is also a noun so would need to be "offsetBy" or "withOffset"; the former is still a bit ambiguous, and while the latter I think clearly returns a new Duration, it suggests there's some kind of distinct "Offset" entity.

"shift"/"shifted" - perhaps with a "By" to make reading flow more naturally - gets the translation idea across I think: we're moving an Instant from _here_ to _there_ by _this_ amount. Larry has already commented on the connotations of the "-ed" suffix: the former is active, the latter is passive; one changes the object it's applied to, the other returns a new one.

(If there's any debate on that, my preference would be for immutable objects: having an object _here_ change because of something done to it over _there_ ... such communication should be done out in the open and not hidden inside arbitrary values.)

(And that active/passive tension is often a thing in coordinate transforms - are we changing the object and seeing the results in our reference frame, or are we changing our reference frame and just changing our description of the object?)

Hi

On 6/29/26 19:42, Tim Düsterhus wrote:

Thank you for providing that example. The restriction was primarily
added as a safety: Removing restrictions later is always possible,
adding them is not. So absent a good use-case choosing the “error”
option is safer. Given you have provided an example, it makes sense to
me to lift the restriction.

For the `fromSeconds()` constructor it's not completely obvious how to
handle negative values there (see the discussion with Marc). I think
requiring both parameters to have the same sign would be the correct
solution here, since this will provide for an intuitive representation
of magnitude (see also below). What do you think?

I'll also discuss this with Derick before adjusting the RFC.

I have now discussed this with Derick. He said that he would prefer keeping the restriction for now, particularly since the `fromSeconds()` constructor is not totally obvious.

But as mentioned in my previous email, this is something we can revisit later, for example when adding the “Future Scope” classes, before that happens, negative Durations are not particularly useful anyways.

Given the small remaining time until the freeze, I'm going to “close out” this thread, but it will definitely serve as a useful reference in the future. Thanks again!

Best regards
Tim Düsterhus

Hi

On 6/23/26 14:12, Tim Düsterhus wrote:

Am 2026-06-18 15:47, schrieb Tim Düsterhus:

All that said, you can find the RFC at:
PHP: rfc:duration_class. It hopefully includes all the
important explanation and also provides a rationale as to why we made
the design decisions we made.

Derick and I had a first discussion about the feedback so far and I have
made the following changes to the RFC earlier today.

Regarding the “method naming question” as the last remaining issue, I've also made a quick informal poll in PHP Foundation Slack and then discussed the topic with Derick:

`add` and `sub` were in unanimous agreement. So we go with those.

For the other “mathematical operations”, there was a non-trivial number of folks in favor of abbreviated names (for consistency with BCMath and GMP). In favor of the -ed suffix was only Larry, who also replied on list. Given that the class is clearly marked as `readonly`, and that, following Larry’s reasoning, mathematical operations are generally not working in-place, we decided not to consider the -ed suffix, since it would not consistently work (e.g. ->absoluted() as a companion to ->negated() doesn't make sense).

Since the naming policy says that abbreviations should be avoided, but can be okay if accepted by RFC vote, we decided to actually make it a vote:

We added a secondary vote between the current “full” names (multiplyBy, divideBy, negate, absolute) and abbreviated names (mul, divBy, neg, abs).

In accordance with policy (policies/feature-proposals.rst at main · php/policies · GitHub), this secondary vote will be “biased” in favor of the full names, matching the original and current proposal (and the non-abbreviation naming suggested by the naming policy). Specifically, the abbreviated names will require a 2/3 majority. If 4 folks vote in favor of full names, and 8 in favor of abbreviations, it will be abbreviations. If it's 4 in favor of full names, and 7 in favor of abbreviations it will be full names.

-----------

From what we see all open questions for a “minimally viable API” in PHP 8.6 should be resolved with this change and all possible “Future Scope” be properly taken into account so we won’t regret anything in the future. Fingers crossed!

With the feature freeze in the middle of August and the required 14 day cooldown after making major changes to RFCs, we have a few more days to make any finishing touches, but we would consider the RFC to be complete and don’t plan to make any more changes.

Personally I'm super happy with how insightful and productive the discussion was. I'm really looking forward to seeing you all again for the upcoming “Future Scope” discussions for PHP 8.7 :slight_smile:

Best regards
Tim Düsterhus

Hi

On 7/1/26 04:27, Nick Sdot wrote:

This is a new OOP API, maybe we could prioritise DX over consistency
with `bcdivmod()`? Can we please return a value object like
"DurationDivision"?

divideInto() will be left to future scope, so let’s have that discussion then :slight_smile:

Nits:
- while not incorrect, I find `Time` for the namespace not ideal. As
others proposed, I'd also rather go with "Temporal".

I'll just refer to my previous email here: php.internals: Re: [RFC] Duration class

- in the "Proposal" section example of the RFC text the `readonly` are
redundantly on the class and properties; in the later example you only
have it on the properties.

Good catch, that was a mistake. I've adjusted the proof of concept implementation to add the `readonly` on the class. I've kept the redundant `readonly` on the properties to be extra explicit.

Best regards
Tim Düsterhus

Hi

On 7/3/26 17:52, Tim Düsterhus wrote:

We added a secondary vote between the current “full” names (multiplyBy,
divideBy, negate, absolute) and abbreviated names (mul, divBy, neg, abs).

What I planned to mention, but forgot: I've also added a full “Naming of the mathematical operations” section providing background information and arguments regarding the secondary vote:

Best regards
Tim Düsterhus

Hi TIm,

On Fri, Jul 3, 2026 at 9:10 PM Tim Düsterhus <tim@bastelstu.be> wrote:

Hi

On 6/29/26 19:42, Tim Düsterhus wrote:
> Thank you for providing that example. The restriction was primarily
> added as a safety: Removing restrictions later is always possible,
> adding them is not. So absent a good use-case choosing the “error”
> option is safer. Given you have provided an example, it makes sense to
> me to lift the restriction.
>
> For the `fromSeconds()` constructor it's not completely obvious how to
> handle negative values there (see the discussion with Marc). I think
> requiring both parameters to have the same sign would be the correct
> solution here, since this will provide for an intuitive representation
> of magnitude (see also below). What do you think?
>
> I'll also discuss this with Derick before adjusting the RFC.

I have now discussed this with Derick. He said that he would prefer
keeping the restriction for now, particularly since the `fromSeconds()`
constructor is not totally obvious.

But as mentioned in my previous email, this is something we can revisit
later, for example when adding the “Future Scope” classes, before that
happens, negative Durations are not particularly useful anyways.

Given the small remaining time until the freeze, I'm going to “close
out” this thread, but it will definitely serve as a useful reference in
the future. Thanks again!

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.

Someone mentioned the Temporal API for JavaScript, which makes
Duration construction unambiguous. You mentioned:

"I do not believe it is possible to just take an API that was built
for a different programming language and plug it into a different
programming language without making any changes. Different programming
languages have different capabilities, ecosystems and code styles and
without adjusting the API to the respective language, it will just
stick out like a sore thumb."

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.
This isn't a JS-vs-PHP idiom question.

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>
statics. If we'd rather keep fromSeconds, fromMinutes, etc., they
should each accept only what their name promises, nothing else. This
is purely arginfo-driven, so it costs nothing on the implementation
side — no C changes required.

This is also the last point where the shape of from* can change
without a BC break: once released, splitting or consolidating them
means a deprecation cycle spanning multiple major versions.

btw, also you mentioned Java's time package as the closest thing to
DatetimeImmutable etc. I find that package a pain to use for 99% of
usages, if not 100%. It is not that I want to say php is not java.
However, in the current specific situation, I would totally mention
that :slight_smile:

Cheers,

--
Pierre

@pierrejoye

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 php.internals: Re: [RFC] Duration class; 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.

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.

i.e. Duration::from(seconds: 1, milliseconds: 1500) would misleadingly look “between 1 and 2 seconds” at a glance. But when each of the “subunits” are restricted in magnitude, it would be very inconvenient to construct values with the appropriate precision, because it would require splitting inputs across the different parameters.

btw, also you mentioned Java's time package as the closest thing to
DatetimeImmutable etc. I find that package a pain to use for 99% of
usages, if not 100%. It is not that I want to say php is not java.
However, in the current specific situation, I would totally mention
that :slight_smile:

The Duration class is taking inspiration from Java, Rust, Golang, and JavaScript, but is not a direct copy of any of these. I would expect the same from any future scope additions to the new “time and date” API.

I'm also confident that we will manage to build an API that stay true to PHP being a pragmatic language, while at the same time being correct and powerful enough to correctly model complex use cases.

Best regards
Tim Düsterhus

Hi Tim,

On 03.07.26 19:14, Tim Düsterhus 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 php.internals: Re: [RFC] Duration class; 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.

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.

i.e. Duration::from(seconds: 1, milliseconds: 1500) would misleadingly look “between 1 and 2 seconds” at a glance. But when each of the “subunits” are restricted in magnitude, it would be very inconvenient to construct values with the appropriate precision, because it would require splitting inputs across the different parameters.

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

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
* using JS (even Temporal) exposing integer values as "number" often ends up as float in PHP

I totally understand that you want to keep it simple for now but the $nanoseconds second argument makes it impossible to change in the future.

I like the idea of a general "from" constructor - but not restricted to seconds + nanos but ALL supported units

public static function from(
int $hours = 0,
int $minutes = 0,
int $seconds = 0,
int $milliseconds = 0,
int $microseconds = 0,
int $nanoseconds = 0,
) : Duration

This makes it much more flexible, simpler to work with and extensible if we want to increase the precision any time in the future.
It's intentional to start with the most uncommon duration unit of $hours. That forces you to use named arguments which reads nicely and handling floats or not can be discussed at a later time.

Duration::from(seconds: 100, microseconds: 500);
Duration::from(hours: 5, minutes: 30);

$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

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

It makes it simple to reason about for you as implementing it - it forces the burden to the one using the API.

Regards,
Marc

On 24.06.26 13:03, Tim Düsterhus wrote:

Hi

Am 2026-06-23 08:19, schrieb marc@mabe.berlin:

What you feel unintuitive is just addition of two numbers in different units "$seconds + $nanoseconds * 1_000_000_000", which for me feels very natural.

No, what I feel is unintuitive is that the magnitude of the Duration will because *smaller* when the magnitude of the nanosecond value becomes *larger*. You could phrase it as “the representation of a single value as two values with different signs is unintuitive”. The different units are not the issue, the different sign is.

With the negative flag it's more complicated "($negative ? -1 : 1) * $seconds + $nanoseconds * 1_000_000_000" to obtain the correct meaning.

This example snippet is incorrect. The correct version would be: `($negative ? -1 : 1) * ($seconds + $nanoseconds * 1_000_000_000)` (with parentheses). In fact this snippet includes the snippet you consider to be “very natural”, it just makes the application of the sign explicit and effectively applies it to both components (instead of one component like Java does), which means that the magnitude of `$seconds` is a correct lower bound for the magnitude of the entire Duration.

To give a specific example in Java (Online Java Compiler - Programiz):

import java\.time\.Duration;

class Main \{
    public static void main\(String\[\] args\) \{
        Duration d = Duration\.parse\(&quot;PT\-59\.5S&quot;\);
        System\.out\.println\(d\.getSeconds\(\)\);
        System\.out\.println\(d\);
    \}
\}

This will print:

\-60
PT\-59\.5S

Just by looking at the “seconds” component it looks like the Duration is at least 1 minute long. But it isn't, because if the second component is negative, the larger the value of the nanosecond component is, the shorter the duration will be.

An alternative would be to use the same sign for nanoseconds as for seconds (while 0 seconds is neither positive or negative)

1.5s = 1 second + 500_000_000 nanoseconds
-1.5s = -1 seconds + -500_000_000 nanoseconds
-0.5s = 0 seconds + -500_000_000 nanoseconds

No negative flag needed to be handled separately.

In the RFC you say

Negative durations are represented by an explicit $negative property. This makes it easy to deal with absolute values by just ignoring the value of $negative.

That's just not true - you can't simply ignore the sign you have to deal with it no matter what. it's changing your calculations, it'g getting rejected on passing it to other functions.
On the same time the current API makes it harder to deal with negative durations as they can not be constructed directly and are not allowed as operator arguments.

The statement is correct, if you are interested in absolute values, you have the magnitude right there and can ignore the `$negative` flag. But to make it even easier, we added the `->absolute()` method that just clears the sign. Simply replacing seconds by abs(seconds) in Java's representation would *not* be correct. In fact the OpenJDK implementation defers to a BigDecimal calculation to flip the sign, which is not an option for PHP, because the minimal PHP build doesn't include either GMP or bcmath.

"... if you are interested in absolute values, you have the magnitude right there and can ignore the `$negative` flag."

No I can't
* adding two durations does NOT guaranties a value greater than the two
* passing a negative duration as timeout might throw
* ...

Thanks for adding `absolute()` - that's very helpful

Java does not have property hooks (as far as I know) but PHP now has.
That's very powerful and makes it possible to hide implementation details without exposing everything as getters.

$duration->seconds // 0-59
$duration->totalSeconds
$duration->nanoseconds // 0 - 999_999_999
$duration->totalNanoseconds
$duration->milliseconds // 0 - 999
$duration->totalMilliseconds
// maybe later
$duration->picoseconds // 0 - 999_999_999_999
$duration->totalPicosecond

As a user you don't need to care that much of the internal representation.

The currently proposed properties are intended to be used by the user directly. That's why they are public.

This does not answer the meaning difference of `seconds` vs. `nanoseconds` - it's confusing naming that `seconds` is the total amount of seconds in this duration but `nanoseconds` is the number of fractions of a second.

Best regards
Tim Düsterhus

On 03.07.26 16:04, Tim Düsterhus wrote:

Given the small remaining time until the freeze, I'm going to “close out” this thread, but it will definitely serve as a useful reference in the future. Thanks again!

Please don't rush on it - surely the current benefit is not worth it.

Best regards
Tim Düsterhus

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.

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.

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.

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

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

It makes it simple to reason about for you as implementing it - it
forces the burden to the one using the API.

The quoted paragraph is speaking from the perspective of a user using the API. In the internal implementation just ignoring component overflow would be easier than checking for it.

Best regards
Tim Düsterhus

Hi

On 7/3/26 22:45, Marc B. wrote:

An alternative would be to use the same sign for nanoseconds as for
seconds (while 0 seconds is neither positive or negative)

Yes, this is effectively mentioned in the “Design Considerations” section of the RFC.

"... if you are interested in absolute values, you have the magnitude
right there and can ignore the `$negative` flag."

No I can't
* adding two durations does NOT guaranties a value greater than the two
* passing a negative duration as timeout might throw
* ...

The use cases you listed there are all passing the Duration object somewhere else, which I do not consider to be a case of “if you are interested […]”.

The quoted sentence if referring to situations where your code is the end user of the Duration object and extracts the contents from it for further processing. And in those situations a sign-magnitude representation makes processing easier, because you have a single authoritative source of the sign, not one in each component and because you can extract the magnitude right away, without needing to flip the sign of each component.

This does not answer the meaning difference of `seconds` vs.
`nanoseconds` - it's confusing naming that `seconds` is the total amount
of seconds in this duration but `nanoseconds` is the number of fractions
of a second.

A value-object, such as Duration, is uniquely described by the sum of its properties. And in this case, it's quite literally the sum of the $seconds and $nanoseconds.

I'm afraid I don't see how one would get the assumption that $seconds would redundantly be equal to ($nanoseconds / 1_000_000_000) or something like that when the object only has $seconds and $nanoseconds properties without also having $milliseconds, $minutes, or something else. The confusion would also be quickly cleared up just by looking at the object and seeing that the $nanoseconds value is always smaller than 1 billion.

In my opinion renaming the $nanoseconds to $fractionalNanoseconds would greatly decrease the ergonomics of the class.

Best regards
Tim Düsterhus

Hi

On 7/3/26 22:48, Marc B. wrote:

Given the small remaining time until the freeze, I'm going to “close
out” this thread, but it will definitely serve as a useful reference
in the future. Thanks again!

Please don't rush on it - surely the current benefit is not worth it.

The paragraph you are replying to is referring to Paul’s suggestion of allowing negative scalars in the constructors, a limitation that can be lifted in the future without introducing a breaking change.

------

Besides being confident in the basic design of the class, which is much older than the RFC - I first discussed the idea with Derick on PHPDay this year, and had a design lying around for even longer - I also consider the benefit of having a better “Io\Poll” API to be worth trying to hit a deadline.

Best regards
Tim Düsterhus