Does Java have some analog of Oracle\'s function MONTHS_BETWEEN
?
I've the same problem and following the Oracle MONTHS_BETWEEN I have made some changes to @alain.janinm and @Guerneen4 answers in order to correct some cases:
Consider months between 31/07/1998 and 30/09/2013 ("dd/MM/yyyy") Oracle result : 182 Java method from @Guerneen4 answer : 181.96774193548387
The problem is that according to specification if date1 and date2 are both last days of months, then the result is always an integer.
For easy understanding here you can find Oracle MONTHS_BETWEEN specifications: https://docs.oracle.com/cd/B19306_01/server.102/b14200/functions089.htm. I copy here to summarize:
"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."
Here's the changes that I've done get the closest result to the Oracle's months_between() function :
public static double monthsBetween(Date startDate, Date endDate) {
Calendar calSD = Calendar.getInstance();
Calendar calED = Calendar.getInstance();
calSD.setTime(startDate);
int startDayOfMonth = calSD.get(Calendar.DAY_OF_MONTH);
int startMonth = calSD.get(Calendar.MONTH);
int startYear = calSD.get(Calendar.YEAR);
calED.setTime(endDate);
int endDayOfMonth = calED.get(Calendar.DAY_OF_MONTH);
int endMonth = calED.get(Calendar.MONTH);
int endYear = calED.get(Calendar.YEAR);
int diffMonths = endMonth - startMonth;
int diffYears = endYear - startYear;
int diffDays = calSD.getActualMaximum(Calendar.DAY_OF_MONTH) == startDayOfMonth
&& calED.getActualMaximum(Calendar.DAY_OF_MONTH) == endDayOfMonth ? 0 : endDayOfMonth - startDayOfMonth;
return (diffYears * 12) + diffMonths + diffDays / 31.0;
}
In Joda Time there is a monthsBetween in the org.joda.time.Months class.
I had to migrate some Oracle code to java and haven't found the analog for months_between oracle function. While testing listed examples found some cases when they produce wrong results.
So, created my own function. Created 1600+ tests comparing results of db vs my function, including dates with time component - all work fine.
Hope, this can help someone.
public static double oracle_months_between(Timestamp endDate,Timestamp startDate) {
//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.
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String endDateString = sdf.format(endDate), startDateString = sdf.format(startDate);
int startDateYear = Integer.parseInt(startDateString.substring(0,4)), startDateMonth = Integer.parseInt(startDateString.substring(5,7)), startDateDay = Integer.parseInt(startDateString.substring(8,10));
int endDateYear = Integer.parseInt(endDateString.substring(0,4)), endDateMonth = Integer.parseInt(endDateString.substring(5,7)), endDateDay = Integer.parseInt(endDateString.substring(8,10));
boolean endDateLDM = is_last_day(endDate), startDateLDM = is_last_day(startDate);
int diffMonths = -startDateYear*12 - startDateMonth + endDateYear * 12 + endDateMonth;
if (endDateLDM && startDateLDM || extract_day(startDate) == extract_day(endDate)){
// 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.
return (double)(diffMonths);
}
double diffDays = (endDateDay - startDateDay)/31.;
Timestamp dStart = Timestamp.valueOf("1970-01-01 " + startDateString.substring(11)), dEnd = Timestamp.valueOf("1970-01-01 " + endDateString.substring(11));
return diffMonths + diffDays + (dEnd.getTime()-dStart.getTime())/1000./3600./24./31.;
}
public static boolean is_last_day(Timestamp ts){
Calendar calendar = Calendar.getInstance();
calendar.setTime(ts);
int max = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
return max == Integer.parseInt((new SimpleDateFormat("dd").format(ts)));
}
The previous answers are not perfect because they do not handle dates such as Feb 31.
Here is my iterative interpretation of MONTHS_BETWEEN in Javascript...
// Replica of the Oracle function MONTHS_BETWEEN where it calculates based on 31-day months
var MONTHS_BETWEEN = function(d1, d2) {
// Don't even try to calculate if it's the same day
if (d1.getTicks() === d2.getTicks()) return 0;
var totalDays = 0;
var earlyDte = (d1 < d2 ? d1 : d2); // Put the earlier date in here
var laterDate = (d1 > d2 ? d1 : d2); // Put the later date in here
// We'll need to compare dates using string manipulation because dates such as
// February 31 will not parse correctly with the native date object
var earlyDteStr = [(earlyDte.getMonth() + 1), earlyDte.getDate(), earlyDte.getFullYear()];
// Go in day-by-day increments, treating every month as having 31 days
while (earlyDteStr[2] < laterDate.getFullYear() ||
earlyDteStr[2] == laterDate.getFullYear() && earlyDteStr[0] < (laterDate.getMonth() + 1) ||
earlyDteStr[2] == laterDate.getFullYear() && earlyDteStr[0] == (laterDate.getMonth() + 1) && earlyDteStr[1] < laterDate.getDate()) {
if (earlyDteStr[1] + 1 < 32) {
earlyDteStr[1] += 1; // Increment the day
} else {
// If we got to this clause, then we need to carry over a month
if (earlyDteStr[0] + 1 < 13) {
earlyDteStr[0] += 1; // Increment the month
} else {
// If we got to this clause, then we need to carry over a year
earlyDteStr[2] += 1; // Increment the year
earlyDteStr[0] = 1; // Reset the month
}
earlyDteStr[1] = 1; // Reset the day
}
totalDays += 1; // Add to our running sum of days for this iteration
}
return (totalDays / 31.0);
};
You can do that with :
public static int monthsBetween(Date minuend, Date subtrahend){
Calendar cal = Calendar.getInstance();
cal.setTime(minuend);
int minuendMonth = cal.get(Calendar.MONTH);
int minuendYear = cal.get(Calendar.YEAR);
cal.setTime(subtrahend);
int subtrahendMonth = cal.get(Calendar.MONTH);
int subtrahendYear = cal.get(Calendar.YEAR);
return ((minuendYear - subtrahendYear) * (cal.getMaximum(Calendar.MONTH)+1)) +
(minuendMonth - subtrahendMonth);
}
According to this documentation MONTHS_BETWEEN return a fractional result, I think this method do the same :
public static void main(String[] args) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
Date d = sdf.parse("02/02/1995");
Date d2 = sdf.parse("01/01/1995");
System.out.println(monthsBetween(d, d2));
}
public static double monthsBetween(Date baseDate, Date dateToSubstract){
Calendar cal = Calendar.getInstance();
cal.setTime(baseDate);
int baseDayOfYear = cal.get(Calendar.DAY_OF_YEAR);
int baseMonth = cal.get(Calendar.MONTH);
int baseYear = cal.get(Calendar.YEAR);
cal.setTime(dateToSubstract);
int subDayOfYear = cal.get(Calendar.DAY_OF_YEAR);
int subMonth = cal.get(Calendar.MONTH);
int subYear = cal.get(Calendar.YEAR);
//int fullMonth = ((baseYear - subYear) * (cal.getMaximum(Calendar.MONTH)+1)) +
//(baseMonth - subMonth);
//System.out.println(fullMonth);
return ((baseYear - subYear) * (cal.getMaximum(Calendar.MONTH)+1)) +
(baseDayOfYear-subDayOfYear)/31.0;
}
Actually, I think the correct implementation is this one:
public static BigDecimal monthsBetween(final Date start, final Date end, final ZoneId zone, final int scale ) {
final BigDecimal no31 = new BigDecimal(31);
final LocalDate ldStart = start.toInstant().atZone(zone).toLocalDate();
final LocalDate ldEnd = end.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
final int endDay = ldEnd.getDayOfMonth();
final int endMonth = ldEnd.getMonthValue();
final int endYear = ldEnd.getYear();
final int lastDayOfEndMonth = ldEnd.lengthOfMonth();
final int startDay = ldStart.getDayOfMonth();
final int startMonth = ldStart.getMonthValue();
final int startYear = ldStart.getYear();
final int lastDayOfStartMonth = ldStart.lengthOfMonth();
final BigDecimal diffInMonths = new BigDecimal((endYear - startYear)*12+(endMonth-startMonth));
final BigDecimal fraction;
if(endDay==startDay || (endDay==lastDayOfEndMonth && startDay==lastDayOfStartMonth)) {
fraction = BigDecimal.ZERO;
}
else {
fraction = BigDecimal.valueOf(endDay-startDay).divide(no31, scale, BigDecimal.ROUND_HALF_UP);
}
return diffInMonths.add(fraction);
}
public static BigDecimal monthsBetween(final Date start, final Date end) {
return monthsBetween(start, end, ZoneId.systemDefault(), 20);
}