在Java中使用ORACLE函数MONTHS_BETWEEN的模拟

Java是否具有Oracle函数MONTHS_BETWEEN一些模拟?

你可以这样做:

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

编辑:

根据这个文档 MONTHS_BETWEEN返回一个小数结果,我认为这个方法也是这样的:

 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; } 

我遇到了同样的需求并从@ alain.janinm回答开始,这很好但在某些情况下并没有给出完全相同的结果。
例如:

考虑从2013年2月17日到2016年3月11日之间的月份( "dd/MM/yyyy"
Oracle结果: 36,8064516129032
来自@ Alain.janinm的Java方法回答: 36.74193548387097

以下是我所做的更改,以便更接近Oracle的months_between()函数:

 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; } 

使用此function,日期为17/02/2013和36.806451612903224日的通话结果为: 36.806451612903224

注意:根据我的理解,Oracle的months_between()函数认为所有月份都是31天

我不得不将一些Oracle代码迁移到java,并且没有找到oracle函数之间的months_ analog。 虽然测试列出的示例在发现错误结果时发现了一些情况。

所以,创建了我自己的function。 创建了1600多个测试,比较了db与我的函数的结果,包括带有时间组件的日期 – 所有工作正常。

希望,这可以帮助别人。

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

在Joda Time中, org.joda.time.Months类中有一个monthBetween。

我遇到了同样的问题并且遵循Oracle MONTHS_BETWEEN我对@ alain.janinm和@ Guerneen4的答案进行了一些更改以纠正某些情况:

考虑31/07/1998和30/09/2013之间的月份(“dd / MM / yyyy”)Oracle结果:来自@ Guerneen的182 Java方法答案:181.96774193548387

问题是根据规范,如果date1和date2都是月的最后几天,那么结果总是一个整数。

为了便于理解,您可以找到Oracle MONTHS_BETWEEN规范: https ://docs.oracle.com/cd/B19306_01/server.102/b14200/functions089.htm。 我在这里复制总结一下:

“返回日期date1和date2之间的月数。如果date1晚于date2,则结果为正。如果date1早于date2,则结果为负。如果date1和date2是月份的同一天或在几个月的最后几天,结果总是一个整数。否则,Oracle数据库会根据31天的月份计算结果的小数部分,并考虑时间组件date1和date2的差异。“

以下是我所做的更改,得到了与Oracle的months_between()函数最接近的结果:

 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; } 

java.time

其他Answers使用现在遗留的麻烦的旧Calendar类,取而代之的是java.time类。

MONTHS_BETWEEN

医生说 :

MONTHS_BETWEEN返回date1和date2之间的月数。 如果date1晚于date2,则结果为正。 如果date1早于date2,则结果为负数。 如果date1和date2是该月的同一天或两个月的最后几天,则结果始终为整数。 否则,Oracle数据库会根据31天的月份计算结果的小数部分,并考虑时间组件date1和date2的差异。

LocalDate

LocalDate类表示没有时间且没有时区的仅日期值。

使用JDBC 4.2及更高版本从数据库中检索LocalDatejava.sql.Date类现在是遗留的,可以避免。

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

对于我们的演示,让我们模拟那些检索日期。

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

Period

将经过时间计算为未附加到时间轴的时间跨度,即时间段。

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

提取总月数 。

 long months = p.toTotalMonths() ; 

提取部分天数,计算月份后剩余的天数。

 int days = p.getDays() ; 

BigDecimal

为了准确,请使用BigDecimaldoubleDouble类型使用浮点技术 ,折衷准确性以实现快速执行性能。

将我们的值从基元转换为BigDecimal

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

除以获得我们的分数月份。 MathContext提供了解析小数的限制,以及到达那里的舍入模式。 这里我们使用常量MathContext.DECIMAL32 ,因为我猜测Oracle函数使用的是32位数学。 舍入模式是RoundingMode.HALF_EVEN ,由IEEE 754指定的默认值,也称为“银行家舍入” ,这在数学上比通常教给孩子的“校舍舍入”更公平。

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

将此分数添加到我们的整月数中,以获得完整的结果。

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

要更接近地模拟Oracle函数的行为,您可能希望转换为double

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

Oracle没有记录他们计算的血腥细节。 因此,您可能需要进行一些试错法实验,以查看此代码是否具有与Oracle函数一致的结果。

转储到控制台。

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

请参阅IdeOne.com上的此代码 。

从:2018-01-23到:2018-05-04 = 3.3548387个月,使用BigDecimal。 作为双:3.354839

警告:当我按照要求回答问题时,我必须注意: 跟踪经过的时间,如此处所示的分数是不明智的 。 而是使用java.time类PeriodDuration 。 对于文本表示,请使用标准ISO 8601格式 :PnYnMnDTnHnMnS。 例如,在上面的示例中看到的P3M11DP3M11D三个月和十一天。


关于java.time

java.time框架内置于Java 8及更高版本中。 这些类取代了麻烦的旧遗留日期时间类,如java.util.DateCalendarSimpleDateFormat

现在处于维护模式的Joda-Time项目建议迁移到java.time类。

要了解更多信息,请参阅Oracle教程 。 并搜索Stack Overflow以获取许多示例和解释。 规范是JSR 310 。

从哪里获取java.time类?

  • Java SE 8Java SE 9及更高版本
    • 内置。
    • 带有捆绑实现的标准Java API的一部分。
    • Java 9增加了一些小function和修复。
  • Java SE 6Java SE 7
    • 许多java.timefunction都被反向移植到ThreeTen-Backport中的 Java 6和7。
  • Android的
    • 更高版本的Android捆绑java.time类的实现。
    • 对于早期的Android, ThreeTenABP项目采用ThreeTen-Backport (如上所述)。 请参见如何使用ThreeTenABP ….

ThreeTen-Extra项目使用其他类扩展了java.time。 该项目是未来可能添加到java.time的试验场。 您可以在这里找到一些有用的课程,如IntervalYearWeekYearQuarter等。

实际上,我认为正确的实现是这样的:

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

以前的答案并不完美,因为它们不处理2月31日等日期。

这是我在Javascript中对MONTHS_BETWEEN的迭代解释…

  // 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); };