JAVA / DATES, NUMBERS, REGEX AND CORE LIBRARIES
Durations, periods and measuring elapsed time
Choose between Duration, Period and ChronoUnit.between for any time gap, and measure code execution time with a monotonic clock instead of wall time.
What you will learn
- Pick Duration for exact seconds and nanos, Period for calendar years, months and days
- Read PT32H30M30S correctly: Duration.toString never rolls hours up into days
- Use ChronoUnit.between for one whole unit; it truncates toward zero
- Time code with System.nanoTime() deltas, never with Instant.now() differences
Understanding Durations, periods and measuring elapsed time
Two different questions hide behind the phrase "how long". Duration answers it on a machine timeline: it stores a number of seconds plus a nanosecond adjustment, and nothing else. Period answers it on a calendar: it stores three independent integers (years, months, days) whose real length is unknown until you attach them to a date. That is why Duration.ofDays(1) is always 86,400 seconds while Period.ofDays(1) can mean 23, 24 or 25 hours depending on where you add it.
Constructing them mirrors that split. Duration.between accepts any temporal that carries a time of day (Instant, LocalTime, LocalDateTime, ZonedDateTime) and throws UnsupportedTemporalTypeException on a bare LocalDate. Period.between takes two LocalDates and normalises the result so that start.plus(period) equals end again, which is why its day component is a leftover remainder rather than a total. When you want one number instead of three fields, ChronoUnit.MONTHS.between or ChronoUnit.DAYS.between gives whole units, truncated toward zero.
Measuring how long your code took is a third problem. Instant.now() and System.currentTimeMillis() read a wall clock that an NTP correction, a manual change or a suspended virtual machine can move forwards or backwards, so the difference between two readings can come out as zero or even negative. System.nanoTime() reads a monotonic counter with an arbitrary origin: its absolute value means nothing, but the difference between two readings is genuine elapsed time. Wrap that difference in Duration.ofNanos when you want to print or compare it.
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Period;
import java.time.temporal.ChronoUnit;
public class TimeAmounts {
public static void main(String[] args) {
LocalDateTime start = LocalDateTime.of(2024, 3, 1, 9, 15, 0);
LocalDateTime end = LocalDateTime.of(2024, 3, 2, 17, 45, 30);
Duration shift = Duration.between(start, end);
System.out.println("duration : " + shift);
System.out.println("total minutes: " + shift.toMinutes());
System.out.printf("broken down : %dd %dh %dm %ds%n",
shift.toDaysPart(), shift.toHoursPart(),
shift.toMinutesPart(), shift.toSecondsPart());
LocalDate hired = LocalDate.of(2021, 11, 30);
LocalDate today = LocalDate.of(2024, 3, 2);
Period tenure = Period.between(hired, today);
System.out.println("period : " + tenure);
System.out.printf("tenure : %dy %dm %dd%n",
tenure.getYears(), tenure.getMonths(), tenure.getDays());
System.out.println("total months : " + tenure.toTotalMonths());
System.out.println("total days : " + ChronoUnit.DAYS.between(hired, today));
}
}Duration is an exact count of seconds and nanoseconds while Period is a set of calendar fields, and the distance between those two ideas is why one calendar day is not always 86,400 seconds.
Worked examples
A stopwatch needs a monotonic clock
Shows the correct timing pattern with System.nanoTime() and why a wall clock can report a negative elapsed time.
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
public class Elapsed {
public static void main(String[] args) {
long t0 = System.nanoTime();
long sum = 0;
for (int i = 1; i <= 1_000_000; i++) {
sum += i;
}
long t1 = System.nanoTime();
Duration took = Duration.ofNanos(t1 - t0);
System.out.println("sum = " + sum);
System.out.println("elapsed is non-negative: " + (took.toNanos() >= 0));
System.out.println("elapsed under a second: "
+ (took.compareTo(Duration.ofSeconds(1)) < 0));
// Simulate an NTP correction stepping the wall clock back mid-measurement.
Clock beforeFix = Clock.fixed(Instant.parse("2024-03-01T10:00:03Z"), ZoneOffset.UTC);
Clock afterFix = Clock.offset(beforeFix, Duration.ofSeconds(-2));
Instant wallStart = beforeFix.instant();
Instant wallEnd = afterFix.instant();
System.out.println("wall-clock difference: " + Duration.between(wallStart, wallEnd));
}
}Example explained
Line 1System.nanoTime() has no defined origin, so t1 - t0 is the only value you are allowed to interpret.
Line 2Duration.ofNanos wraps that raw delta, which lets you compare it with compareTo instead of remembering how many nanos are in a second.
Line 3Clock.offset(beforeFix, Duration.ofSeconds(-2)) models a clock that was pushed two seconds backwards between the two readings.
Line 4Duration.between then reports PT-2S, a negative elapsed time, which is exactly the failure mode Instant.now() timing produces in production.
One day is not twenty-four hours
Adds a day to a ZonedDateTime as a Period and as a Duration across the US spring-forward transition.
import java.time.Duration;
import java.time.Period;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class DayLength {
public static void main(String[] args) {
ZonedDateTime start =
ZonedDateTime.of(2024, 3, 9, 12, 0, 0, 0, ZoneId.of("America/New_York"));
ZonedDateTime calendarDay = start.plus(Period.ofDays(1));
System.out.println("start: " + start);
System.out.println("plus Period.ofDays(1): " + calendarDay);
System.out.println("plus Duration.ofDays(1): " + start.plus(Duration.ofDays(1)));
System.out.println("real time in between: " + Duration.between(start, calendarDay));
}
}Example explained
Line 1The start prints -05:00 and the next day prints -04:00 because the zone switches to daylight time on 2024-03-10.
Line 2Period.ofDays(1) moves the date field and lets the zone rules choose an offset, so the wall clock still reads 12:00.
Line 3Duration.ofDays(1) adds 86,400 seconds to the instant, and that instant lands at 13:00 local time.
Line 4Duration.between confirms the calendar day was only PT23H long, so the two results can never be interchangeable.
Month arithmetic clamps and does not reverse
Compares Period.between with ChronoUnit counts and shows why subtracting then adding a month loses the original day.
import java.time.LocalDate;
import java.time.Period;
import java.time.temporal.ChronoUnit;
public class MonthMath {
public static void main(String[] args) {
LocalDate back = LocalDate.of(2024, 3, 31).minusMonths(1);
System.out.println("2024-03-31 minus one month: " + back);
System.out.println("then plus one month: " + back.plusMonths(1));
LocalDate a = LocalDate.of(2024, 1, 31);
LocalDate b = LocalDate.of(2024, 3, 1);
Period p = Period.between(a, b);
System.out.println("Period.between: " + p);
System.out.println("MONTHS.between: " + ChronoUnit.MONTHS.between(a, b));
System.out.println("DAYS.between: " + ChronoUnit.DAYS.between(a, b));
System.out.println("a.plus(period): " + a.plus(p));
}
}Example explained
Line 1minusMonths(1) clamps 31 to 2024-02-29 because February has no 31st, and nothing records that the day was clamped.
Line 2Adding the month back gives 2024-03-29, so month arithmetic is lossy and not reversible.
Line 3The same 30-day gap is P1M1D as calendar fields, 1 as whole months and 30 as whole days: three correct answers to three different questions.
Line 4a.plus(p) returns 2024-03-01, the invariant Period.between is designed to preserve, which is why its day field is a remainder.
Important notes
Duration.toString() uses hours as its largest unit, so 50 hours prints as PT50H and never as days; Duration.parse reads that same ISO-8601 text back, and no DateTimeFormatter handles durations.
toDaysPart, toSecondsPart and Duration.toSeconds() were added in Java 9; on Java 8 you divide and take remainders from getSeconds() by hand.
Common mistakes
Calling Duration.between on two LocalDate values. It compiles, because the parameters are declared as Temporal, then throws UnsupportedTemporalTypeException: Unsupported unit: Seconds at runtime.
Reading period.getDays() as a total. From 2021-11-30 to 2024-03-02 that field is 2, not 823, so a "days since" display shows 2 while the real gap is over two years.
Scheduling with Duration.ofDays(1) or timing with Instant.now(). The first drifts an hour whenever a zone crosses a daylight-saving boundary; the second can report zero or negative elapsed time after a clock correction.
Try it yourself
Change, predict, then run
Create LocalDateTime values for a departure at 2024-06-01T22:40 and an arrival at 2024-06-02T06:05, then print the flight time as "7h 25m" using Duration with toHoursPart and toMinutesPart. Also print Period.between of the two dates and add a comment explaining why one result is 7h25m and the other is P1D.
Open the Java workspaceCheck your understanding
You hold 2024-03-09T12:00 in America/New_York, where clocks jump forward on 2024-03-10. What do plus(Period.ofDays(1)) and plus(Duration.ofDays(1)) give you?
- Period gives 12:00 the next day, Duration gives 13:00, because Duration adds exactly 86,400 seconds
- Both give 12:00 the next day, because Duration.ofDays converts to calendar days
- Both give 13:00, because the zone shifts the local time no matter which amount you add
- Period gives 13:00 and Duration gives 12:00, because Period is the calendar-aware type
Show answer
Period.ofDays(1) is date-field arithmetic: it moves the local date and the zone rules then pick a valid offset, so the wall clock stays at 12:00 even though only 23 real hours passed. Duration.ofDays(1) is 86,400 seconds of real time, which pushes the instant a full day forward and reads as 13:00 local. Option 2 is tempting because Duration exposes ofDays and toDays, but those are plain 24-hour conversions with no knowledge of the calendar or the zone.