Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Adding Period to startDate doesn't produce endDate

I have two LocalDates declared as following:

val startDate = LocalDate.of(2019, 10, 31)  // 2019-10-31
val endDate = LocalDate.of(2019, 9, 30)     // 2019-09-30

Then I calculate the period between them using Period.between function:

val period = Period.between(startDate, endDate) // P-1M-1D

Here the period has the negative amount of months and days, which is expected given that endDate is earlier than startDate.

However when I add that period back to the startDate, the result I'm getting is not the endDate, but the date one day earlier:

val endDate1 = startDate.plus(period)  // 2019-09-29

So the question is, why doesn't the invariant

startDate.plus(Period.between(startDate, endDate)) == endDate

hold for these two dates?

Is it Period.between who returns an incorrect period, or LocalDate.plus who adds it incorrectly?

like image 900
Ilya Avatar asked Oct 26 '19 18:10

Ilya


1 Answers

If you look how plus is implemented for LocalDate

@Override
public LocalDate plus(TemporalAmount amountToAdd) {
    if (amountToAdd instanceof Period) {
        Period periodToAdd = (Period) amountToAdd;
        return plusMonths(periodToAdd.toTotalMonths()).plusDays(periodToAdd.getDays());
    }
    ...
}

you'll see plusMonths(...) and plusDays(...) there.

plusMonths handles cases when one month has 31 days, and the other has 30. So the following code will print 2019-09-30 instead of non-existent 2019-09-31

println(startDate.plusMonths(period.months.toLong()))

After that, subtracting one day results in 2019-09-29. This is the correct result, since 2019-09-29 and 2019-10-31 are 1 month 1 day apart

The Period.between calculation is weird and in this case boils down to

    LocalDate end = LocalDate.from(endDateExclusive);
    long totalMonths = end.getProlepticMonth() - this.getProlepticMonth();
    int days = end.day - this.day;
    long years = totalMonths / 12;
    int months = (int) (totalMonths % 12);  // safe
    return Period.of(Math.toIntExact(years), months, days);

where getProlepticMonth is total number of months from 00-00-00. In this case, it's 1 month and 1 day.

From my understanding, it's a bug in a Period.between and LocalDate#plus for negative periods interaction, since the following code has the same meaning

val startDate = LocalDate.of(2019, 10, 31)
val endDate = LocalDate.of(2019, 9, 30)
val period = Period.between(endDate, startDate)

println(endDate.plus(period))

but it prints the correct 2019-10-31.

The problem is that LocalDate#plusMonths normalises date to be always "correct". In the following code, you can see that after subtracting 1 month from 2019-10-31 the result is 2019-09-31 that is then normalised to 2019-10-30

public LocalDate plusMonths(long monthsToAdd) {
    ...
    return resolvePreviousValid(newYear, newMonth, day);
}

private static LocalDate resolvePreviousValid(int year, int month, int day) {
    switch (month) {
        ...
        case 9:
        case 11:
            day = Math.min(day, 30);
            break;
    }
    return new LocalDate(year, month, day);
}
like image 175
Evgeny Bovykin Avatar answered Sep 18 '22 12:09

Evgeny Bovykin