I'm trying to find how many months are between 2 dates. My code is something like this right now
ChronoUnit.MONTHS.between(d1, d2)
The problem is that the result is a long. For example if the dates differ only in a few days I should get a result something like 0.34 instead of 0.
Also I need my code to account for the calendar, I cant assume each month has 31 days.
Diff between 1999-05-12 and 1999-08-24
Assuming all months have 31 days for simplicity
result = (12/31 31/31 31/31 24/31) = 2.786
According to the calendar we replace the 31s with the correct number of days for that specific year and month
CodePudding user response:
Here is my solution:
public static double monthsBetween(LocalDate start, LocalDate end) {
if (start.isAfter(end)) throw new IllegalArgumentException("Start must be before end!");
var lastDayOfStartMonth = start.with(TemporalAdjusters.lastDayOfMonth());
var firstDayOfEndMonth = end.with(TemporalAdjusters.firstDayOfMonth());
var startMonthLength = (double)start.lengthOfMonth();
var endMonthLength = (double)end.lengthOfMonth();
if (lastDayOfStartMonth.isAfter(firstDayOfEndMonth)) { // same month
return ChronoUnit.DAYS.between(start, end) / startMonthLength;
}
long months = ChronoUnit.MONTHS.between(lastDayOfStartMonth, firstDayOfEndMonth);
double startFraction = ChronoUnit.DAYS.between(start, lastDayOfStartMonth.plusDays(1)) / startMonthLength;
double endFraction = ChronoUnit.DAYS.between(firstDayOfEndMonth, end) / endMonthLength;
return months startFraction endFraction;
}
The idea is that you find the last day of start
's month (lastDayOfStartMonth
), and the first day of end
's month (firstDayOfEndMonth
) using temporal adjusters. These two dates are very important. The number you want is the sum of:
- the fractional number of a month between
start
andlastDayOfStartMonth
- the whole number of months between
lastDayOfStartMonth
andfirstDayOfEndMonth
. - the fractional number of a month between
firstDayOfEndMonth
andend
.
Then there is the edge case of when both dates are within the same month, which is easy to handle.
Note that in the first calculation, you have to add one day to lastDayOfStartMonth
, because ChronoUnit.between
treats the upper bound as exclusive, but we actually want to count it as one day here.
CodePudding user response:
To approach this problem, you need to consider the following cases:
dates belong to the same year and month;
dates belong to different year and/or month;
dates are invalid.
When dates belong to the same year and month, then the result would be the difference in days between the two dates divided by the number of days in this month, which can be found using LocalDate.lengthOfMonth()
.
In general the range of dates can be split into three parts:
- two fractional parts at the beginning and at the end of the given range of dates (both could be evaluated using algorithm for the simplest case when both data belong to the same year/month)
- the whole part, we can use
ChronoUnit.MONTHS.between()
to calculate it.
Here's how implementation might look like (d1
- inclusive, d2
- exclusive):
public static double getFractionalMonthDiff(LocalDate d1, LocalDate d2) {
if (d1.isAfter(d2)) throw new IllegalArgumentException(); // or return a value like -1
int daysInMonthD1 = d1.lengthOfMonth();
if (d1.getYear() == d2.getYear() && d1.getMonth() == d2.getMonth()) { // dates belong to same month and year
return (d2.getDayOfMonth() - d1.getDayOfMonth())
/ (double) daysInMonthD1;
}
LocalDate endOfMonthD1 = d1.withDayOfMonth(daysInMonthD1);
LocalDate startOfMonthD2 = d2.withDayOfMonth(1);
return getFractionalMonthDiff(d1, endOfMonthD1)
getFractionalMonthDiff(startOfMonthD2, d2)
ChronoUnit.MONTHS.between(endOfMonthD1, startOfMonthD2);
}