Keep in mind: calendar months and years do not represent a fixed temporal length. For example, if this code shows a difference of 1 month, it could mean anything from 28 days to 31 days (where 1 day = 24h). If that's not ok for your use case, I strongly recommend ditching months and years entirely (so you can just use the Duration
class), or at most setting a fixed temporal value for months and years (e.g. 30 days for a month and 365 days for a year).
If you still want to go ahead (for example because your users already expect months and years to mean calendar months and years), you will notice the code is not as straightforward as one would initially think it should be (this also means it's more error-prone; thanks to @Alex for noticing a bug, which has now been fixed), and that is precisely because Java library designers were smart enough not to mix calendar periods (Period
class) and exact durations (Duration
class) together.
TL;DR
Precondition: start <= end
.
// Closest start datetime with same time-of-day as the end datetime.
// This is to make sure to only count full 24h days in the period.
LocalDateTime closestFullDaysStart = LocalDateTime.of(
start.toLocalDate()
.plusDays(end.toLocalTime().compareTo(start.toLocalTime()) < 0 ? 1 : 0),
end.toLocalTime()
);
// Get the calendar period between the dates (full years, months & days).
Period period = Period.between(closestFullDaysStart.toLocalDate(), end.toLocalDate());
// Get the remainder as a duration (hours, minutes, etc.).
Duration duration = Duration.between(start, closestFullDaysStart);
and then use the methods period.getYears()
, period.getMonths()
, period.getDays()
, duration.toHoursPart()
, duration.toMinutesPart()
, duration.toSecondsPart()
.
Try it online!
Expanded answer
I'll answer the original question, i.e. how to get the time difference between two LocalDateTimes
in years, months, days, hours & minutes, such that the "sum" (see note below) of all the values for the different units equals the total temporal difference, and such that the value in each unit is smaller than the next bigger unit—i.e. minutes < 60
, hours < 24
, and so on.
Given two LocalDateTimes
start
and end
, e.g.
LocalDateTime start = LocalDateTime.of(2019, 11, 28, 17, 15);
LocalDateTime end = LocalDateTime.of(2020, 11, 30, 16, 44);
we can represent the absolute timespan between the two with a Duration
—perhaps using Duration.between(start, end)
. But the biggest unit we can extract out of a Duration
is days (as a temporal unit equivalent to 24h)—see the note below for an explanation. To use larger units* (months, years) we can represent this Duration
with a pair of (Period
, Duration
), where the Period
measures the difference up to a precision of days and the Duration
represents the remainder.
We need to be careful here, because a Period
is really a date difference, not an amount of time, and all its calculations are based on calendar dates (see the section below). For example, from 1st January 2000 at 23:59 to 2nd January 2000 at 00:01, a Period
would say there is a difference of 1 day, because that's the difference between the two dates, even though the time delta is 2 minutes, much less than 24h. So, we need to start counting the calendar period at the next closest point in time which has the same time of day as the end point, so that any calendar days that we count actually correspond to full 24h durations:
LocalDateTime closestFullDaysStart = LocalDateTime.of(
start.toLocalDate()
// if the end time-of-day is earlier than the start time-of-day,
// the next point in time with that time-of-day is one calendar day ahead
// (the clock "wraps around" at midnight while advancing to it)
.plusDays(end.toLocalTime().compareTo(start.toLocalTime()) < 0 ? 1 : 0),
end.toLocalTime()
);
Now we have effectively split the timespan between start
and end
into two parts: the span from start
to the closestFullDaysStart
, which by construction will be less than 24h, so we can measure it with a Duration
object with no days part,
Duration duration = Duration.between(start, closestFullDaysStart);
and the span from closestFullDaysStart
and end
, which we know we can now reliably* measure with a Period
.
Period period = Period.between(closestFullDaysStart.toLocalDate(), end.toLocalDate());
Now we can simply use the methods defined on Period
and Duration
to extract the individual units:
System.out.printf(
"%d years, %d months, %d days, %d hours, %d minutes, %d seconds",
period.getYears(), period.getMonths(), period.getDays(),
duration.toHoursPart(), duration.toMinutesPart(), duration.toSecondsPart()
);
1 years, 0 months, 1 days, 23 hours, 29 minutes, 0 seconds
or, using the default format:
System.out.println(period + " + " + duration);
P1Y1D + PT23H29M
*Note on years, months & days
Note that, in java.time
's conception, period "units" like "month" or "year" don't represent a fixed, absolute temporal value—they're date- and calendar-dependent, as the following example illustrates:
LocalDateTime
start1 = LocalDateTime.of(2020, 1, 1, 0, 0),
end1 = LocalDateTime.of(2021, 1, 1, 0, 0),
start2 = LocalDateTime.of(2021, 1, 1, 0, 0),
end2 = LocalDateTime.of(2022, 1, 1, 0, 0);
System.out.println(Period.between(start1.toLocalDate(), end1.toLocalDate()));
System.out.println(Duration.between(start1, end1).toDays());
System.out.println(Period.between(start2.toLocalDate(), end2.toLocalDate()));
System.out.println(Duration.between(start2, end2).toDays());
P1Y
366
P1Y
365
As another example, from 1st January 2000 at 23:59 to 2nd January 2000 at 00:01, a Period
would say there is a difference of 1 day, because that's the difference between the two dates, even though the time delta is less than 24h.
Negative spans
If start > end
, the code above produces an answer which is technically correct if we sum all the units but is presented in an unexpected way.
For example, for
LocalDateTime start = LocalDateTime.of(2020, 1, 1, 1, 00);
LocalDateTime end = LocalDateTime.of(2020, 1, 1, 0, 0);
we get:
0 years, 0 months, -1 days, 23 hours, 0 minutes, 0 seconds
-1 day plus 23 hours is -1 hour, which is correct. But we would expect the answer to be just -1 hour.
Currently durations shown are always positive, and this is because closestFullDaysStart
is always in the future with respect to start
.
If start > end
, however, the direction from start to end is "back in time", and so we need to retrocede from start
in order to find the closest datetime with the end time-of-day that lies between start
and end
.
Also, the wrap-around condition changes: since we are winding the clock backwards, we wrap around the clock (and thus need to subtract one calendar day) if the start time-of-day is earlier than the end time.
Combining all of this yields:
int endStartComparison = end.compareTo(start);
int endStartTimeComparison = end.toLocalTime().compareTo(start.toLocalTime());
LocalDateTime closestFullDaysStart = LocalDateTime.of(
start.toLocalDate()
.plusDays(
(
(endStartComparison > 0)
? (endStartTimeComparison < 0)
: (endStartTimeComparison > 0)
)
? (long) Math.signum(endStartComparison) // advance one unit in the direction start->end
: 0
),
end.toLocalTime()
);
We can rewrite that ugly nested conditional in a more compact but more hacky way:
(endStartComparison * endStartTimeComparison < 0)
Try it online!