RFR: 8368856: Add a method to safely add duration to instant

Pavel Rappo prappo at openjdk.org
Mon Sep 29 16:22:08 UTC 2025


On Mon, 29 Sep 2025 13:55:35 GMT, Pavel Rappo <prappo at openjdk.org> wrote:

> We recently [discussed] the possibility of introducing saturating arithmetic for deadline computation. Consider this PR as a starting point. Once we agree on the implementation, I'll file a CSR.
> 
> I created a method in `Instant` to add `Duration`. One could argue that the proper way would be to go all the way and create a method in `Temporal` to add `TemporalAmount`. Or maybe even expand the functionality, and create an additional method in `Temporal` to subtract `TemporalAmount`.
> 
> My current thinking is that if we were to do that, there would be a lot of expensive, unused code. Saturating logic seems to be only useful for `Instant` and `Duration`.
> 
> Even if we decide to extend `Temporal` to add/subtract `TemporalAmount`, it could always be done later. From the perspective of `Instant`, `plus(TemporalAmount)` will be just an overload of `plus(Duration)`.
> 
> [discussed]: https://mail.openjdk.org/pipermail/core-libs-dev/2025-September/151098.html

Here's a related issue I discovered while implementing this PR's functionality.

It seems that for any `Instant i` (1) and (2) should be equivalent:

    i.plus(Duration.between(i, Instant.MAX));   // (1)
    i.minus(Duration.between(Instant.MAX, i));  // (2)

Well, they aren't: (1) results in `Instant.MAX` as expected, whereas (2) sometimes throws `DateTimeException`, indicating instant overflow.

Mind you, `Duration` can address values larger than `Duration.between(Instant.MIN, Instant.MAX)`, so in (1) and (2) `Duration` never overflows and this property holds for any `i`:

    Duration.between(i, Instant.MAX)
           .equals(Duration.between(Instant.MAX, i).negated())

So, why does (2) overflow? What happens is this:

* If `Duration.nanos` are non-zero, negative seconds are farther away from 0 than the equivalent positive seconds are.

  For example, this

        var pos = Duration.ofSeconds(1, 1);
        var neg = pos.negated();
        System.out.println(pos.getSeconds() + ", " + pos.getNano());
        System.out.println(neg.getSeconds() + ", " + neg.getNano());

  outputs

        1, 1
        -2, 999999999

* `minus` first operates on seconds disregarding nanos

So (2) overflows on seconds even if its intended, compound result doesn't. However, if you carefully select `i`, then no overflow will happen, and both (1) and (2) will result in `Instant.MAX`. For example,

    Instant i = Instant.ofEpochSecond(0, 999_999_999);

This issue somewhat reminded me of the difference between `Math.sqrt(x*x + y*y)` and [`Math.hypot(x, y)`][].

[`Math.hypot(x, y)`]: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Math.html#hypot(double,double)

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

PR Comment: https://git.openjdk.org/jdk/pull/27549#issuecomment-3347925868


More information about the core-libs-dev mailing list