Skip to content

Commit 2758c6f

Browse files
committed
8368856: Add a method that performs saturating addition of a Duration to an Instant
Reviewed-by: naoto, rriggs, scolebourne
1 parent 5a2b0ca commit 2758c6f

File tree

2 files changed

+104
-0
lines changed

2 files changed

+104
-0
lines changed

src/java.base/share/classes/java/time/Instant.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,32 @@ public Instant plus(TemporalAmount amountToAdd) {
788788
return (Instant) amountToAdd.addTo(this);
789789
}
790790

791+
/**
792+
* Returns a copy of this instant with the specified duration added, with
793+
* saturated semantics.
794+
* <p>
795+
* If the result is "earlier" than {@link Instant#MIN}, this method returns
796+
* {@code MIN}. If the result is "later" than {@link Instant#MAX}, it
797+
* returns {@code MAX}. Otherwise it returns {@link #plus(TemporalAmount) plus(duration)}.
798+
*
799+
* @apiNote This method can be used to calculate a deadline from
800+
* this instant and a timeout. Unlike {@code plus(duration)},
801+
* this method never throws {@link ArithmeticException} or {@link DateTimeException}
802+
* due to numeric overflow or {@code Instant} range violation.
803+
*
804+
* @param duration the duration to add, not null
805+
* @return an {@code Instant} based on this instant with the addition made, not null
806+
*
807+
* @since 26
808+
*/
809+
public Instant plusSaturating(Duration duration) {
810+
if (duration.isNegative()) {
811+
return until(Instant.MIN).compareTo(duration) >= 0 ? Instant.MIN : plus(duration);
812+
} else {
813+
return until(Instant.MAX).compareTo(duration) <= 0 ? Instant.MAX : plus(duration);
814+
}
815+
}
816+
791817
/**
792818
* Returns a copy of this instant with the specified amount added.
793819
* <p>

test/jdk/java/time/tck/java/time/TCKInstant.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
import java.util.List;
107107
import java.util.Locale;
108108

109+
import java.util.Optional;
109110
import org.testng.annotations.BeforeMethod;
110111
import org.testng.annotations.DataProvider;
111112
import org.testng.annotations.Test;
@@ -1225,6 +1226,83 @@ public void plusNanos_long_overflowTooSmall() {
12251226
t.plusNanos(-1);
12261227
}
12271228

1229+
@DataProvider(name = "PlusSaturating")
1230+
Object[][] provider_plusSaturating() {
1231+
return new Object[][]{
1232+
// 1. {edge or constant instants} x {edge or constant durations}
1233+
{Instant.MIN, Duration.ofSeconds(Long.MIN_VALUE, 0), Optional.of(Instant.MIN)},
1234+
{Instant.MIN, Duration.ofSeconds(Long.MIN_VALUE, 0), Optional.of(Instant.MIN)},
1235+
{Instant.MIN, Duration.ZERO, Optional.empty()},
1236+
{Instant.MIN, Duration.ofSeconds(Long.MAX_VALUE, 999_999_999), Optional.of(Instant.MAX)},
1237+
{Instant.EPOCH, Duration.ofSeconds(Long.MIN_VALUE, 0), Optional.of(Instant.MIN)},
1238+
{Instant.EPOCH, Duration.ZERO, Optional.empty()},
1239+
{Instant.EPOCH, Duration.ofSeconds(Long.MAX_VALUE, 999_999_999), Optional.of(Instant.MAX)},
1240+
{Instant.MAX, Duration.ofSeconds(Long.MIN_VALUE, 0), Optional.of(Instant.MIN)},
1241+
{Instant.MAX, Duration.ZERO, Optional.empty()},
1242+
{Instant.MAX, Duration.ofSeconds(Long.MAX_VALUE, 999_999_999), Optional.of(Instant.MAX)},
1243+
// 2. {edge or constant instants} x {normal durations}
1244+
{Instant.MIN, Duration.ofDays(-32), Optional.of(Instant.MIN)},
1245+
{Instant.MIN, Duration.ofDays(32), Optional.empty()},
1246+
{Instant.EPOCH, Duration.ofDays(-32), Optional.empty()},
1247+
{Instant.EPOCH, Duration.ofDays(32), Optional.empty()},
1248+
{Instant.MAX, Duration.ofDays(-32), Optional.empty()},
1249+
{Instant.MAX, Duration.ofDays(32), Optional.of(Instant.MAX)},
1250+
// 3. {normal instants with both positive and negative epoch seconds} x {edge or constant durations}
1251+
{Instant.parse("1950-01-01T00:00:00Z"), Duration.ofSeconds(Long.MIN_VALUE, 0), Optional.of(Instant.MIN)},
1252+
{Instant.parse("1950-01-01T00:00:00Z"), Duration.ZERO, Optional.empty()},
1253+
{Instant.parse("1950-01-01T00:00:00Z"), Duration.ofSeconds(Long.MAX_VALUE, 999_999_999), Optional.of(Instant.MAX)},
1254+
{Instant.parse("1990-01-01T00:00:00Z"), Duration.ofSeconds(Long.MIN_VALUE, 0), Optional.of(Instant.MIN)},
1255+
{Instant.parse("1990-01-01T00:00:00Z"), Duration.ZERO, Optional.empty()},
1256+
{Instant.parse("1990-01-01T00:00:00Z"), Duration.ofSeconds(Long.MAX_VALUE, 999_999_999), Optional.of(Instant.MAX)},
1257+
// 4. {normal instants with both positive and negative epoch seconds} x {normal durations}
1258+
{Instant.parse("1950-01-01T00:00:00Z"), Duration.ofDays(-32), Optional.empty()},
1259+
{Instant.parse("1950-01-01T00:00:00Z"), Duration.ofDays(32), Optional.empty()},
1260+
{Instant.parse("1990-01-01T00:00:00Z"), Duration.ofDays(-32), Optional.empty()},
1261+
{Instant.parse("1990-01-01T00:00:00Z"), Duration.ofDays(32), Optional.empty()},
1262+
// 5. instant boundary
1263+
{Instant.MIN, Duration.between(Instant.MIN, Instant.MAX), Optional.of(Instant.MAX)},
1264+
{Instant.EPOCH, Duration.between(Instant.EPOCH, Instant.MAX), Optional.of(Instant.MAX)},
1265+
{Instant.EPOCH, Duration.between(Instant.EPOCH, Instant.MIN), Optional.of(Instant.MIN)},
1266+
{Instant.MAX, Duration.between(Instant.MAX, Instant.MIN), Optional.of(Instant.MIN)}
1267+
};
1268+
}
1269+
1270+
@Test(dataProvider = "PlusSaturating")
1271+
public void plusSaturating(Instant i, Duration d, Optional<Instant> value) {
1272+
var actual = i.plusSaturating(d);
1273+
try {
1274+
assertEquals(actual, i.plus(d));
1275+
// If `value` is present, perform an additional check. It may be
1276+
// important to ensure that not only does the result of `plusSaturating`
1277+
// match that of `plus`, but that it also matches our expectation.
1278+
// Because if it doesn’t, then the test isn’t testing what we think
1279+
// it is, and needs to be fixed.
1280+
value.ifPresent(instant -> assertEquals(actual, instant));
1281+
} catch (DateTimeException /* instant overflow */
1282+
| ArithmeticException /* long overflow */ e) {
1283+
if (value.isEmpty()) {
1284+
throw new AssertionError();
1285+
}
1286+
assertEquals(actual, value.get());
1287+
}
1288+
}
1289+
1290+
@DataProvider(name = "PlusSaturating_null")
1291+
Object[][] provider_plusSaturating_null() {
1292+
return new Object[][]{
1293+
{Instant.MIN},
1294+
{Instant.EPOCH},
1295+
{Instant.MAX},
1296+
// any non-random but also non-special instant
1297+
{Instant.parse("2025-10-13T20:47:50.369955Z")},
1298+
};
1299+
}
1300+
1301+
@Test(expectedExceptions = NullPointerException.class, dataProvider = "PlusSaturating_null")
1302+
public void test_plusSaturating_null(Instant i) {
1303+
i.plusSaturating(null);
1304+
}
1305+
12281306
//-----------------------------------------------------------------------
12291307
@DataProvider(name="Minus")
12301308
Object[][] provider_minus() {

0 commit comments

Comments
 (0)