java.time Comparison Is Not Consistent

Matan Itzhak :

I'm using LocalData objects in my code, and I've noticed that using compareTo method is not consistent.
I use the compareTo method in order to get the difference (in days) between the two dates.
However, it seems like it only works for the dates in the same months.

I'm attaching a code snippet to demonstrate this issue:

import java.text.MessageFormat;
import java.time.LocalDate;

public class timeComparison {
    public static void main(String[] args) {
        LocalDate valentinesDay = LocalDate.of(2020, 2, 14);
        LocalDate darwinDay = LocalDate.of(2020, 2, 12);
        LocalDate leapDay = LocalDate.of(2020, 2, 29);
        LocalDate superTuesday = LocalDate.of(2020, 3, 3);

        String valentinesVsDarwin = MessageFormat.format(
                "Valentine day is {0} days after Darwin day",
                valentinesDay.compareTo(darwinDay)
        );
        System.out.println(valentinesVsDarwin);

        String darwinVsLeap = MessageFormat.format(
                "Leap day is {0} days after Darwin day",
                leapDay.compareTo(darwinDay)
        );
        System.out.println(darwinVsLeap);

        String superTuesdayVsLeap = MessageFormat.format(
                "Super Tuesday is {0} days after leap day",
                superTuesday.compareTo(leapDay)
        );
        System.out.println(superTuesdayVsLeap);
    }
}

The output I get is:

Valentine day is 2 days after Darwin day
Leap day is 17 days after Darwin day
Super Tuesday is 1 days after leap day

I was expecting to get Super Tuesday is 3 days after leap day.

I would like to know what causes the problem and how can I get the difference between two separate dates.

Matan Itzhak :

TL;DR

The compareTo method is not meant for this use.
It is meant to be an indicator of order, not show time differences.

Solving the Mystery

In order to understand this better one can look at the source code of LocalDate.
On any IDE you can choose "Go To => Implementation" to view the source code.

public int compareTo(ChronoLocalDate other) {
    return other instanceof LocalDate ? this.compareTo0((LocalDate)other) : super.compareTo(other);
}

int compareTo0(LocalDate otherDate) {
    int cmp = this.year - otherDate.year;
    if (cmp == 0) {
        cmp = this.month - otherDate.month;
        if (cmp == 0) {
            cmp = this.day - otherDate.day;
        }
    }

    return cmp;
}

From the above source code, you can learn that the compareTo method does not return the difference between the date in days, but rather the difference between dates in the first parameter that has a difference (it could be years, could be months, or could be days).

What Is the Purpose of compareTo Methods

In order the understand the above code and its purpose,
one would need to understand a little bit how comparison in Java works.
Java's LocalDate class implements ChronoLocalDate, which is a comparable object.
This means that this object is capable of comparing itself with another object.
In order to do so, the class itself must implement the java.lang.Comparable interface to compare its instances.
As described in Java's API documentation:

This interface imposes a total ordering on the objects of each class that implements it. This ordering is referred to as the class's natural ordering, and the class's compareTo method is referred to as its natural comparison method.

Moreover, the method detail of the compareTo method is the following:

Compares this object with the specified object for order.
Returns a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object.

Meaning, that in order for the comparison to work we need to return an integer that corresponds with the above ruling. Therefore, the number itself doesn't have any meaning, rather than being an indication of the "order" of the objects. That is why the method, as shown above, does not calculate the difference in days but rather seeks the immediate indication for the natural order of the objects: first in years, then in months, and in the ends in days.
When you were comparing dates that had a different mount - the method showed just that.
The same would apply if you were to compare between a date in 2020 and a date in 2018 - you would get 2 / -2 as a result.

How You Should Measure Data Difference

We found out that compareTo isn't meant to be used as a measurement tool of date difference.
You can use either Period or ChronoUnit, depending on your needs.
From Java's Period and Duration tutorial:

ChronoUnit

The ChronoUnit enum, discussed in the The Temporal Package, defines the units used to measure time.
The ChronoUnit.between method is useful when you want to measure an amount of time in a single unit of time only, such as days or seconds.
The between method works with all temporal-based objects, but it returns the amount in a single unit only.

Period

To define an amount of time with date-based values (years, months, days), use the Period class.
The Period class provides various get methods, such as getMonths, getDays, and getYears, so that you can extract the amount of time from the period.
The total period of time is represented by all three units together: months, days, and years.
To present the amount of time measured in a single unit of time, such as days, you can use the ChronoUnit.between method.

Here's a code snippet demonstrating the usage of the mentioned above methods:

jshell> import java.time.LocalDate;

jshell> LocalDate darwinDay = LocalDate.of(2020, 2, 12);
darwinDay ==> 2020-02-12

jshell> LocalDate leapDay = LocalDate.of(2020, 2, 29);
leapDay ==> 2020-02-29

jshell> leapDay.compareTo(darwinDay);
 ==> 17

jshell> import java.time.Period;

jshell> Period.between(darwinDay, leapDay).getDays();
 ==> 17

jshell> import java.time.temporal.ChronoUnit;

jshell> ChronoUnit.DAYS.between(darwinDay, leapDay);
 ==> 17

jshell> LocalDate now = LocalDate.of(2020, 3, 28);
now ==> 2020-03-28

jshell> LocalDate yearAgo = LocalDate.of(2019, 3, 28);
yearAgo ==> 2019-03-28

jshell> yearAgo.compareTo(now);
 ==> -1

jshell> Period.between(yearAgo, now).getDays();
 ==> 0

jshell> ChronoUnit.DAYS.between(yearAgo, now);
 ==> 366

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=371628&siteId=1