【问题标题】:Analog of ORACLE function MONTHS_BETWEEN in JavaJava中ORACLE函数MONTHS_BETWEEN的模拟
【发布时间】:2012-02-24 09:52:57
【问题描述】:

Java 有没有类似 Oracle 的函数MONTHS_BETWEEN

【问题讨论】:

  • 其实和那个问题不一样,主要是months_between返回一个分数,而另一个q要求整数。

标签: java oracle function date


【解决方案1】:

你可以这样做:

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

编辑:

根据这个documentationMONTHS_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;
} 

【讨论】:

  • Oracle 的months_between 返回一个分数,而不是一个整数。
  • 很好,但我认为它仍然是错误的。考虑 1 月 31 日 -> 2 月 28 日。我怀疑甲骨文会告诉你这些日期之间有 1.0 个月,而你有 28/31。重新创建函数几乎是不可能的,因为月份很奇怪,我们只能猜测 Oracle 是如何对其进行编程的。
  • 实际上,我错了,months_between 确实在该日期返回 28/31。但考虑 2 月 1 日至 3 月 1 日,它确实返回 1。
  • 啊,我忘了今年是闰年:1 月 31 日 -> 2 月 29 日有 1.0 个月。
  • @AdrianMouat 请参阅我对 Oracle 的确切功能的回答。
【解决方案2】:

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

【讨论】:

【解决方案3】:

之前的答案并不完美,因为它们不处理诸如 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);
    };

【讨论】:

  • 不错的尝试,但我怀疑这也行得通 - 我希望您需要一个“每月最后一天”的概念来供初学者使用。另外,它不是 Java...
【解决方案4】:

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

考虑 2013 年 2 月 2 日至 2016 年 3 月 11 日之间的月份 ("dd/MM/yyyy")
甲骨文结果: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;
} 

使用此函数,调用日期 17/02/2013 和 11/03/2016 的结果是:36.806451612903224

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

【讨论】:

    【解决方案5】:

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

    考虑 1998 年 7 月 31 日至 2013 年 9 月 30 日之间的月份(“dd/MM/yyyy”)Oracle 结果:来自@Guerneen4 的 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;
    }
    

    【讨论】:

      【解决方案6】:

      其实我觉得正确的实现是这个:

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

      【讨论】:

      • 仅供参考,这里使用的麻烦类现在是遗留的,应该避免使用,由 java.time 类取代。
      • 抱歉,Basil,现在好点了吗?
      【解决方案7】:

      java.time

      其他答案使用麻烦的旧 Calendar 类,现在是遗留的,被 java.time 类取代。

      MONTHS_BETWEEN

      doc says:

      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

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

      提取total number of months

      long months = p.toTotalMonths() ; 
      

      提取number of days part,计算月份后剩余的天数。

      int days = p.getDays() ;
      

      BigDecimal

      为了准确,请使用BigDecimaldoubleDouble 类型使用 floating-point technology,以牺牲准确性换取快速执行性能。

      将我们的值从原始值转换为BigDecimal

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

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

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

      看到这个code run live at IdeOne.com

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

      警告:虽然我按要求回答了问题,但我必须指出:将经过的时间作为分数跟踪是不明智的。而是使用 java.time 类 PeriodDuration。对于文本表示,请使用标准ISO 8601 format:PnYnMnDTnHnMnS。例如,在我们上面的示例中看到的PeriodP3M11D 三个月零十一天。


      关于java.time

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

      Joda-Time 项目现在位于maintenance mode,建议迁移到java.time 类。

      要了解更多信息,请参阅Oracle Tutorial。并在 Stack Overflow 上搜索许多示例和解释。规格为JSR 310

      从哪里获得 java.time 类?

      ThreeTen-Extra 项目通过附加类扩展了 java.time。该项目是未来可能添加到 java.time 的试验场。您可以在这里找到一些有用的类,例如IntervalYearWeekYearQuartermore

      【讨论】:

        【解决方案8】:

        我不得不将一些 Oracle 代码迁移到 java 并且没有找到类似的 months_between oracle 函数。在测试列出的示例时发现了一些产生错误结果的情况。

        所以,创建了我自己的函数。创建了 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)));
        }
        

        【讨论】:

          猜你喜欢
          • 2011-09-06
          • 2011-12-21
          • 1970-01-01
          • 1970-01-01
          • 2023-04-01
          • 1970-01-01
          • 2018-11-08
          • 1970-01-01
          • 2021-06-24
          相关资源
          最近更新 更多