An analogue of the ORACLE MONTHS_BETWEEN function in Java

Does Java have some analogue of the Oracle MONTHS_BETWEEN function?

+7
source share
8 answers

You can do it 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); } 

Edit:

According to this documentation MONTHS_BETWEEN returns a fractional result, I think this method does 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; } 
+2
source

I ran into the same need and started with @ alain.janinm's answer, which is good but doesn't give exactly the same result in some cases.
ex:

Consider the months between 02/17/2013 and 11/03/2016 ( "dd/MM/yyyy" )
Oracle Result: 36,8064516129032
Java method from answer @ Alain.janinm: 36.74193548387097

Here are the changes I made to get a closer result to the Oracle 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; } 

Using this function, the query result for the dates 02/17/2013 and 03/11/2016 is: 36.806451612903224

Note. In my opinion, the Oracle months_between() function thinks that all months are 31 days long

+2
source

I had to port some Oracle code to java and did not find an analog of the months_between oracle function. When testing the listed examples, some cases were found when they lead to incorrect results.

So, I created my own function. 1600+ tests have been created comparing db results with my function, including dates with a time component - everything works 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))); } 
+2
source

In Joda Time, there is a period of months in org.joda. time.Months .

0
source

I have the same problem and after Oracle MONTHS_BETWEEN I made some changes to @ alain.janinm and @ Guerneen4 answers to fix some cases:

Consider the months between 07/31/1998 and 09/30/2013 ("dd / MM / yyyy"). Oracle result: 182 Java method from @ Guerneen4 answer: 181.96774193548387

The problem is that according to the specification, if date1 and date2 are the last months of the months, then the result is always an integer.

For convenience, you can find the Oracle MONTHS_BETWEEN specifications here: https://docs.oracle.com/cd/B19306_01/server.102/b14200/functions089.htm . I am copying here to summarize:

"returns the number of months between dates date1 and date 2. 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 on the same days of the month or both last months of the month, then the result is always Otherwise, Oracle Database calculates the fractional part of the result based on the 31-day month and considers the difference in the time components date1 and date2. "

Here the changes I made give the closest result to the Oracle 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; } 
0
source

In fact, I believe that the correct implementation is as follows:

 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); } 
0
source

java.time

Other answers use the nasty old Calendar class, which is now deprecated, being superseded by java.time classes.

MONTHS_BETWEEN

doc says :

MONTHS_BETWEEN returns the number of months between dates date1 and date2. If date1 is later than date2, the result is positive. If date1 is earlier than date2, the result is negative. If date1 and date2 are the same days of the month or the last two days of the months, then the result is always an integer. Otherwise, Oracle Database calculates the fractional part of the result based on the 31-day month and considers the difference in the time components date1 and date2.

LocalDate

The LocalDate class represents a value only for a date with no time and no time zone.

LocalDate from the database using JDBC 4.2 and later. The java.sql.Date class is now deprecated and can be avoided.

 LocalDate start = myResultSet.getObject( … , LocalDate.class ) ; // Retrieve a `LocalDate` from database using JDBC 4.2 and later. 

For our demonstration here, let's simulate the dates received.

 LocalDate start = LocalDate.of( 2018 , Month.JANUARY , 23 ); LocalDate stop = start.plusDays( 101 ); 

Period

Calculate elapsed time as a period of time not tied to the timeline, Period .

 Period p = Period.between( start , stop ); 

Extract the total number of months .

 long months = p.toTotalMonths() ; 

Extract the number of days, part , days remaining after calculating the months.

 int days = p.getDays() ; 

BigDecimal

Use BigDecimal for accuracy. The double and double types use technology technology with floating point , which allows you to quickly evaluate the effectiveness of the execution.

Convert our values ​​from primitives to BigDecimal .

 BigDecimal bdDays = new BigDecimal( days ); BigDecimal bdMaximumDaysInMonth = new BigDecimal( 31 ); 

Divide to get our fractional month. MathContext provides a limit to the resolution of a fractional number plus a rounding mode to get there. Here we use the constant MathContext.DECIMAL32 , because I assume that the Oracle function uses 32-bit math. Rounding mode RoundingMode.HALF_EVEN, defaults to IEEE 754 , and is also known as Rounding Bankers , which is more mathematically fair than school rounding, usually taught to children.

 BigDecimal fractionalMonth = bdDays.divide( bdMaximumDaysInMonth , MathContext.DECIMAL32 ); 

Add this share to our number of whole months to get the full result.

 BigDecimal bd = new BigDecimal( months ).add( fractionalMonth ); 

To more closely mimic the behavior of an Oracle function, you can convert to double .

 double d = bd.round( MathContext.DECIMAL32 ).doubleValue(); 

Oracle has not documented the details of its calculations. Thus, you may need to do a trial evaluation to see if this code matches the results of your Oracle function.

Dump for the console.

 System.out.println( "From: " + start + " to: " + stop + " = " + bd + " months, using BigDecimal. As a double: " + d ); 

See this code run on IdeOne.com .

From: 2018-01-23 to: 2018-05-04 = 3.3548387 months using BigDecimal. In double: 3.354839

Caution: While I was answering the question of how I was asked, I should note: keeping track of elapsed time as a particle, as shown here, is unreasonable. Use the java.time Period and Duration classes instead. For textual representation, use the standard ISO 8601 format : PnYnMnDTnHnMnS. For example, Period in our example above: P3M11D for three months and eleven days.


About java.time

The java.time framework is built into Java 8 and later. These classes supersede the nasty old legacy datetime classes such as java.util.Date , Calendar and SimpleDateFormat .

The Joda-Time project, now in maintenance mode , is advised to switch to the java.time classes.

To learn more, see the Oracle Tutorial . And search for qaru for many examples and explanations. JSR 310 specification .

Where to get java.time classes?

The ThreeTen-Extra project extends java.time with additional classes. This project is a proof of possible future additions to java.time. Here you can find useful classes such as Interval , YearWeek , YearQuarter and more .

0
source

The previous answers are not perfect, because they do not process dates, for example, February 31st.

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 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); }; 
-one
source

All Articles