Does Java have some analog of Oracle\'s function MONTHS_BETWEEN
?
I've run into the same need and started from @alain.janinm answer which is good but doesn't give the exact same result in some cases.
ex :
Consider months between 17/02/2013 and 11/03/2016 ("dd/MM/yyyy"
)
Oracle result : 36,8064516129032
Java method from @Alain.janinm answer : 36.74193548387097
Here's the changes i made, to get a closer result to Oracle's months_between()
function :
public static double monthsBetween(Date startDate, Date endDate){
Calendar cal = Calendar.getInstance();
cal.setTime(startDate);
int startDayOfMonth = cal.get(Calendar.DAY_OF_MONTH);
int startMonth = cal.get(Calendar.MONTH);
int startYear = cal.get(Calendar.YEAR);
cal.setTime(endDate);
int endDayOfMonth = cal.get(Calendar.DAY_OF_MONTH);
int endMonth = cal.get(Calendar.MONTH);
int endYear = cal.get(Calendar.YEAR);
int diffMonths = endMonth - startMonth;
int diffYears = endYear - startYear;
int diffDays = endDayOfMonth - startDayOfMonth;
return (diffYears * 12) + diffMonths + diffDays/31.0;
}
With this function the result of the call for the dates 17/02/2013 and 11/03/2016 is : 36.806451612903224
Note : From my understanding Oracle's months_between()
function considers that all months are 31 days long
The other Answers use the troublesome old Calendar
class that is now legacy, supplanted by the java.time classes.
MONTHS_BETWEEN
The doc says:
MONTHS_BETWEEN returns number of months between dates date1 and date2. If date1 is later than date2, then the result is positive. If date1 is earlier than date2, then the result is negative. If date1 and date2 are either the same days of the month or both last days of months, then the result is always an integer. Otherwise Oracle Database calculates the fractional portion of the result based on a 31-day month and considers the difference in time components date1 and date2.
LocalDate
The LocalDate class represents a date-only value without time-of-day and without time zone.
Retrieve a LocalDate
from the database using JDBC 4.2 and later. The java.sql.Date
class is now legacy, and can be avoided.
LocalDate start = myResultSet.getObject( … , LocalDate.class ) ; // Retrieve a `LocalDate` from database using JDBC 4.2 and later.
For our demo here, let’s simulate those retrieved dates.
LocalDate start = LocalDate.of( 2018 , Month.JANUARY , 23 );
LocalDate stop = start.plusDays( 101 );
Period
Calculate the elapsed time as a span of time unattached to the timeline, a Period.
Period p = Period.between( start , stop );
Extract the total number of months.
long months = p.toTotalMonths() ;
Extract the number of days part, the days remaining after calculating the months.
int days = p.getDays() ;
BigDecimal
For accuracy, use BigDecimal
. The double
and Double
types use floating-point technology, trading away accuracy for fast execution performance.
Convert our values from primitives to BigDecimal
.
BigDecimal bdDays = new BigDecimal( days );
BigDecimal bdMaximumDaysInMonth = new BigDecimal( 31 );
Divide to get our fractional month. The MathContext provides a limit to resolving the fractional number, plus a rounding mode to get there. Here we use the constant MathContext.DECIMAL32, because I am guessing the Oracle function is using 32-bit math. The rounding mode is RoundingMode.HALF_EVEN, the default specified by IEEE 754, and also known as “Banker’s rounding” which is more mathematically fair than “schoolhouse rounding” commonly taught to children.
BigDecimal fractionalMonth = bdDays.divide( bdMaximumDaysInMonth , MathContext.DECIMAL32 );
Add this fraction to our number of whole months, for a complete result.
BigDecimal bd = new BigDecimal( months ).add( fractionalMonth );
To more closely emulate the behavior of the Oracle function, you may want to convert to a double
.
double d = bd.round( MathContext.DECIMAL32 ).doubleValue();
Oracle did not document the gory details of their calculation. So you may need to do some trial-and-error experimentation to see if this code has results in line with your Oracle function.
Dump to console.
System.out.println( "From: " + start + " to: " + stop + " = " + bd + " months, using BigDecimal. As a double: " + d );
See this code run live at IdeOne.com.
From: 2018-01-23 to: 2018-05-04 = 3.3548387 months, using BigDecimal. As a double: 3.354839
Caveat: While I answered the Question as asked, I must remark: Tracking elapsed time as a fraction as seen here is unwise. Instead use the java.time classes Period
and Duration
. For textual representation, use the standard ISO 8601 format: PnYnMnDTnHnMnS. For example, the Period
seen in our example above: P3M11D
for three months and eleven days.
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.
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.