I need to find the no. of months between 2 dates, both months inclusive. I am using ChronoUnit.Months.between, but am getting unexpected results.
Code works for : Jan 31 - July 31
Fails for: Jan 31 - June 30
CODE THAT FAILS:
import java.time.LocalDate;
import static java.time.temporal.ChronoUnit.MONTHS;
public class HelloWorld{
public static void main(String []args){
LocalDate startDate=LocalDate.of(2018,01,31);
LocalDate endDate=LocalDate.of(2018,6,30);
//Both dates inclusive, hence adding 1
long noOfMonths = (MONTHS.between(startDate,endDate)) + 1L;
System.out.println(" ********* No. of months between the two dates : " + noOfMonths);
}
}
Expected: 6 (Jan, Feb, Mar, Apr, May, Jun) Actual: 5
Code that works:
import java.time.LocalDate;
import static java.time.temporal.ChronoUnit.MONTHS;
public class HelloWorld{
public static void main(String []args){
LocalDate startDate=LocalDate.of(2018,01,31);
LocalDate endDate=LocalDate.of(2018,7,31);
//Both dates inclusive, hence adding 1
long noOfMonths = (MONTHS.between(startDate,endDate)) + 1L;
System.out.println(" ********* No. of months between the two dates : " + noOfMonths);
}
}
Expected: 7 (Jan, Feb, Mar, Apr, May, Jun,July) Actual: 7
Do you see the same behaviour? Is there any alternate way of doing this?
Thanks
The other two Answers by davidxxx and by Ole V.V. are both correct and informative.
I want to add about an alternative approach commonly used in date-time work for defining a span of time: Half-Open. In Half-Open approach, the beginning is inclusive while the ending is exclusive. So the months of the first half of the year (Jan, Feb, Mar, Apr, and Jun) are tracked as starting on January 1 and running up to, but not including, July 1.
Sometimes we intuitively use this approach in our daily life. For example, if a classroom of children breaks for lunch from 12:00 noon to 1 PM, the students are expected to be in their seats before the bell strikes at 1 PM. Lunch break runs up to, but does not include, 1 PM.
I believe you will find consistent use of the Half-Open approach makes your code easier to read, debug, and maintain.
The modern java.time classes use this approach in defining a span of time.
LocalDateRange
The Answer by Ole V.V. wisely suggests you look at YearMonth
class to help your work.
Another helpful class is from the ThreeTen-Extra project: org.threeten.extra.LocalDateRange
. You can represent your entire date range in a single object.
You can instantiate with a start date and a Period
of six months.
LocalDateRange ldr = LocalDateRange.of(
LocalDate.of( 2018 , Month.JANUARY , 1 ) ,
Period.ofMonths( 6 )
) ;
Or specify start and stop dates.
LocalDateRange ldr = LocalDateRange.of(
LocalDate.of( 2018 , Month.JANUARY , 1 ) ,
LocalDate.of( 2018 , Month.JULY , 1 ) // Half-open.
) ;
While I don't recommend it, if you insist on using the Fully-Closed approach where beginning and ending are both inclusive, the LocalDateRange
does offer LocalDateRange.ofClosed()
.
LocalDateRange ldr = LocalDateRange.ofClosed( // `ofClosed` vs `of`.
LocalDate.of( 2018 , Month.JANUARY , 1 ) ,
YearMonth( 2018 , Month.JUNE ).atEndOfMonth() // Fully-closed.
) ;
You can get a count of months from a LocalDateRange
via the Period
class.
LocalDateRange ldr = LocalDateRange.of(
LocalDate.of( 2018 , Month.JANUARY , 1 ) ,
Period.ofMonths( 6 )
) ;
Period p = ldr.toPeriod() ;
long totalMonths = p.toTotalMonths() ; // 6
The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date
, Calendar
, & SimpleDateFormat
.
The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.
To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.
You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.*
classes.
Where to obtain the java.time classes?
The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval
, YearWeek
, YearQuarter
, and more.
davidxxx in the other answer has already explained why your code gave an unexpected result. Allow me to add that if you’re interested in months rather than dates, the YearMonth
class is the good and correct one for you to use:
YearMonth startMonth = YearMonth.of(2018, Month.JANUARY);
YearMonth endMonth = YearMonth.of(2018, Month.JUNE);
// Both months inclusive, hence adding 1
long noOfMonths = ChronoUnit.MONTHS.between(startMonth, endMonth) + 1;
System.out.println(" ********* No. of months between the two months (inclusive): " + noOfMonths);
Output:
********* No. of months between the two months (inclusive): 6
This obviously also eliminates the problems coming from months having unequal lengths.
If you already got LocalDate
objects, it’s probably worth converting:
LocalDate startDate = LocalDate.of(2018, Month.JANUARY, 31);
YearMonth startMonth = YearMonth.from(startDate);
As an aside, do not use two-digit 01
for a month. For January it works, but neither 08
nor 09
will work for August or September, so it’s a bad habit to acquire. Java (and many other languages) understand numbers beginning with 0
as octal numbers.
In terms of calendar, your question is understandable : the two comparisons represent 1 complete month.
But java.time.temporal.ChronoUnit.between
doesn't reason in this way but in terms of complete units.
And the result is expected according to its javadoc :
The calculation returns a whole number, representing the number of complete units between the two temporals. For example, the amount in hours between the times 11:30 and 13:29 will only be one hour as it is one minute short of two hours.
The LocalDate.until
javadoc that is used under the hood by ChronoUnit.between()
is more explicit for your case and also gives an interesting example about between()
used with MONTHS
as TemporalUnit
:
The calculation returns a whole number, representing the number of complete units between the two dates. For example, the amount in months between 2012-06-15 and 2012-08-14 will only be one month as it is one day short of two months.
In your working example :
LocalDate startDate=LocalDate.of(2018,01,31);
LocalDate endDate=LocalDate.of(2018,7,31);
long noOfMonths = MONTHS.between(startDate,endDate);
You have 31 days of month / 31 days of month, on 6 months
=> 6 months
.
In your failing example :
LocalDate startDate=LocalDate.of(2018,01,31);
LocalDate endDate=LocalDate.of(2018,6,30);
long noOfMonths = MONTHS.between(startDate,endDate);
You have 30 days of month / 31 days of month, on 5 months
=> 4 months + 1 month short of 1 day
<=> 4 months
(rounded).
If you would write something like :
LocalDate startDate=LocalDate.of(2018,01,30);
LocalDate endDate=LocalDate.of(2018,6,30);
long noOfMonths = MONTHS.between(startDate,endDate);
You would have 30 days of month / 30 days of month, on 5 months
=> 5 months
.
About your question :
Is there any alternate way of doing this?
The most simple : set the day of month to 1
or to any same number in both dates.
If it is not suitable for your requirement, you could check if the two dates are the last day of their current month, in this case, set it to the same month value.
You could write something like :
int lastMonthStart = startDate.getMonth()
.length(startDate.isLeapYear());
int lastMonthEnd = endDate.getMonth()
.length(endDate.isLeapYear());
if (startDate.getDayOfMonth() == lastMonthStart && endDate.getDayOfMonth() == lastMonthEnd) {
startDate = startDate.withDayOfMonth(1);
endDate = endDate.withDayOfMonth(1);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With