Java模拟Oracle函数MONTHS_BETWEEN注意事项

MONTHS_BETWEEN(DATE1, DATE2) 用来计算两个日期的月份差。

最近接到一个迁移需求,把Oracle SQL接口迁移到新平台上,但新平台是采用Java计算的方式,所以我需求把SQL逻辑转成Java语言。

在遇到MONTHS_BETWEEN时,遇到一些奇怪的问题,在此记录一下。

情景在现

一开始,我的大致思路:先计算出两个日期的月份差,再拿开始日期加上月份差再与结束日期计算出日差,如果日差大于0,月份差+1;日差小于0,则月份差-1。

为什么不保留小数?

因为在SQL逻辑中使用到MONTHS_BETWEEN都是用来计算近x个月、未来x个月这类数据,只需要判断是否大于或小于某个整数,所有这里取整是没有问题的(当时是这样想的)。

package com.chen.util;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.util.Calendar;
import java.util.Date;
import java.util.Objects; @Slf4j
public class DateUtil { public static final SimpleDateFormat yyyyMMddDateFormat = new SimpleDateFormat("yyyyMMdd"); public static Date strToDate(String str) {
if (StringUtils.isBlank(str)) {
return null;
}
return yyyyMMddDateFormat.parse(str, new ParsePosition(0));
} /**
* 计算两个日期差月份差
*
* @param begDate 开始日期
* @param endDate 结束日期
* @return 月份差
*/
public static Integer monthsBetween(Date begDate, Date endDate) {
try {
if (Objects.isNull(begDate) || Objects.isNull(endDate)) {
return null;
}
Temporal beg = begDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
Temporal end = endDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
int between = (int) ChronoUnit.MONTHS.between(beg, end);
Calendar calendar = Calendar.getInstance();
calendar.setTime(begDate);
calendar.add(Calendar.MONTH, between);
Date begDateNew = calendar.getTime();
Temporal begNew = begDateNew.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
long dayDiff = ChronoUnit.DAYS.between(begNew, end);
if (dayDiff > 0) {
between += 1;
} else if (dayDiff < 0) {
between -= 1;
}
return between;
} catch (Exception e) {
log.warn("DateUtil monthsBetweenWithMon() Occurred Exception.", e);
return null;
}
} public static void main(String[] args) {
System.out.printf("%-9s %-9s %-3s\n", "日期1", "日期2", "月份差");
String date1 = "20240405", date2 = "20240807";
Integer between = monthsBetween(strToDate(date1), strToDate(date2));
System.out.printf("%-10s %-10s %-3s\n", date1, date2, between);
}
}

结果与Oracle比对

开始日期 结束日期 JAVA ORACLE
20240405 20240807 5 4.06451612903226
20240715 20240102 -7 -6.41935483870968
20231130 20240131 3 2
20240117 20231224 -1 -0.774193548387097
20240229 20240529 -3 -3
20240229 20240530 -4 -3.03225806451613
20240229 20240531 -4 -3
20240731 20240430 -3 3

结果分析

自测与冒烟测试都没发现问题,正式测试时,发现当两个日期均是月末时,就会导致结果不正确(结果中的20231130与20240131)。

并且还发现Orcale的MONTHS_BETWEEN在处理月末时更是打破常规思维!比如20240731的近3个月应该是从20240501开始计算的;还有一种情况是当两个日期中有一个日期是2月末时,与大月比较29号、30号、31号时,29号与31号的月份差居然是相同的。

查了很多资料最后在ORACLE 日期函数 MONTHS_BETWEEN文章中找到原因。

MONTHS_BETWEEN函数返回两个日期之间的月份数。如果两个日期月份内天数相同,或者都是某个月的最后一天,返回一个整数,否则,返回数值带小数,以每天1/31月来计算月中剩余天数。如果日期1比日期2小 ,返回值为负数。

问题解决

思路:

日差 = 如果两个日期都是月末,日差为0,否则 (开始日期日 - 结束日期日)

月差 = (开始日期年份 * 12 + 开始日期月份) - (结束日期年份 * 12 + 结束日期月份) + (日差 / 31)

package com.chen.util;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.util.Calendar;
import java.util.Date;
import java.util.Objects; @Slf4j
public class DateUtil { public static final SimpleDateFormat yyyyMMddDateFormat = new SimpleDateFormat("yyyyMMdd"); public static Date strToDate(String str) {
if (StringUtils.isBlank(str)) {
return null;
}
return yyyyMMddDateFormat.parse(str, new ParsePosition(0));
} /**
* 判断日期是否是月末
* @param date 日期
* @return 是否月末
*/
public static Boolean isEndOfMonth(Calendar date) {
if (Objects.isNull(date)) {
return false;
}
return date.get(Calendar.DAY_OF_MONTH) == date.getActualMaximum(Calendar.DAY_OF_MONTH);
} /**
* 适配ORACLE数据库MONTHS_BETWEEN()计算结果
* MONTHS_BETWEEN(startDate, endDate)
*
* @param startDate 开始时间
* @param endDate 结果时间
* @return 月份差
*/
public static BigDecimal oracleMonthsBetween(Date startDate, Date endDate) {
Calendar startCalendar = Calendar.getInstance();
startCalendar.setTime(startDate);
Calendar endCalendar = Calendar.getInstance();
endCalendar.setTime(endDate); int startYear = startCalendar.get(Calendar.YEAR);
int endYear = endCalendar.get(Calendar.YEAR);
int startMonth = startCalendar.get(Calendar.MONTH);
int endMonth = endCalendar.get(Calendar.MONTH);
int startDay = startCalendar.get(Calendar.DATE);
int endDay = endCalendar.get(Calendar.DATE);
// 月份差
double result = (startYear * 12 + startMonth) - (endYear * 12 + endMonth);
// 小数月份
double countDay;
// 如果是两个日期都是月末,就只处理月份;否则使用日差 / 31 算出小数月份
if (isEndOfMonth(startCalendar) && isEndOfMonth(endCalendar)) {
countDay = 0;
} else {
countDay = (startDay - endDay) / 31d;
}
result += countDay;
// 返回并保留14位小数位
return BigDecimal.valueOf(result)
.setScale(14, RoundingMode.HALF_UP)
.stripTrailingZeros();
} public static void main(String[] args) {
System.out.printf("%-9s %-9s %-3s\n", "日期1", "日期2", "月份差");
String date1 = "20240405", date2 = "20240807";
BigDecimal between = oracleMonthsBetween(strToDate(date1), strToDate(date2));
System.out.printf("%-10s %-10s %-3s\n", date1, date2, between.toPlainString());
}
}

结果与Oracle比对

开始日期 结束日期 JAVA ORACLE
20240405 20240807 -4.06451612903226 -4.06451612903226
20240423 20240614 -1.70967741935484 -1.70967741935484
20240229 20240529 -3 -3
20240229 20240530 -3.03225806451613 -3.03225806451613
20240229 20240531 -3 -3
20230228 20230528 -3 -3
20231130 20240131 -2 -2
20231130 20240201 -2.06451612903226 -2.06451612903226
20240731 20240430 3 3
20240731 20240429 3.06451612903226 3.06451612903226
20240430 20240731 -3 -3
20240114 20231010 3.12903225806452 3.12903225806452

Java模拟Oracle函数MONTHS_BETWEEN注意事项的更多相关文章

  1. Java字符串split函数的注意事项

    Java字符串的split方法可以分割字符串,但和其他语言不太一样,split方法的参数不是单个字符,而是正则表达式,如果输入了竖线(|)这样的字符作为分割字符串,会出现意想不到的结果, 如, Str ...

  2. java调用oracle函数

    /** * 调用函数取得数据表的ID值 * @param tableName 表名 * @return * @throws SQLException */ public String callFun( ...

  3. oracle函数 months_between(d1,d2)

    [功能]:返回日期d1到日期d2之间的月数. [参数]:d1,d2 日期型 [返回]:数字 如果d1>d2,则返回正数 如果d1<d2,则返回负数 [示例] select sysdate, ...

  4. EscapeAndUnescapeUtil【java模拟js的escape和unescape函数】

    版权声明:本文为HaiyuKing原创文章,转载请注明出处! 前言 在这里做一个记录,基本代码同参考资料<java模拟js的escape和unescape函数>一样. 效果图     代码 ...

  5. Oracle的学习三:java连接Oracle、事务、内置函数、日期函数、转换函数、系统函数

    1.java程序操作Oracle java连接Oracle JDBC_ODBC桥连接 1.加载驱动: Class.forName("sun.jdbc.odbc.JdbcodbcDriver& ...

  6. 【函数】Oracle函数系列(2)--数学函数及日期函数

    [函数]Oracle函数系列(2)--数学函数及日期函数 1  BLOG文档结构图 2  前言部分 2.1  导读和注意事项 各位技术爱好者,看完本文后,你可以掌握如下的技能,也可以学到一些其它你所不 ...

  7. oracle函数,游标,视图使用总结0.000000000000000000001

    oracle函数或者叫存储过程,在实际的开发过程中对于复杂的业务需求是非常有用的,非常有效率的也是非常好玩儿的一个技术点. 平常在开发过程中对于CRUD功能较多.一般SQL即可应付,大不了就是长一点而 ...

  8. java向oracle数据库中插入当前时间

    public class Test{public static void main (String args []){ java.util.Date a = new java.util.Date(); ...

  9. oracle函数listagg的使用说明(分组后连接字段)

    关于oracle函数listagg的使用说明 工作中经常遇到客户提出这样的需求,希望在汇总合并中,能够把日期逐个枚举出来. 如图,原始数据是这样的: 客户希望能够实现这样的汇总合并: 那么通常我会使用 ...

  10. Android使用JNI(从java调用本地函数)

    当编写一个混合有本地C代码和Java的应用程序时,需要使用Java本地接口(JNI)作为连接桥梁.JNI作为一个软件层和API,允许使用本地代码调用Java对象的方法,同时也允许在Java方法中调用本 ...

随机推荐

  1. About CSP

    好了,猜猜今年第一题会考什么 linux 终端指令 这样吧 CTH 装了 ubuntu 系统的电脑被人施加了 rm -rf /home/Desktop/ 指令,导致他打不开桌面了,以下哪一个是 CTH ...

  2. [OI] 整体二分

    整体二分可以理解成普通二分改版,其实并没有改多少,并且一般对 check() 函数的复杂度要求更宽松 先来看一道经典题目:求区间排名 给一个数列,若干组询问 \((l,r,k)\),求 \([l,r] ...

  3. namespace hdk

    没有高精类,因为这玩意太占内存了,正在优化 demap Rander StringAddition_InFix string ordered_vector #include<bits/stdc+ ...

  4. Codeforces[CF1036B]Diagonal Walking v.2题解

    题目大意 很明显,这道题就是求 k 步之内到达点 \((a,b)\) ,然后尽量走对角线,求能走对角线的最大值. 做题思路 首先明白一个事实,即一个对角线可以通过增加一步而抵达点不变,如图: 我们可以 ...

  5. Android性能优化:getResources()与Binder交火导致的界面卡顿优化

    背景 某轮测试发现,我们的设备运行一个第三方的App时,卡顿感非常明显: 界面加载很慢,菊花转半天 滑屏极度不跟手,目测观感帧率低于15 对比机(竞品)也会稍微一点卡,但是好很多,基本不会有很大感觉的 ...

  6. vue前端开发仿钉图系列(1)高德地图的使用详解

    最近公司让参考钉图做图层模块相关的功能,很庆幸有机会细细研究地图相关的东西.因为手机端用的是高德地图,web端也使用高德地图.还是和往常一样,先贴上效果图. 步骤1.在高德开放平台注册信息,创建自己的 ...

  7. threejs 几何体的本质 顶点

    几何体的线框模式, 一个正方平面最少可以由4个顶点组成,两个三角形组成(公用了 2个顶点,使用了索引创建顶点属性) . // 导入 threejs import * as THREE from &qu ...

  8. 什么是Streamlit

    最近,我在数据分析的一些任务中尝试了闻名已久的Streamlit,再一次感受到Python的强大之处. 于是,准备根据自己的掌握情况,写一个介绍Streamlit的系列. 本文作为第一篇, 先介绍介绍 ...

  9. 大数据技术之Shell

    1. shell概述 示意图: Shell是一个命令行解释器,它为用户提供了一个向Linux内核发送请求以便运行程序的界面系统级程序,用户可以用Shell来启动.挂起.停止甚至是编写一些程序. ● L ...

  10. KubeSphere 社区双周报 | 本周六上海站 Meetup 准时开启 | 2023.7.21-08.03

    KubeSphere 社区双周报主要整理展示新增的贡献者名单和证书.新增的讲师证书以及两周内提交过 commit 的贡献者,并对近期重要的 PR 进行解析,同时还包含了线上/线下活动和布道推广等一系列 ...