skip to content

What happens when you add a year to February 29 (a leap day), and how does java.time handle leap years generally?

level: seniorimportance: should knowfreq 45%

answer

  1. Feb 29 + 1 yr (non-leap) → Feb 28, not an error
  2. Clamp day-of-month to last valid day
  3. Leap rule: /4, not /100, but yes /400
  4. isLeapYear() / Year.isLeap
  5. plus then minus is NOT always reversible

basics

~20 s

Feb 29 only exists in leap years. If you add one year to Feb 29 and the target year isn't a leap year, java.time adjusts the result to Feb 28 instead of failing. It never produces an invalid date like Feb 30.

solid answer

~50 s

LocalDate.of(2024, 2, 29) is valid because 2024 is a leap year. Add one year and you'd land on 2025-02-29, which does not exist (2025 is not a leap year). Rather than throw, java.time applies a documented resolution: when a plus/minus/with operation would create an invalid day-of-month, it clamps the day down to the last valid day of that month — so plusYears(1) yields 2025-02-28. The same clamping happens for plusMonths onto shorter months (Jan 31 + 1 month = Feb 28/29). Leap-year determination follows the proleptic Gregorian calendar rule: divisible by 4, except centuries not divisible by 400 (1900 is not leap, 2000 is). You can query it with LocalDate.isLeapYear() or Year.isLeap(year). The clamping is intentional and consistent; it makes date arithmetic total (never throws on overflow) at the cost of plusYears not always being reversible.

code

java · 11 lines
java
LocalDate leap = LocalDate.of(2024, 2, 29);
leap.plusYears(1);    // 2025-02-28  (clamped, 2025 not a leap year)

LocalDate.of(2026, 1, 31).plusMonths(1);  // 2026-02-28
LocalDate.of(2024, 1, 31).plusMonths(1);  // 2024-02-29 (leap year)

// Not reversible across the leap boundary:
LocalDate.of(2024, 2, 29).plusYears(1).minusYears(1); // 2024-02-28

boolean a = LocalDate.of(1900, 1, 1).isLeapYear(); // false (/100, not /400)
boolean b = Year.isLeap(2000);                      // true  (/400)

go deeper

for a junior

Knows Feb 29 only exists in leap years and that adding a year to it gives Feb 28 rather than an error.

for a middle

States the general clamp-to-last-valid-day rule (also for Jan 31 + 1 month) and the 4/100/400 leap rule, and can use isLeapYear().

for a senior

Explains that clamping makes plus/minus non-reversible across leap/month boundaries and designs anniversary/subscription logic to handle it deliberately; aware of MIN/MAX overflow throwing.

for a principal

Sets domain conventions for anniversary/billing-date semantics (e.g. policy for Feb-29 renewals), and understands proleptic Gregorian / ISO chronology implications for historical and edge-range data.

## Background: what a leap year is The Earth's orbit isn't a whole number of days, so the Gregorian calendar inserts a **leap day** — February 29 — in certain years to stay aligned. The rule (the *proleptic Gregorian* rule java.time uses for all years, even before the calendar historically existed) is: - divisible by 4 → leap, **except** - divisible by 100 → **not** leap, **except** - divisible by 400 → leap. So 2024 is leap, 1900 is **not** (divisible by 100, not 400), and 2000 **is** (divisible by 400). Query it with `LocalDate.isLeapYear()` or the static `Year.isLeap(2024)`. ## The Feb 29 + 1 year problem `LocalDate.of(2024, 2, 29)` is valid. But `2025-02-29` is **not a real date** because 2025 isn't a leap year. So what should `feb29.plusYears(1)` do? The options are: throw, skip to March 1, or clamp to Feb 28. **java.time clamps**: the result is `2025-02-28`. ```java LocalDate leap = LocalDate.of(2024, 2, 29); LocalDate next = leap.plusYears(1); // 2025-02-28 (NOT 2025-02-29, NOT March 1) ``` ## The general rule: clamp day-of-month to the valid range This is not special-cased for February; it is a general resolution. Whenever a `plus`/`minus`/`with` operation produces a day-of-month that is too large for the target month, java.time **reduces the day to the last valid day of that month**: ```java LocalDate.of(2026, 1, 31).plusMonths(1); // 2026-02-28 (Feb has no 31st... or 29th) LocalDate.of(2026, 3, 31).minusMonths(1); // 2026-02-28 LocalDate.of(2024, 1, 31).plusMonths(1); // 2024-02-29 (leap year, so 29 is valid) ``` The documented behavior (see the javadoc for `plusYears`/`plusMonths`): "If the day-of-month is invalid for the resulting year/month, it is changed to the last valid day of the month." ## Important consequence: arithmetic isn't always reversible Because of clamping, `plusYears` and `minusYears` are **not exact inverses** across a leap boundary: ```java LocalDate.of(2024, 2, 29).plusYears(1).minusYears(1); // 2025-02-28 -> 2024-02-28, NOT 2024-02-29 ``` The day-of-month information (29) is lost once clamped. This is a frequent source of subtle bugs in subscription/anniversary logic — be deliberate about it (e.g. store the original day and re-derive, or define your own anniversary policy). ## Boundaries and ranges - `LocalDate.MIN` is `-999999999-01-01` and `LocalDate.MAX` is `+999999999-12-31` — the extreme supported dates, useful as sentinels for open-ended ranges. (`LocalTime.MIN`/`MAX`/`MIDNIGHT`/`NOON` exist similarly.) - Overflowing past `MAX`/`MIN` with arithmetic throws `DateTimeException` rather than clamping silently. ## Terms defined - **Proleptic Gregorian calendar**: applying today's Gregorian rules uniformly to all years, including those before the calendar's historical adoption — what java.time uses internally (the ISO chronology). - **Clamp**: reduce a value to the nearest valid one in range (here, the day-of-month). ## Takeaways 1. java.time never produces an invalid date; it clamps day-of-month down. 2. Feb 29 + 1 year → Feb 28 in a non-leap year. 3. The leap rule is 4 / 100 / 400; check with isLeapYear()/Year.isLeap. 4. Clamping makes plus/minus non-reversible across month/leap boundaries — design anniversary logic accordingly.

  • Is 1900 a leap year? Is 2000?
    1900 is NOT a leap year (divisible by 100 but not 400). 2000 IS a leap year (divisible by 400). This is the classic century-year edge case.
  • Why can plusYears followed by minusYears give back a different date?
    Clamping discards the original day-of-month. Feb 29 + 1 year clamps to Feb 28; minus 1 year then returns Feb 28, not Feb 29 — the 29 was lost, so the operations aren't exact inverses across a leap boundary.

Like an elevator that can't stop on a floor that doesn't exist: ask for the 30th of February and it quietly takes you to the highest floor that does exist — the 28th (or 29th).

saying these in an interview costs you the question

  • Claiming Feb 29 + 1 year throws an exception
  • Saying it rolls over to March 1 instead of clamping to Feb 28
  • Believing every year divisible by 100 is a leap year (1900 is not)
  • Assuming plus/minus year arithmetic is always reversible
  • Thinking java.time can hold an invalid date like Feb 30

context