Add or Subtract Days and Hours from a Date in Java
Java 13 min read
plusDays against plusHours across a daylight-saving change, the month-end clamping that turns 31 January into 28 February, and why Calendar.add mutates the object you passed in.
Date arithmetic in Java is a single method call, and two of its behaviours are decisions rather than bugs: adding one day is not the same as adding 24 hours, and adding one month to 31 January gives 28 February rather than 3 March. Both are correct, both surprise people, and both matter for billing.
Written against Java 17.
The plus and minus methods
Every java.time type is immutable, so every arithmetic method returns a new object:
LocalDate date = LocalDate.of(2026, 8, 25);
LocalDate tomorrow = date.plusDays(1);
LocalDate lastWeek = date.minusWeeks(1);
LocalDate nextMonth = date.plusMonths(1);
LocalDate nextYear = date.plusYears(1);
date; // still 2026-08-25 — unchanged
Ignoring the return value is the most common mistake when moving from Calendar, because
Calendar.add mutates. date.plusDays(1); on its own line compiles, does nothing observable, and
reads as though it worked.
The available units follow the type. LocalDate has days, weeks, months and years. LocalTime has
hours, minutes, seconds and nanos. LocalDateTime and ZonedDateTime have all of them.
LocalDateTime meeting = LocalDateTime.of(2026, 8, 25, 14, 0);
meeting.plusHours(2).minusMinutes(15); // 2026-08-25T15:45
There is also a generic form taking a unit, which is what to use when the unit is a variable:
LocalDate result = date.plus(3, ChronoUnit.WEEKS);
LocalDate back = date.minus(amount, unit);
Month-end clamping
LocalDate.of(2026, 1, 31).plusMonths(1); // 2026-02-28
LocalDate.of(2026, 3, 31).minusMonths(1); // 2026-02-28
LocalDate.of(2024, 1, 31).plusMonths(1); // 2024-02-29 — leap year
Adding a month keeps the day of the month when it exists and clamps to the last valid day when it does not. That is the behaviour a subscription renewal wants.
It is also not reversible:
LocalDate.of(2026, 1, 31).plusMonths(1).minusMonths(1); // 2026-01-28, not the 31st
Once the value has been clamped the original day is gone. For a recurring monthly date, store the original day of month and reapply it rather than repeatedly adding one month to the previous result — otherwise a subscription that started on the 31st drifts to the 28th permanently after one February.
static LocalDate nthOccurrence(LocalDate start, int monthsLater) {
LocalDate target = start.plusMonths(monthsLater);
return target.withDayOfMonth(Math.min(start.getDayOfMonth(), target.lengthOfMonth()));
}
One day is not 24 hours
This is the difference that produces wrong invoices:
ZoneId berlin = ZoneId.of("Europe/Berlin");
ZonedDateTime before = ZonedDateTime.of(2026, 10, 24, 12, 0, 0, 0, berlin);
before.plusDays(1); // 2026-10-25T12:00+01:00 — same wall clock, 25 hours later
before.plusHours(24); // 2026-10-25T11:00+01:00 — exactly 24 hours later
The clocks go back on that night. plusDays preserves the wall-clock time and therefore spans 25
real hours; plusHours preserves the elapsed time and therefore lands an hour earlier on the clock.
Which is correct depends on the question. “Same time tomorrow” is plusDays. “24 hours of access” is
plusHours. Using one where the other was meant is invisible for 363 days a year.
The spring transition has a sharper edge, because a wall-clock time can be skipped entirely:
ZonedDateTime beforeSpring = ZonedDateTime.of(2026, 3, 28, 2, 30, 0, 0, berlin);
beforeSpring.plusDays(1); // 2026-03-29T03:30+02:00 — 02:30 does not exist that night
No exception is thrown; the value moves forward by the gap. Code that assumes
x.plusDays(1).getHour() == x.getHour() is wrong twice a year, and a scheduled job configured to run
at 02:30 local time either runs twice or not at all, which is the practical argument for scheduling
in UTC and converting for display.
Period and Duration encode the same distinction:
before.plus(Period.ofDays(1)); // calendar day — same as plusDays
before.plus(Duration.ofDays(1)); // exactly 24 hours — same as plusHours(24)
Duration.ofDays exists and means 86,400 seconds, which is why it is the wrong name for a calendar
day. On a LocalDateTime, which has no zone and therefore no transitions, the two agree.
TemporalAdjusters, for the calendar questions
Arithmetic answers “how far”; adjusters answer “which one”:
import static java.time.temporal.TemporalAdjusters.*;
LocalDate d = LocalDate.of(2026, 8, 25);
d.with(firstDayOfMonth()); // 2026-08-01
d.with(lastDayOfMonth()); // 2026-08-31
d.with(firstDayOfNextMonth()); // 2026-09-01
d.with(next(DayOfWeek.MONDAY)); // the following Monday
d.with(nextOrSame(DayOfWeek.MONDAY)); // today if it is Monday
d.with(lastInMonth(DayOfWeek.FRIDAY)); // the last Friday in August
lastDayOfMonth handles February and leap years without a conditional, which is the point. The
firstInMonth, lastInMonth and dayOfWeekInMonth adjusters cover the “third Thursday” style of
rule that appears in scheduling and in tax deadlines, and they are worth reaching for before writing
a loop over the month. A custom
adjuster is a lambda when nothing built in fits:
TemporalAdjuster nextWorkingDay = temporal -> {
LocalDate day = LocalDate.from(temporal);
do { day = day.plusDays(1); }
while (day.getDayOfWeek() == DayOfWeek.SATURDAY || day.getDayOfWeek() == DayOfWeek.SUNDAY);
return day;
};
Business days, and why there is no method for it
The JDK has no plusBusinessDays, and the reason is that “business day” is not a property of the
calendar: it depends on the country, the industry and often the specific contract. Anything that
looks like a general implementation is encoding assumptions.
The honest version takes the holidays as an argument:
static LocalDate plusBusinessDays(LocalDate start, int days, Set<LocalDate> holidays) {
LocalDate result = start;
int remaining = days;
while (remaining > 0) {
result = result.plusDays(1);
boolean weekend = result.getDayOfWeek() == DayOfWeek.SATURDAY
|| result.getDayOfWeek() == DayOfWeek.SUNDAY;
if (!weekend && !holidays.contains(result)) {
remaining--;
}
}
return result;
}
The loop is fine for small offsets. For “90 business days from now” the arithmetic version is worth it (full weeks are a division, and only the remainder needs stepping) but the loop is easier to verify, and correctness is the scarce property here.
Two details that catch people out. Whether the start day itself counts is a specification question, not a coding one, and both answers are common: settlement periods usually exclude it, notice periods often include it. And weekends are not Saturday and Sunday everywhere; several countries run a Friday–Saturday weekend, so the day-of-week test belongs in configuration if the code crosses borders.
Chained arithmetic reads better as one expression
LocalDateTime start = LocalDateTime.of(2026, 8, 25, 14, 32, 7);
LocalDateTime nextMorning = start
.plusDays(1)
.withHour(9)
.withMinute(0)
.truncatedTo(ChronoUnit.MINUTES);
Because every method returns a new object, chaining is the natural style and there is no intermediate state to get wrong. The order matters, though: truncating before adding and adding before truncating give different answers whenever the addition crosses the unit being truncated.
Setting a field rather than shifting it
date.withDayOfMonth(1);
date.withMonth(12);
dateTime.withHour(9).withMinute(0).withSecond(0).withNano(0);
dateTime.truncatedTo(ChronoUnit.DAYS); // the same thing, in one call
withDayOfMonth(31) on a 30-day month throws rather than clamping, the opposite of plusMonths.
The difference is deliberate: shifting is approximate by nature, setting an explicit value is not.
The legacy Calendar
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_MONTH, 7);
calendar.add(Calendar.MONTH, -1);
Date result = calendar.getTime();
Three traps in four lines. add mutates the calendar rather than returning a new one, so passing
a Calendar to a method that shifts it changes the caller’s object. Calendar.MONTH is zero-based,
so Calendar.AUGUST is 7. And Calendar.getInstance() reads both the default zone and the default
locale, the latter of which decides which day the week starts on.
add also rolls over into larger fields while roll does not: roll(Calendar.DAY_OF_MONTH, 7) on
28 August gives 4 August, not 4 September.
Convert at the boundary and do the work in java.time:
Instant shifted = legacyDate.toInstant().plus(7, ChronoUnit.DAYS);
Date back = Date.from(shifted);
Related: current date and time, comparing and formatting. More in the Java guides.
Frequently asked questions
Why did my date not change after calling plusDays?
java.time types are immutable. plusDays
returns a new object; the original is untouched. Assign the result.
Why does adding one month to 31 January give 28 February?
Adding a month keeps the day of month where it exists and clamps to the last valid day where it does not. It is the behaviour a monthly renewal needs.
Is plusMonths reversible?
No. Once a value has been clamped, subtracting the month does not restore the original day. Keep the original day of month and reapply it for a recurring date.
What is the difference between plusDays(1) and plusHours(24)?
On a zoned value, across a
daylight-saving change, 25 or 23 hours against exactly 24. plusDays preserves the wall clock;
plusHours preserves elapsed time.
Period.ofDays(1) or Duration.ofDays(1)?
Period is a calendar day and follows the wall clock.
Duration is 86,400 seconds. They differ only on transition days, which is when it matters.
How do I get the last day of the month?
date.with(TemporalAdjusters.lastDayOfMonth()). It
handles February and leap years without a conditional.
How do I find the next Monday?
date.with(TemporalAdjusters.next(DayOfWeek.MONDAY)), or
nextOrSame if today counts.
Why does withDayOfMonth(31) throw in February when plusMonths clamps?
Setting an explicit value is exact and an invalid value is an error; shifting by a month is approximate by nature and clamps.
Does Calendar.add return a new object?
No. It mutates the calendar in place, which is the main
behavioural difference from java.time and a source of action at a distance.
How do I add days to a java.util.Date?
Convert it: date.toInstant().plus(7, ChronoUnit.DAYS),
then Date.from(...) if an API demands the old type back.