Period Class
java.time.Period represents an amount of time in calendar units: years, months, and days, such as "1 year, 2 months, and 3 days." It is the right type for human-scale, date-based amounts: ages, subscription plans, loan terms, warranty periods, notice periods, and "30 days after the invoice date." Because months and years vary in length, a Period is not a fixed number of seconds; it means "move the calendar forward by this many units." For exact, time-based amounts, use Duration (next lesson).
Creating a Period
import java.time.Period;
Period p = Period.of(1, 2, 3); // P1Y2M3D: 1 year, 2 months, 3 days
Period.ofYears(1); // P1Y
Period.ofMonths(6); // P6M
Period.ofWeeks(2); // P14D: stored as days
Period.ofDays(45); // P45D
Period.parse("P1Y6M"); // from ISO-8601 text
Period.ZERO; // P0D
The text form follows ISO-8601: P (period), then a number before Y, M, W, or D. It is what toString() produces and what parse() accepts.
p.getYears(); // 1
p.getMonths(); // 2
p.getDays(); // 3
A Period keeps the three units separately. It does not convert days to months or months to years automatically.
Measuring a Period Between Dates
Period.between(start, end) measures the gap between two LocalDates in years, months, and days, the way a person counts it on a calendar:
LocalDate dob = LocalDate.of(1998, 2, 14);
LocalDate today = LocalDate.of(2026, 11, 13);
Period age = Period.between(dob, today);
age; // P28Y8M30D
age.getYears(); // 28: the person's age
age.toTotalMonths(); // 344
The end date is exclusive. If the end is before the start, the result is negative:
Period.between(LocalDate.of(2026, 3, 1), LocalDate.of(2026, 1, 15)); // P-1M-17D
Period versus a day count
Period.between answers "how many years, months, and days?"; ChronoUnit.DAYS.between answers "how many days in total?":
ChronoUnit.DAYS.between(dob, today); // 10499
A common mistake is to call period.getDays() expecting the total number of days. For P28Y8M30D, getDays() returns 30, only the day component.
Applying a Period to a Date
LocalDate start = LocalDate.of(2026, 1, 31);
start.plus(Period.ofMonths(1)); // 2026-02-28: month-end clamped
start.plus(Period.ofDays(30)); // 2026-03-02
start.minus(Period.ofYears(1)); // 2025-01-31
plus(Period) adds years first, then months, then days, adjusting to a valid date after the month step. "One month" and "30 days" give different results because months differ in length, which is exactly why both units exist.
Normalization and Equality
Period.ofMonths(14).normalized(); // P1Y2M: months over 11 become years
Period.ofDays(45).normalized(); // P45D: days are never converted (months vary in length)
equals compares the three fields individually, so periods that represent "the same" amount in different units are not equal:
Period.of(0, 12, 0).equals(Period.ofYears(1)); // false
Period.ofMonths(1).equals(Period.ofDays(30)); // false
Normalize before comparing when years and months should be considered interchangeable. Days and months are never interchangeable.
Other useful methods: isZero(), isNegative(), negated(), multipliedBy(n), plusMonths(n), withDays(n).
Period with ZonedDateTime
Applied to a ZonedDateTime, a Period keeps the local time of day, even across a daylight-saving change, whereas a Duration adds exact elapsed time:
ZonedDateTime z = ZonedDateTime.of(2026, 3, 7, 12, 0, 0, 0, ZoneId.of("America/New_York"));
z.plus(Period.ofDays(1)); // 2026-03-08T12:00-04:00: same wall-clock time next day
z.plus(Duration.ofDays(1)); // 2026-03-08T13:00-04:00: exactly 24 hours later
The night of 7–8 March 2026 is when New York moves its clocks forward, so that calendar day lasts only 23 hours. Use Period for "same time tomorrow" and Duration for "exactly 24 hours from now."
Period cannot be applied to a LocalTime or an Instant, since neither has a calendar date (Instant.plus(Period.ofDays(1)) works only for days, interpreted as 24 hours, and throws for months and years).
Period Versus Duration
Period | Duration | |
|---|---|---|
| Units | Years, months, days | Seconds and nanoseconds (shown as hours, minutes, seconds) |
| Based on | The calendar | Exact elapsed time |
| Example | P1Y2M3D | PT8H30M |
| Works with | LocalDate, LocalDateTime, ZonedDateTime | Time-based types: LocalTime, LocalDateTime, ZonedDateTime, Instant |
| Typical use | Ages, subscriptions, loan terms | Timeouts, elapsed time, session lengths |
Practical Example: Membership Renewals
A learning platform sells memberships of one month, three months, or one year. The admin dashboard shows each member's renewal date and time left, or how long ago a plan expired.
import java.time.*;
import java.util.List;
public class Per {
record Member(String name, LocalDate joined, Period plan) { }
public static void main(String[] args) {
LocalDate today = LocalDate.of(2026, 11, 13);
List<Member> members = List.of(
new Member("Ada", LocalDate.of(2025, 12, 14), Period.ofYears(1)),
new Member("Linus", LocalDate.of(2026, 8, 31), Period.ofMonths(3)),
new Member("Grace", LocalDate.of(2026, 10, 5), Period.ofMonths(1))
);
for (Member m : members) {
LocalDate expiry = m.joined().plus(m.plan());
Period left = Period.between(today, expiry);
String status = expiry.isBefore(today) ? "expired " + Period.between(expiry, today).getDays() + " day(s) ago"
: "renews " + expiry + " (in " + left.getMonths() + "m " + left.getDays() + "d)";
System.out.println(m.name() + " joined " + m.joined() + ", plan " + m.plan() + ": " + status);
}
}
}
Output:
Ada joined 2025-12-14, plan P1Y: renews 2026-12-14 (in 1m 1d)
Linus joined 2026-08-31, plan P3M: renews 2026-11-30 (in 0m 17d)
Grace joined 2026-10-05, plan P1M: expired 8 day(s) ago
Each plan is a Period, so joined.plus(plan) produces the expiry date according to calendar rules. Linus joined on 31 August; three months later would be 31 November, which does not exist, so the date is clamped to 30 November rather than overflowing into December. Period.between(today, expiry) expresses the time remaining in months and days, which reads naturally on a dashboard. For an expired plan, the period is measured the other way round to report how long ago it ended. Because the expired gap here is under a month, getDays() alone is the full count; a production version would format years and months too, or use ChronoUnit.DAYS.between for a single total.