首页 > 数据库 >Java模拟Oracle函数MONTHS_BETWEEN注意事项

Java模拟Oracle函数MONTHS_BETWEEN注意事项

时间:2024-12-13 16:55:03浏览次数:6  
标签:%- java MONTHS return Java 日期 BETWEEN import Calendar

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,MONTHS,return,Java,日期,BETWEEN,import,Calendar
From: https://www.cnblogs.com/likeyou99315/p/18605300

相关文章

  • 【代码设计】Java 用注解简洁明了的标注数据筛选特性
    基础设计默认值:仅包含正常状态数据,例如账户:不包含离职账户的正常账户即为deletedData=false,wantsEnabled=true,wantsDisabled=false场景举例:场景一:正常的只展示正常的账户,则为默认的情况场景二:只搜索不正常的已经删除的账户,则与上一条完全相反,则为wantsEnabled=......
  • Java实习常见面试题(一)
    1.==与equals的区别==在比较基本数据类型时比较的是值,在比较引用类型时比较的是内存地址equals在重写之后比较的是值,在不重写时比较的是地址equals不能比较基本数据类型2.StringStringbufferStringBuilder区别String是final修饰的常量对象内容不可变StringBufffer对方......
  • 基于java ssm鲜活农产品销售商城系统(源码+文档+运行视频+讲解视频)
     文章目录系列文章目录目的前言一、详细视频演示二、项目部分实现截图三、技术栈后端框架SSM前端框架vueSSM框架详细介绍系统测试四、代码参考源码获取目的摘要: 基于JavaSSM的鲜活农产品销售商城系统连接了农民和消费者,促进了鲜活农产品的销售。SSM框架实现了高......
  • 基于java ssm网上书店销售管理系统二手书籍回收出售商城(源码+文档+运行视频+讲解视频)
     文章目录系列文章目录目的前言一、详细视频演示二、项目部分实现截图三、技术栈后端框架SSM前端框架vueSSM框架详细介绍系统测试四、代码参考源码获取目的摘要: 基于JavaSSM的网上书店销售管理系统二手书籍回收出售商城促进了二手书籍的循环利用。SSM框架实现了......
  • 2025年Java面经(附答案)
    一、Java基础部分面试题1.Java面向对象的三个特征封装:对象只需要选择性的对外公开一些属性和行为。继承:子对象可以继承父对象的属性和行为,并且可以在其之上进行修改以适合更特殊的场景需求。多态:允许不同类的对象对同一消息做出响应。2.Java中基本的数据类型有哪些以......
  • node.js毕设基于Java的航班订票管理系统 论文+程序
    本系统(程序+源码+数据库+调试部署+开发环境)带文档lw万字以上,文末可获取源码系统程序文件列表开题报告内容一、选题背景关于航班订票管理系统的研究,现有研究主要以大型综合票务系统或特定功能模块的优化为主,专门针对基于Java构建具有多种功能(如用户、机票信息、订单信息、......
  • 全网最强Java面试题(全网最全、最细、附答案)
    1、悲观锁、乐观锁和分布式锁的实现和细节悲观锁:认为线程安全问题一定会发生,所以在操作数据之前先获取锁,保证线程串行执行,例如synchronized,lock细节:悲观锁适合插入数据锁的粒度要尽量小,只锁住需要串行执行的代码配合事务使用时,要先提交事务再释放锁乐观锁:认为线程安......
  • 火爆Github的1000道Java面试题
    开篇小叙现在Java面试可以说是老生常谈的一个问题了,确实也是这么回事。面试题、面试宝典、面试手册......各种Java面试题一搜一大把,根本看不完,也看不过来,而且每份面试资料也都觉得Nice,然后就开启了收藏之路。Java开发者应该是不会很容易满足的,现在拿着20K的工作,下一步就想着......
  • 2024最新最全面Java复习路线(含P5-P8),已收录 GitHub
    小编整理出一篇Java进阶架构师之路的核心知识,同时也是面试时面试官必问的知识点,篇章也是包括了很多知识点,其中包括了有基础知识、Java集合、JVM、多线程并发、spring原理、微服务、Netty与RPC、Kafka、日记、设计模式、Java算法、数据库、Zookeeper、分布式缓存、数据......
  • jmeter压测报Java reset的解决办法
    解决办法:1、在注册表中按照下面的数据项去设置。win+r打开dos窗口,输入regedit,可打开注册表。一般不建议新手直接去修改注册表。2、新建txt,保存以下脚本修改后缀为reg文件,编辑值如下,保存后双击执行;重启电脑,再次压测即不会出现报错。解析中值为10进制,下方脚本已全转换为16进制。W......