☕ Java

Period

Period is the java.time class representing a date-based amount of time expressed in years, months, and days — a human-calendar quantity like '2 years, 3 months, and 10 days' rather than a fixed number of elapsed seconds. This entry covers how Period differs fundamentally from Duration in what unit of measurement it operates in, how Period.between() computes a breakdown rather than a single total, why adding a Period to a date is calendar-aware rather than fixed-length, and the common pitfall of expecting a Period's individual fields to sum to a meaningful total without normalization.

Period as a Calendar-Based, Not Duration-Based, Amount

Period stores three separate integer fields — years, months, and days — and represents a date-based quantity of time the way a human would naturally describe a span between two calendar dates, rather than measuring a fixed quantity of elapsed seconds the way Duration does. This distinction matters because calendar units genuinely do not have a fixed length: a "month" can be 28, 29, 30, or 31 days depending on which month and whether it's a leap year, and a "year" can be 365 or 366 days depending on leap years — Period embraces this variability rather than trying to flatten it into some fixed-length approximation, which is exactly why Period.ofMonths(1) added to January 31st correctly lands on a real calendar date (the last day of February, whatever that happens to be that year) rather than crashing or silently producing an invalid date. This is the fundamental reason Period exists as a separate type from Duration rather than the two being interchangeable or convertible into one another in any general way: a Period of "1 month" simply has no fixed number of seconds it corresponds to in isolation — it can only be resolved into an actual elapsed length once it's anchored to a specific starting date, which is precisely the kind of date-dependent, calendar-aware behavior Duration (covered next) is structurally incapable of expressing, since Duration only ever deals in fixed-length units like seconds and nanoseconds.
Java
// ── Period embraces variable-length calendar units, doesn't flatten them
Period oneMonth = Period.ofMonths(1);

LocalDate jan31 = LocalDate.of(2024, 1, 31);
LocalDate result = jan31.plus(oneMonth);
System.out.println(result);   // 2024-02-29 — correctly lands on Feb's actual last day
// (2024 is a leap year — the SAME Period added to Jan 31, 2023 would give Feb 28)

// ── A Period's "length" is undefined until anchored to a real date ────
Period genericMonth = Period.ofMonths(1);
// genericMonth.toTotalMonths() works (just adds years*12+months), but there is
// NO equivalent "total seconds"1 month has no fixed number of seconds at all,
// since that number depends entirely on WHICH month it's applied to

Creating a Period and Period.between()

Period offers direct factory methods — Period.ofYears(n), Period.ofMonths(n), Period.ofDays(n), and the combined Period.of(years, months, days) — for constructing a Period with specific field values directly, useful when the intended date-based amount is already known up front (such as "add exactly 3 months" for a quarterly billing cycle). Far more commonly, though, a Period is obtained by computing the calendar-based difference between two existing LocalDate values via Period.between(startDate, endDate), which returns a Period broken down into the largest possible whole years, then the largest possible whole remaining months, then the remaining days — not a single flattened number in any one unit, but a genuine, human-readable breakdown across all three fields simultaneously. It's important to note that Period.between() is order-sensitive and date-only: if endDate is before startDate, the resulting Period contains negative field values reflecting that, and because Period only operates on LocalDate (never LocalDateTime or LocalTime), any time-of-day information on either input is necessarily irrelevant to the calculation, since Period only ever measures whole calendar days, months, and years.
Java
// ── Direct construction ────────────────────────────────────────────────
Period threeMonths = Period.ofMonths(3);
Period combined = Period.of(1, 6, 15);   // 1 year, 6 months, 15 days

// ── Period.between — breakdown across years/months/days simultaneously
LocalDate start = LocalDate.of(2022, 3, 10);
LocalDate end = LocalDate.of(2024, 6, 25);

Period gap = Period.between(start, end);
System.out.println(gap);                // P2Y3M15D
System.out.println(gap.getYears());     // 2
System.out.println(gap.getMonths());    // 3
System.out.println(gap.getDays());      // 15

// ── Order-sensitive — reversed arguments produce a NEGATIVE Period ────
Period reversed = Period.between(end, start);
System.out.println(reversed);            // P-2Y-3M-15D
System.out.println(reversed.isNegative());   // true

Applying a Period and the Normalization Pitfall

A Period is added to or subtracted from a LocalDate (or LocalDateTime/ZonedDateTime) via the usual plus(period)/minus(period) methods, applying the years, then months, then days fields in that order, with each individual addition correctly handling its own month-length and leap-year edge cases as it goes — this ordering matters specifically because adding the fields in a different sequence can occasionally produce a different final date when month-end overflow is involved. A common pitfall is assuming a Period's individual getYears()/getMonths()/getDays() fields can be combined arithmetically into some other unit (like "total days") through simple multiplication — they cannot, in general, because the actual number of days a given number of months represents depends entirely on which specific months are involved, which a standalone Period, once already computed, no longer has any information about. Period.between() does return a fully normalized result (months are kept within 0-11, with any excess rolled up into years), but calling toTotalMonths() only flattens the years and months fields together into a single month count (since years-to-months is a fixed, unambiguous 12:1 ratio); there is deliberately no equivalent toTotalDays(), because converting months into days is exactly the variable-length operation Period.between() avoided defining in the first place — getting an actual day count requires going back to applying the Period to a real anchor date, or using ChronoUnit.DAYS.between() directly on the two original dates instead.
Java
LocalDate start = LocalDate.of(2024, 1, 31);
Period gap = Period.of(0, 1, 0);   // "1 month"

// ── Applying a Period — correctly handles month-end overflow ─────────
LocalDate result = start.plus(gap);
System.out.println(result);   // 2024-02-29 — NOT Mar 2nd, correctly clamped to Feb's end

// ── toTotalMonths() — years+months flatten fine, fixed 12:1 ratio ─────
Period combined = Period.of(1, 6, 0);
System.out.println(combined.toTotalMonths());   // 181 year (12) + 6 months

// ── There is NO toTotalDays() — days are NOT a fixed multiple of months
Period threeMonths = Period.ofMonths(3);
// threeMonths.toTotalDays();   // ← does not exist — no fixed answer is possible

// ── To get an actual day count, go back to the real dates instead ─────
LocalDate a = LocalDate.of(2024, 1, 1);
LocalDate b = LocalDate.of(2024, 4, 1);
long actualDays = ChronoUnit.DAYS.between(a, b);
System.out.println(actualDays);   // 91 — the REAL number of days for THESE specific months

Related Topics in Date Time API

LocalDate
LocalDate is the java.time class (introduced in Java 8 as part of the redesigned date-time API replacing the deprecated java.util.Date/Calendar) representing a date without a time component or a time zone — just a year, month, and day on the ISO-8601 calendar. This entry covers why LocalDate deliberately carries no time-of-day or zone information, how it is created and parsed, how it is manipulated via its immutable, fluent with*/plus*/minus* methods, how two LocalDate instances are compared and the period between them measured, and the common pitfalls of treating it as if it secretly carried a time or zone after all.
LocalTime
LocalTime is the java.time class representing a time-of-day without any date or time zone attached — just an hour, minute, second, and nanosecond on a 24-hour clock. This entry covers what LocalTime intentionally omits and why, how to create and parse LocalTime values with varying precision, how arithmetic on LocalTime wraps around midnight instead of rolling into a different day, and how LocalTime is meant to be combined with LocalDate (via LocalDateTime) rather than used as a date substitute.
LocalDateTime
LocalDateTime is the java.time class combining a LocalDate and a LocalTime into a single date-and-time-of-day value that still, deliberately, carries no time zone or UTC-offset information. This entry covers why LocalDateTime is best understood as a composite of the two simpler types rather than a primitive type in its own right, how it is created from and decomposed back into its LocalDate/LocalTime parts, why the absence of a zone makes it unsuitable for representing one unambiguous, real-world instant, and how it relates to and converts into the zone-aware types (ZonedDateTime, Instant) when an actual moment in time is what's needed.
ZonedDateTime
ZonedDateTime is the java.time class representing a LocalDateTime combined with a ZoneId, fully resolved against the time zone's rules to also carry a specific UTC offset — making it the type capable of representing one genuinely unambiguous real-world instant while still preserving human-readable, zone-local wall-clock fields. This entry covers the relationship between ZoneId and the resolved offset, how daylight saving transitions create gaps and overlaps that ZonedDateTime must resolve, how zone-aware arithmetic differs from plain LocalDateTime arithmetic across those transitions, and when to prefer ZonedDateTime over the simpler offset-only OffsetDateTime or the zone-independent Instant.