W3docs

Java Period

Represent date-based amounts (years, months, days) in Java with Period.

Java Period

Period is the calendar-shaped sibling of Duration. Where Duration is "X seconds plus Y nanos," Period is "X years, Y months, Z days." It's the right type for any length you'd quote in calendar terms: "trial period of 30 days," "annual subscription," "two-month notice period," "add one billing cycle to the renewal date."

The two never mix. Duration.ofDays(30) is 30 × 24 × 3600 seconds exactly. Period.ofDays(30) is 30 calendar days, which usually but not always equals 30 × 24 hours (DST transitions add or remove an hour). For "exactly that many seconds," Duration. For "the calendar day that's N days later," Period.

Creating

Period.ofDays(30);
Period.ofWeeks(2);                                           // stored as 14 days
Period.ofMonths(3);
Period.ofYears(1);
Period.of(1, 6, 0);                                          // 1 year, 6 months, 0 days

Period.between(startDate, endDate);                           // takes LocalDate (not LocalDateTime)
Period.parse("P1Y2M3D");                                     // ISO-8601: P[years]Y[months]M[days]D

The string form is PnYnMnDP1Y2M3D is one year, two months, three days. The P prefix is mandatory. No T (that would make it a Duration); no hours, minutes, or seconds (those don't fit).

Period.between(start, end) takes two LocalDates and returns a breakdown of the difference:

Period age = Period.between(LocalDate.of(1990, 3, 15), LocalDate.of(2025, 11, 4));
// P35Y7M20D — 35 years, 7 months, 20 days

That's the standard "compute an age" idiom. The result is a breakdown, not a single number — there are 35 years worth in there, then 7 months on top, then 20 days. To collapse to a single count, use ChronoUnit.YEARS.between(...), which returns a long.

Inspection

Period p = Period.of(1, 6, 14);
p.getYears();      // 1
p.getMonths();     // 6
p.getDays();       // 14

p.toTotalMonths();                                            // 1 * 12 + 6 = 18 (years + months, ignoring days)
p.isZero();                                                   // false
p.isNegative();                                               // true if any component is negative

Three accessors for the three components, plus toTotalMonths for a quick aggregation. There's no toTotalDays — that would require knowing the calendar context (a year is 365 or 366 days; a month is 28-31).

Arithmetic

p.plus(Period.ofMonths(1));
p.plusYears(1);
p.plusMonths(6);
p.plusDays(14);
p.minus(Period.ofDays(7));

p.multipliedBy(3);
p.negated();
p.normalized();                                               // collapse extra months into years

normalized() is interesting: it collapses any month count of 12 or more into years. Period.of(0, 14, 0).normalized() is Period.of(1, 2, 0). It does not touch days — there's no "normalize 31 days into 1 month and 1 day" because months aren't constant-length.

Adding to a date

Period is a TemporalAmount. Any date-like Temporal accepts it:

LocalDate maturity = LocalDate.of(2025, 11, 4).plus(Period.ofMonths(6));
LocalDate retirement = LocalDate.of(1990, 3, 15).plus(Period.ofYears(65));

LocalDateTime renewal = LocalDateTime.of(2025, 11, 4, 9, 0).plus(Period.ofYears(1));
ZonedDateTime nextBill = zdt.plus(Period.ofMonths(1));

Adding a Period to an Instant throwsInstant has no calendar, so the JDK refuses to compute "an instant one month later" without a zone. If you want it, convert via ZonedDateTime:

Instant nextMonth = inst.atZone(ZoneId.of("UTC"))
                        .plus(Period.ofMonths(1))
                        .toInstant();

That's deliberate verbosity — the conversion is the place where you supply the missing calendar info.

The plusMonths clamping rule from LocalDate applies to Period-arithmetic the same way: January 31 + Period.ofMonths(1) is February 28, not March 3.

Period doesn't normalise across components

A subtle behavior: Period.of(1, 0, 365) is not equal to Period.of(2, 0, 0), even though they describe the same length when added to a typical date. The class stores the breakdown verbatim and compares by structure:

Period.of(1, 0, 365).equals(Period.of(2, 0, 0));              // false
Period.of(0, 14, 0).equals(Period.of(1, 2, 0));               // false (until normalized())
Period.of(0, 14, 0).normalized().equals(Period.of(1, 2, 0));  // true

For "is this period at least a year regardless of how it's broken down," compare on dates: start.plus(p1).isEqual(start.plus(p2)) is the only fully-correct check.

Distance: Period.between vs ChronoUnit.between

Period diff = Period.between(start, end);                     // calendar breakdown
long days   = ChronoUnit.DAYS.between(start, end);            // single long
long months = ChronoUnit.MONTHS.between(start, end);
long years  = ChronoUnit.YEARS.between(start, end);

The two answer different questions:

  • Period.between(start, end) returns "1 year, 6 months, 14 days" — useful when you want to display a breakdown.
  • ChronoUnit.DAYS.between(start, end) returns 567 (or whatever the literal day count is) — useful when you want to compare or accumulate.

Use the second when you need to do arithmetic on the result. Use the first when you want to show a human.

A worked example: subscriptions, trials, and ages

The program below uses Period for a small subscription scenario: trial ends one month from signup, the renewal date repeats every year, the customer's age is computed from a birthdate, and the clamping behavior on month boundaries is made explicit. It also shows the contrast with Duration for "the same span in elapsed time."

java— editable, runs on the server

What to take from the run:

  • Adding Period.ofMonths(1) to January 31 produced February 28 — the same clamping rule from LocalDate. Period.plusMonths(1).minusMonths(1) is not always the identity. When you're computing billing dates near month-end, design around the clamp explicitly (e.g., always bill on the 1st of the next month) rather than assuming round-trip symmetry.
  • Period.between(birth, today) returned a calendar breakdown — years, months, days. For "are they an adult?" you want ChronoUnit.YEARS.between(birth, today) >= 18, not age.getYears() >= 18. Both give the same answer for this case, but they answer different questions in general — ChronoUnit.YEARS.between is the total whole years, age.getYears() is the years component of the breakdown.
  • Period.of(0, 14, 0).normalized() became Period.of(1, 2, 0). The day count was untouched — days can't normalize without knowing which months are in play. If you build a Period from arithmetic and want a "clean" representation, call normalized before storing or displaying.
  • P1Y.equals(P12M) was false, and P1Y.equals(P365D) was also false. Equality is structural, not by length. When you really want "do these two periods produce the same end date?", compute both end dates and compare with LocalDate.isEqual. The .normalized() form fixes the year/month case but never the day case.
  • The inst.plus(Period.ofDays(1)) call threw. Instant has no calendar, so a calendar-shaped amount has no meaning. Convert through ZonedDateTime first — the type system makes you choose a zone explicitly. The mirror failure from the Duration chapter (Duration on a LocalDate) is the same design: the JDK refuses to invent the missing context.

What's next

Period closes out the "lengths of time" pair. The next two chapters cover the string ↔ value boundary: Java Date Formatting for turning java.time values into strings, and Java Date Parsing for the inverse. Both run on DateTimeFormatter, which is the modern, thread-safe replacement for the legacy SimpleDateFormat.

Practice

Practice

A user signs up on January 31, 2025. Your billing code computes the next charge with `signupDate.plus(Period.ofMonths(1))`. What date is the next charge, and what should you know about the behaviour?