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

时间:2012-02-01 17:40:44

标签: java oracle function date

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

8 个答案:

答案 0 :(得分:2)

你可以这样做:

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

编辑:

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

答案 1 :(得分:2)

我遇到了同样的需求并从@ 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;
} 

使用此功能,日期为17/02/2013和2016年3月11日的通话结果为:36.806451612903224

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

答案 2 :(得分:2)

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

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

答案 3 :(得分:0)

Joda Time中,org.joda.time.Months类中有一个月份。

答案 4 :(得分:0)

我遇到同样的问题并且遵循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;
}

答案 5 :(得分:0)

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

class Point {
  constructor(stattX, startY){
    this.x = startX;
    this.y = startY;
  }
  // class methods here...
}

答案 6 :(得分:0)

java.time

其他Answers使用现在遗留下来的麻烦的旧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_EVEN,默认由IEEE 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.DateCalendar和& SimpleDateFormat

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

要了解详情,请参阅Oracle Tutorial。并搜索Stack Overflow以获取许多示例和解释。规范是JSR 310

从哪里获取java.time类?

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

答案 7 :(得分:-1)

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