Durations in existing JDK APIs
Martin Buchholz
martinrb at google.com
Mon Jun 4 21:47:31 UTC 2018
TimeUnit#toDuration is gone.
Here's a non-strawman attempt at TimeUnit#convert(Duration).
I was astonished how hard it was to get this really correct instead of
merely mostly correct.
For negative numbers, the long/int representation of Duration is sort-of a
"floored" division instead of java's default "truncating" division.
/**
* Converts the given time duration to this unit.
*
* <p>For any TimeUnit {@code unit},
* {@code unit.convert(Duration.ofNanos(n))}
* is equivalent to
* {@code unit.convert(n, NANOSECONDS)}, and
* {@code unit.convert(Duration.of(n, unit.toChronoUnit()))}
* is equivalent to {@code n} (in the absence of overflow).
*
* @param duration the time duration
* @return the converted duration in this unit,
* or {@code Long.MIN_VALUE} if conversion would negatively overflow,
* or {@code Long.MAX_VALUE} if it would positively overflow.
* @throws NullPointerException if {@code duration} is null
* @see Duration#of(long,TemporalUnit)
* @since 11
*/
public long convert(Duration duration) {
final long secs = duration.getSeconds();
final int nano = duration.getNano();
final long s, candidate;
if ((s = scale) == SECOND_SCALE)
return (secs < 0 && nano > 0) ? secs + 1 : secs;
if (s > SECOND_SCALE) {
long div = secs / secRatio;
if (secs < 0 && nano > 0 && div * secRatio == secs)
div++;
return div;
}
return (secs >= 0)
? ((secs > maxSecs
|| (candidate = secs * secRatio + nano / s) < 0)
? Long.MAX_VALUE : candidate)
: ((secs < - maxSecs - 1
|| (candidate = secs * secRatio + (nano + s - 1) / s) > 0)
? Long.MIN_VALUE : candidate);
}
But is it really correct? Here are some tests as well:
/**
* convert(Duration) roundtrips with Duration.ofXXXX and
Duration.of(long, ChronoUnit)
*/
public void testConvertDuration_roundtripDurationOf() {
long n = ThreadLocalRandom.current().nextLong();
assertEquals(n, NANOSECONDS.convert(Duration.ofNanos(n)));
assertEquals(n, NANOSECONDS.convert(Duration.of(n,
ChronoUnit.NANOS)));
assertEquals(n, MILLISECONDS.convert(Duration.ofMillis(n)));
assertEquals(n, MILLISECONDS.convert(Duration.of(n,
ChronoUnit.MILLIS)));
assertEquals(n, SECONDS.convert(Duration.ofSeconds(n)));
assertEquals(n, SECONDS.convert(Duration.of(n,
ChronoUnit.SECONDS)));
n /= 60;
assertEquals(n, MINUTES.convert(Duration.ofMinutes(n)));
assertEquals(n, MINUTES.convert(Duration.of(n,
ChronoUnit.MINUTES)));
n /= 60;
assertEquals(n, HOURS.convert(Duration.ofHours(n)));
assertEquals(n, HOURS.convert(Duration.of(n, ChronoUnit.HOURS)));
n /= 24;
assertEquals(n, DAYS.convert(Duration.ofDays(n)));
assertEquals(n, DAYS.convert(Duration.of(n, ChronoUnit.DAYS)));
}
/**
* convert(Duration.ofNanos(n)) agrees with convert(n, NANOSECONDS)
*/
public void testConvertDuration_roundtripDurationOfNanos() {
// Test values near unit transitions and near overflow.
LongStream.concat(
Arrays.stream(TimeUnit.values()).mapToLong(u ->
u.toNanos(1)),
LongStream.of(Long.MAX_VALUE, Long.MIN_VALUE))
.flatMap(n -> LongStream.of(n, n + 1, n - 1))
.flatMap(n -> LongStream.of(n, n + 1_000_000_000, n -
1_000_000_000))
.flatMap(n -> LongStream.of(n, -n))
// .peek(System.err::println)
.forEach(n -> Arrays.stream(TimeUnit.values()).forEach(
u -> assertEquals(u.convert(n, NANOSECONDS),
u.convert(Duration.ofNanos(n)))));
}
/**
* convert(Duration) doesn't misbehave near Long.MAX_VALUE and
Long.MIN_VALUE.
*/
public void testConvertDuration_nearOverflow() {
ChronoUnit NANOS = ChronoUnit.NANOS;
Duration maxDuration = Duration.ofSeconds(Long.MAX_VALUE,
999_999_999);
Duration minDuration = Duration.ofSeconds(Long.MIN_VALUE, 0);
for (TimeUnit u : TimeUnit.values()) {
ChronoUnit cu = u.toChronoUnit();
long r;
if (u.toNanos(1) > SECONDS.toNanos(1)) {
r = u.toNanos(1) / SECONDS.toNanos(1);
assertThrows(ArithmeticException.class,
() -> Duration.of(Long.MAX_VALUE, cu),
() -> Duration.of(Long.MIN_VALUE, cu));
} else {
r = 1;
Duration max = Duration.of(Long.MAX_VALUE, cu);
Duration min = Duration.of(Long.MIN_VALUE, cu);
assertEquals(Long.MAX_VALUE, u.convert(max));
assertEquals(Long.MAX_VALUE - 1, u.convert(max.minus(1,
NANOS)));
assertEquals(Long.MAX_VALUE - 1, u.convert(max.minus(1,
cu)));
assertEquals(Long.MIN_VALUE, u.convert(min));
assertEquals(Long.MIN_VALUE + 1, u.convert(min.plus(1,
NANOS)));
assertEquals(Long.MIN_VALUE + 1, u.convert(min.plus(1,
cu)));
assertEquals(Long.MAX_VALUE, u.convert(max.plus(1, NANOS)));
if (u != SECONDS) {
assertEquals(Long.MAX_VALUE, u.convert(max.plus(1,
cu)));
assertEquals(Long.MIN_VALUE, u.convert(min.minus(1,
NANOS)));
assertEquals(Long.MIN_VALUE, u.convert(min.minus(1,
cu)));
}
}
assertEquals(Long.MAX_VALUE / r, u.convert(maxDuration));
assertEquals(Long.MIN_VALUE / r, u.convert(minDuration));
}
}
On Thu, May 31, 2018 at 12:32 AM, Stephen Colebourne <scolebourne at joda.org>
wrote:
> I'm not convinced TimeUnit::toDuration(long amount) has enough value.
> We don't have a similar method on ChronoUnit
>
> Duration.of(amount, timeUnit.toChronoUnit()) seems sufficient. Maybe
> document this in the convert(Duration) method?
>
> Stephen
>
>
> On 31 May 2018 at 01:19, Martin Buchholz <martinrb at google.com> wrote:
> > v.0.2 has both conversion methods in TimeUnit. The unexpected weirdness
> is
> > that convert(Duration) saturates while toDuration throws
> > ArithmeticException, but both seem author-culture-consistent. Perhaps
> > TimeUnit#toDuration doesn't provide enough value in view of the existing
> > Duration.of and TimeUnit#toChronoUnit. And most of the time you'd
> expect to
> > convert from Duration to long, just before calling a TimeUnit based
> method.
> >
> > /**
> > * Converts the given time duration to this unit.
> > *
> > * @param duration the time duration
> > * @return the converted duration in this unit,
> > * or {@code Long.MIN_VALUE} if conversion would negatively overflow,
> > * or {@code Long.MAX_VALUE} if it would positively overflow.
> > * @throws NullPointerException if {@code duration} is null
> > */
> > public long convert(Duration duration) {
> > long s = convert(duration.getSeconds(), SECONDS);
> > if (s == Long.MIN_VALUE) return s;
> > long n = convert(duration.getNano(), NANOSECONDS);
> > assert n >= 0 && n < 1_000_000_000;
> > return (s + n < s) ? Long.MAX_VALUE : s + n;
> > }
> >
> > /**
> > * Converts the given time duration in this unit to a Duration.
> > *
> > * @param duration the time duration
> > * @return the time duration represented as a Duration
> > * @throws ArithmeticException if the duration cannot be represented
> > * as a Duration due to numeric overflow
> > */
> > public Duration toDuration(long duration) {
> > return Duration.of(duration, toChronoUnit());
> > }
> >
>
More information about the core-libs-dev
mailing list