【发布时间】:2012-02-24 09:52:57
【问题描述】:
Java 有没有类似 Oracle 的函数MONTHS_BETWEEN?
【问题讨论】:
-
其实和那个问题不一样,主要是months_between返回一个分数,而另一个q要求整数。
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);
}
根据这个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;
}
【讨论】:
在Joda Time 中有一个monthBetween 在org.joda.time.Months 类中。
【讨论】:
之前的答案并不完美,因为它们不处理诸如 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);
};
【讨论】:
我遇到了同样的需求,并从@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 天
【讨论】:
我遇到了同样的问题,在 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;
}
【讨论】:
其实我觉得正确的实现是这个:
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);
}
【讨论】:
其他答案使用麻烦的旧 Calendar 类,现在是遗留的,被 java.time 类取代。
MONTHS_BETWEENMONTHS_BETWEEN 返回日期 date1 和 date2 之间的月数。如果 date1 晚于 date2,则结果为正。如果 date1 早于 date2,则结果是否定的。如果 date1 和 date2 是一个月中的同一天或两个月的最后一天,则结果始终为整数。否则,Oracle 数据库会根据 31 天的月份计算结果的小数部分,并考虑时间组件 date1 和 date2 的差异。
LocalDateLocalDate 类表示没有时间和时区的仅日期值。
使用 JDBC 4.2 及更高版本从数据库中检索 LocalDate。 java.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 );
long months = p.toTotalMonths() ;
提取number of days part,计算月份后剩余的天数。
int days = p.getDays() ;
BigDecimal为了准确,请使用BigDecimal。 double 和 Double 类型使用 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 类 Period 和 Duration。对于文本表示,请使用标准ISO 8601 format:PnYnMnDTnHnMnS。例如,在我们上面的示例中看到的Period:P3M11D 三个月零十一天。
java.time 框架内置于 Java 8 及更高版本中。这些类取代了麻烦的旧 legacy 日期时间类,例如 java.util.Date、Calendar 和 SimpleDateFormat。
Joda-Time 项目现在位于maintenance mode,建议迁移到java.time 类。
要了解更多信息,请参阅Oracle Tutorial。并在 Stack Overflow 上搜索许多示例和解释。规格为JSR 310。
从哪里获得 java.time 类?
ThreeTen-Extra 项目通过附加类扩展了 java.time。该项目是未来可能添加到 java.time 的试验场。您可以在这里找到一些有用的类,例如Interval、YearWeek、YearQuarter 和more。
【讨论】:
我不得不将一些 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)));
}
【讨论】: