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.
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
I was expecting to get Super Tuesday is 3 days after leap day.
This was a wrong expectation. See how Java compared two instances of LocalDate:
int compareTo0(LocalDate otherDate) {
int cmp = (year - otherDate.year);
if (cmp == 0) {
cmp = (month - otherDate.month);
if (cmp == 0) {
cmp = (day - otherDate.day);
}
}
return cmp;
}
Since the year was same (2020), it gave you the difference between months which is 3 - 2 = 1.
You should use java.time.temporal.ChronoUnit::between to find the number of days between two instances of LocalDate.
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
public class Main {
public static void main(String[] args) {
System.out.println(ChronoUnit.DAYS.between(LocalDate.of(2020, 2, 29), LocalDate.of(2020, 4, 3)));
}
}
Output:
34
Related
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 = (19/31 + 31/31 + 31/31 + 24/31) = 2.793
According to the calendar we replace the 31s with the correct number of days for that specific year and month
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 and lastDayOfStartMonth
the whole number of months between lastDayOfStartMonth and firstDayOfEndMonth.
the fractional number of a month between firstDayOfEndMonth and end.
Then there is the edge case of when both dates are within the same month, which is easy to handle.
By using this definition, the nice property that the number of months between the first day of any two months is always a whole number is maintained.
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.
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 the general case, 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 the approach 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
if (d1.getYear() == d2.getYear() && d1.getMonth() == d2.getMonth()) { // dates belong to same month and year
return getFractionalPartOfMonth(d2.getDayOfMonth() - d1.getDayOfMonth(), d1.lengthOfMonth());
}
int monthLen1 = d1.lengthOfMonth();
return getFractionalPartOfMonth(monthLen1 - (d1.getDayOfMonth() - 1), monthLen1) // from the given day of month of the First Date d1 Inclusive to the Last day of month
+ getFractionalPartOfMonth(d2.getDayOfMonth() - 1, d2.lengthOfMonth()) // from the First day of month to given day of month of the Second Date d2 Exclusive (for that reason 1 day was subtracted, and similarly on the previous line 1 day was added)
+ ChronoUnit.MONTHS.between(d1.withDayOfMonth(monthLen1), d2.withDayOfMonth(1));
}
public static double getFractionalPartOfMonth(int daysInterval, int monthLength) {
return daysInterval / (double) monthLength;
}
I'm a newbie to Java. I am given 3 times, in the "HH:mm:ss" format. I'm supposed to take an input(in integers(and must be in minutes)) add it to the 3rd time. If the sum of time is less than the 1st and 2nd time(1st time <=2nd time), it must print 1. If it is only less than the 2nd time, it should print 2. If neither is less, it prints false.
However, the values of the double variable I'm mapping the date to are returning
values in the negative sometimes (Please check the output).
Could someone help me understand why? Thanks.
import java.util.*;
import java.text.*;
public class TimeTrial2 {
public static void main(String q[]) throws ParseException
{
Scanner x=new Scanner(System.in);
SimpleDateFormat y=new SimpleDateFormat("HH");
SimpleDateFormat k=new SimpleDateFormat("HH:mm:ss");
String b=x.nextLine();
Date c=k.parse(b);//1st Time
String d=x.nextLine();
Date e=k.parse(d);//2nd Time
String f=x.nextLine();
Date g=k.parse(f);//Time to be added to the input
int a=x.nextInt();//Input time i
a=a/60;
Date z=y.parse(Integer.toString(a));
double p=(z.getTime()+g.getTime());
double r=c.getTime();
double s=e.getTime();
System.out.println(p+"\n"+r+"\n"+s); `//to check the values of p,r and s`
if(r>=p)
{
System.out.println("1");
}
else if(s>=p)
{
System.out.println("2");
}
else
{
System.out.println("FALSE!");
}
}
}
Input:
4:30:00
6:00:00
4:00:00
60
This is my output:
-2.16E7
-3600000.0
1800000.0
1
Edit: Adding 330 minutes (+5:30 GMT) rectifies the issue.
However, I'd recommend using Ole V.V's answer as it is much easier. I was unaware that such a date class and method existed. Thank you all for your help.
Consider how you add the time to the Date. I tried out the following code for adding minutes to the date and it worked.
Calendar calendar = GregorianCalendar.getInstance();
calendar.setTime(g);
calendar.add(GregorianCalendar.MINUTE, a);
Input:
g = 5:00:00
a = 40
Output:
Thu Jan 01 05:00:00 IST 1970
Thu Jan 01 05:40:00 IST 1970
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Scanner;
public class TimeTrial3 {
public static void main(String... commandLineArguments) {
Scanner consoleInput = new Scanner(System.in);
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("H:mm:ss");
String time1String = consoleInput.nextLine();
LocalTime time1 = LocalTime.parse(time1String, timeFormatter);
String time2String = consoleInput.nextLine();
LocalTime time2 = LocalTime.parse(time2String, timeFormatter);
if (time2.isBefore(time1)) {
System.err.println("First time must be before or equal to second time, they were"
+ time1 + " and " + time2);
System.exit(1);
}
String timeToAddString = consoleInput.nextLine();
LocalTime timetoAdd = LocalTime.parse(timeToAddString, timeFormatter);
int minutesToAdd = consoleInput.nextInt();
LocalTime timeToCompare = timetoAdd.plusMinutes(minutesToAdd);
if (timeToCompare.isBefore(time1))
{
System.out.println("1");
}
else if (timeToCompare.isBefore(time2))
{
System.out.println("2");
}
else
{
System.out.println("False");
}
}
}
Issues with your code
The Date class isn’t very well suited for representing a time of day only.
The Date and SimpleDateFormat classes are long outdated and the latter in particular notoriously troublesome. I recommend you don’t use them at all. Instead I am using and warmly recommending java.time, the modern Java date and time API. It is so much nicer to work with.
When you can use library classes for dates and/or times, do that. Don’t use double nor long.
An observation, not really a problem: It seems you are in a time zone at offset -05:30 from UTC (like Sri Lanka or India)? For example, your input of 4:30:00 was parsed into this time on January 1, 1970 (the epoch day) at offset -5:50, which is equal to 1 hour before the epoch, so it came out as -3600000.0 because -3600 seconds equals -1 hour. This is not a problem in itself as long as all your times are at the same offset. Then you can still compare them using before or after as harsha kumar Reddy said in another answer.
The trouble comes when you try to add the milliseconds from two Date objects. The third time, 4:00:00, is parsed into 1 hour 30 min before the epoch, and the 60 minutes into 4 hours 30 minutes before the epoch. When adding the two you expect a time of 5:00:00 (in your time zone). Instead you get 6 hours before the epoch, which is printed as -2.16E7 (on my computer I get a different incorrect result because I am in a different time zone).
By dividing the minutes from input by 60 to obtain hours, you are losing precision. For example, if the input was 90 minutes (one and a half hours), you get just 1 hour after division.
And as mentioned in the comment, you should use explanatory variable names. You may have had an idea behind the names you chose, but I didn’t get it.
Link
Oracle tutorial: Date Time explaining how to use java.time.
Try using equals() , before() , after() methods of Date class .
refer java Api docs
How do I compare two Periods in java 8?
E.g.
Period one = Period.of(10,0,0);
Period two = Period.of(8,0,0);
here in this case one is greater than two.
It is true that the comparison of two Period objects does not make sense in a general case, due to the undefined standard length of a month.
However, in many situations you can quite well live with an implementation similar to that which follows. The contract will be similar to the contract of compareTo():
public int comparePeriodsApproximately(Period p1, Period p2) {
return period2Days(p1) - period2Days(p2);
}
private int period2Days(Period p) {
if (p == null) {
return 0;
}
return (p.getYears() * 12 + p.getMonths()) * 30 + p.getDays();
}
Rightly said by JB Nizet.
You cannot compare Periods, as per java doc in Period class there is similar class to Period (Duration) available in java, you can use that depends on your business requirement.
"Durations and periods differ in their treatment of daylight savings time when added to ZonedDateTime. A Duration will add an exact number of seconds, thus a duration of one day is always exactly 24 hours. By contrast, a Period will add a conceptual day, trying to maintain the local time."
Period period = Period.of(10, 0, 0);
Period period2 = Period.of(10, 0, 0);
// No compareTo method in period
System.out.println(period.compareTo(period2));
Duration duration = Duration.ofDays(3);
Duration duration2 = Duration.ofDays(3);
// Can compare durations as it gives you the exact time
System.out.println(duration.compareTo(duration2));
In case you have a period of months and period of years you can do as follows:
Period periodOfMonths = Period.ofMonths(16);
Period periodOfYears = Period.ofYears(1);
// Check if period of months is ghreather that the period of years
System.out.println(periodOfMonths.toTotalMonths() > periodOfYears.toTotalMonths());
i like the answer of Martin Cassidy, but propose to use a hard coded LocalDate instead of LocalDate.now, for example LocalDate.MIN
public static int compareTo(final Period first, final Period second)
{
return LocalDate.MIN.plus(first).compareTo(LocalDate.MIN.plus(second));
}
This one should be bullet-proof. Whether it’s useful?
I claim that you get the smallest possible amount of days from a given number of years and months by counting forward from February 1, 2097 since you are first counting February (28 days) and it takes a long time until you get two months with 31 days in a row (July and August) and still longer until you hit a leap year (2104). Also 2200 and 2300 are non-leap years, should the counting reach that far. Conversely you get the greatest possible number of days when counting backward from the same date. You start by counting through 2 months # 31 days (January and December). The first February you encounter is in 2096, a leap year, so 29 days. And 2000 is a leap year.
So my trick is to count the difference between the two periods both ways from the mentioned date. If counts agree that the difference is positive (or negative), then two (or one) will always be longer no matter which day we count from.
In some cases the counts will not agree. Then I refuse to call a longer period.
Period one = Period.of(10, 0, 0);
Period two = Period.of(8, 0, 0);
if (one.normalized().equals(two.normalized())) {
System.out.println("They are the same.");
} else {
Period diff = two.minus(one);
LocalDate anchor = LocalDate.of(2097, Month.FEBRUARY, 1);
LocalDate plusDiff = anchor.plus(diff);
LocalDate minusDiff = anchor.minus(diff);
if (plusDiff.isAfter(anchor) && minusDiff.isBefore(anchor)) {
System.out.println("Two is greater (unambiguously)");
} else if (plusDiff.isBefore(anchor) && minusDiff.isAfter(anchor)) {
System.out.println("One is greater (unambiguously)");
} else {
System.out.println("Cannot decide");
}
}
Output in this case (using the periods from the question):
One is greater (unambiguously)
The normalized method I am using in the first comparison converts between years and months ensuring that the months are in the interval from -11 through +11 and have the same sign as the years (except if years are 0). This can be done unambiguously since there are always 12 months in a year.
You may be able to refine it to report that one period is longer than or equal to the other. For example if one is a month and two is 31 days, we know that two is at least as long as one, even though we can’t decide whether it’s also strictly longer.
This can be done with a proper compareTo() approach by borrowing from LocalDate.
public static int compareTo(final Period first, final Period second)
{
final LocalDate now = LocalDate.now();
return now.plus(first).compareTo(now.plus(second));
}
If I do:
public static void main(String[] args) {
LocalDate d1 = LocalDate.of(2015, Month.MARCH, 12);
LocalDate d2 = LocalDate.of(2015, Month.APRIL, 13);
System.out.println(d1.until(d2).getDays());
// Prints 11
LocalDate d3 = LocalDate.of(2015, Month.APRIL, 25);
// Prints 23.
Both of which are incorrect. The second output makes sense as there is 1 month and 23 days between them.
How do I get the total number of days between?
I would want the first output to be 32 Days and the second to be 44 days (the total number of days between
the two dates).
What am I doing wrong? I don't see a totalDays() method.
You should probably not use a period (which has year and month components) if you are only interested in days. One solution to your question is:
System.out.println(DAYS.between(d1, d2)); //32
or alternatively:
System.out.println(d1.until(d2, DAYS)); //32
Note: I'm using import static java.time.temporal.ChronoUnit.DAYS;
I don't know how much you want to do with dates and possible you don't need it but I really recommend Joda Time and here is how to get number of days between two dates.
In Java 8 we finally have good Dates library but if you don't use Java 8 then you have to get Joda Time
This question already has answers here:
How do I calculate someone's age in Java?
(28 answers)
Closed 9 years ago.
I am a newbie and appreciate if someone help me out.
When I tried to calculate age using the below source , it does not give me the value of what I want . For example : date->29/12/2010 , dob->30/12/1992 , it will give me 18 instead of 17.
Is there any method that I can code to return me 17yrs 11mths based on the above 2 dates instead of 18yrs0mths?
public double getAgeAsOf( Date date ) {
return ((date.getTime() - dob.getTime())/(1000*60*60*24))/365;
}
Thanks.
You can use Joda Time and compute a Period between two LocalDate values (which is what you've got here) using months and years as the units.
Sample code:
import org.joda.time.*;
public class Test {
public static void main(String[] args) {
LocalDate dob = new LocalDate(1992, 12, 30);
LocalDate date = new LocalDate(2010, 12, 29);
Period period = new Period(dob, date, PeriodType.yearMonthDay());
System.out.println(period.getYears() + " years and " +
period.getMonths() + " months");
}
}
(This uses a period type which includes days as well, but that won't affect the answer.)
In general, Joda Time is a much better API than using Date/Calendar - and you really don't want to get into the business of performing date calculations yourself if you can avoid it. It gets messy really quickly.
As per aioobe's answer, if you divide two integer expressions the arithmetic will be performed in integer arithmetic, which may not be what you want - but for date and time arithmetic, just let someone else do the hard work in the first place :)
The code above will use the ISO-8601 calendar by the way, which is basically the Gregorian calendar. If you want to use something else, specify it as another constructor argument after the year/month/day for LocalDate.
You have a few problems with your code:
You're doing integer division, which truncates the result to the closest lower integer.
For example, if you divide 729 by 365 you'll get 1 (and you've lost the fractional part, which you would need when calculating months etc)
Another problem is that you're using 365 days for one year, while it is actually closer to 365.25 (when including extra days due to leap years).
Here's a slightly improved snippet of code:
import java.util.Date;
public class Test {
static double msPerGregorianYear = 365.25 * 86400 * 1000;
static Date dob = new Date(1992, 12, 30);
public static double getAgeAsOf(Date date) {
return (date.getTime() - dob.getTime()) / msPerGregorianYear;
}
public static void main(String[] args) {
double years = getAgeAsOf(new Date(2010, 12, 29));
// years == 17.99315537303217
int yy = (int) years;
int mm = (int) ((years - yy) * 12);
// Prints "17 years and 11 moths."
System.out.printf("%d years and %d moths.", yy, mm);
}
}
If you're ever doing anything more complicated than figuring out the number of years given the number of milliseconds, I suggest you turn to some time-library such as Joda time as suggested by Jon Skeet.