axstel
axstel

Reputation: 151

ZonedDateTime returns wrong amount of months for a date until another end date

Using ZonedDateTime.until returns a wrong amount of months for a given date until a given end date. Looks like something to do with February ;)

This is the test I have written

    @Test
    fun `three months from end of november till first of march`() {
        val dateOfNow = LocalDate.of(2022, 11, 30)
        val timeOfNow = LocalTime.of(0, 0, 0, 1)
        val dateTimeOfNow = LocalDateTime.of(dateOfNow, timeOfNow)
        val timeZoneOfTestland = ZoneOffset.of("+00:00")
        val zonedDateTimeOfNow = dateTimeOfNow.atZone(timeZoneOfTestland)

        val dateEnd = LocalDate.of(2023, 3, 1)
        val timeEnd = LocalTime.of(0, 0, 0, 0)
        val dateTimeEnd = LocalDateTime.of(dateEnd, timeEnd)
        val zonedDateEnd = dateTimeEnd.atZone(timeZoneOfTestland)

        val until = zonedDateTimeOfNow.until(zonedDateEnd, ChronoUnit.MONTHS)
        assertEquals(3, until)
    }

And it returns 2 instead of expected 3.

The test succeeds when I change nano seconds of dateTimeOfNow

    val timeOfNow = LocalTime.of(0, 0, 0, 0)

But it fails again when I set the date to Nov 29th

    val dateOfNow = LocalDate.of(2022, 11, 29)
    val timeOfNow = LocalTime.of(0, 0, 0, 1)

In my point of view, it's clearly 3 months until March 1st 2023. In any case.

I tried it without year change using these dates

    val dateOfNow = LocalDate.of(2022, 1, 31)
    val timeOfNow = LocalTime.of(0, 0, 0, 1)
    val dateTimeOfNow = LocalDateTime.of(dateOfNow, timeOfNow)
    val timeZoneOfTestland = ZoneOffset.of("+00:00")
    val zonedDateTimeOfNow = dateTimeOfNow.atZone(timeZoneOfTestland)

    val dateEnd = LocalDate.of(2022, 5, 1)
    val timeEnd = LocalTime.of(0, 0, 0, 0)
    val dateTimeEnd = LocalDateTime.of(dateEnd, timeEnd)
    val zonedDateEnd = dateTimeEnd.atZone(timeZoneOfTestland)

    val until = zonedDateTimeOfNow.until(zonedDateEnd, ChronoUnit.MONTHS)
    assertEquals(3, until)

But it fails as well with until == 2 instead of 3.

Any explanation for that?

Upvotes: 2

Views: 461

Answers (2)

axstel
axstel

Reputation: 151

Looks like using Date instead of DateTime is the best way to calculate amount of months between two dates. Since my function gets has two ZonedDateTime parameters I'm going to do something like this

LocalDate.from(zonedDateTimeOfNow).until(LocalDate.from(zonedDateTimeEnd), ChronoUnit.MONTHS)

Upvotes: 0

Arvind Kumar Avinash
Arvind Kumar Avinash

Reputation: 79395

Check the following documentation from OffsetDateTime#until (emphasis mine):

The calculation returns a whole number, representing the number of complete units between the two date-times. For example, the amount in months between 2012-06-15T00:00Z and 2012-08-14T23:59Z will only be one month as it is one minute short of two months.

In your case, the difference is caused due to 1 nanosecond. Keep in mind that 2023-02-29 and 2023-02-30 do not exist.

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;

public class Main {
    public static void main(String[] args) {
        // 2 months
        // 2022-11-30T0:0:0:1 to 2022-12-30T0:0:0:1 - complete
        // 2022-12-30T0:0:0:1 to 2023-01-30T0:0:0:1 - complete
        // 2023-01-30T0:0:0:1 to 2023-03-01T0:0:0:1 would have been one month but
        // the endtime is 2023-03-01T0:0:0:0, 1 nanosecond short
        System.out.println(LocalDateTime.of(2022, 11, 30, 0, 0, 0, 1).until(LocalDateTime.of(2023, 3, 1, 0, 0, 0, 0),
                ChronoUnit.MONTHS));

        // 3 months as explained above
        System.out.println(LocalDateTime.of(2022, 11, 30, 0, 0, 0, 1).until(LocalDateTime.of(2023, 3, 1, 0, 0, 0, 1),
                ChronoUnit.MONTHS));

        // 3 months
        // 2022-11-30T0:0:0:0 to 2022-12-30T0:0:0:0 - complete
        // 2022-12-30T0:0:0:0 to 2023-01-30T0:0:0:0 - complete
        // 2023-01-30T0:0:0:0 to 2023-03-01T0:0:0:0 - complete
        System.out.println(LocalDateTime.of(2022, 11, 30, 0, 0, 0, 0).until(LocalDateTime.of(2023, 3, 1, 0, 0, 0, 0),
                ChronoUnit.MONTHS));

        // 3 months
        // 2022-11-28T0:0:0:1 to 2022-12-28T0:0:0:1 - complete
        // 2022-12-28T0:0:0:1 to 2023-01-28T0:0:0:1 - complete
        // 2023-01-28T0:0:0:1 to 2023-02-28T0:0:0:1 - complete
        System.out.println(LocalDateTime.of(2022, 11, 28, 0, 0, 0, 1).until(LocalDateTime.of(2023, 3, 1, 0, 0, 0, 0),
                ChronoUnit.MONTHS));
    }
}

Output:

2
3
3
3

Upvotes: 2

Related Questions