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