Java 实现 Oracle 的 MONTHS_BETWEEN 函数

介绍

因为系统迁移, 有一些函数要转成 Java 版本, Oracle 的

官方介绍 - MONTHS_BETWEEN

MONTHS_BETWEEN returns number of months between dates date1 and date2. The month and the last day of the month are defined by the parameter NLS_CALENDAR. 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.

翻译

MONTHS_BETWEEN返回日期date1和日期date2之间的月数。月份和月份的最后一天由参数NLS_CALENDAR定义。如果date1晚于date2,则结果为正。如果date1早于date2,则结果为负。如果date1和date2是同一个月的同一天,或者都是同一个月的最后一天,那么结果总是一个整数。否则,Oracle数据库根据31天的月份计算结果的小数部分,并考虑时间组件date1和date2的差异。

Java 代码

java 复制代码
    /**
     * 计算两个日期之间的月份差值,精确到小数点.
     * 逻辑参考 Oracle 的 MONTHS_BETWEEN 函数.由于函数的特殊性,两个日期的格式尽可能规整为 YYYY-MM-DD 格式,会忽略时分秒
     *
     * <pre>
     * select MONTHS_BETWEEN('2019-01-31', '2019-02-28') from dual;
     * </pre>
     *
     * @param startDate 开始日期
     * @param endDate   结束日期
     * @return 月份差值
     */
    public static double monthsBetween(Date startDate, Date endDate) {
        // 将旧版Date转换为Instant,然后调整到系统默认时区并转换为LocalDateTime
        LocalDateTime dateTime1 = startDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
        LocalDateTime dateTime2 = endDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();

        // 确保dateTime1 >= dateTime2,若否则交换并取负数
        if (dateTime1.isBefore(dateTime2)) {
            return -monthsBetween(endDate, startDate);
        }

        LocalDate date1 = dateTime1.toLocalDate();
        LocalDate date2 = dateTime2.toLocalDate();

        // 判断是否为月末
        boolean isDate1EndOfMonth = date1.equals(date1.withDayOfMonth(date1.lengthOfMonth()));
        boolean isDate2EndOfMonth = date2.equals(date2.withDayOfMonth(date2.lengthOfMonth()));

        // 计算年月日差值
        int yearDiff = date1.getYear() - date2.getYear();
        int monthDiff = date1.getMonthValue() - date2.getMonthValue();
        int dayDiff = date1.getDayOfMonth() - date2.getDayOfMonth();

        // 核心计算逻辑 - 月份部分
        double totalMonths = yearDiff * 12 + monthDiff;

        // 处理天数部分
        if (isDate1EndOfMonth && isDate2EndOfMonth) {
            // 若均为月末,天数差清零
            dayDiff = 0;
        }
        // 核心计算逻辑 - 天数部分 Oracle按31天计算日占比
        totalMonths += dayDiff / 31.0;

        // 处理时间部分
        Duration duration = Duration.between(dateTime2.toLocalTime(), dateTime1.toLocalTime());
        long hours = duration.toHours();
        long minutes = duration.toMinutes() % 60;
        long seconds = duration.getSeconds() % 60;

        // 核心计算逻辑 - 时分秒部分 将时分钟秒转换为月的比例 Oracle按31天744小时计算
        double timeAsMonth = hours / 744.0 + minutes / (744.0 * 60) + seconds / (744.0 * 60 * 60);

        // 处理位数为小数点后14位. Oracle 默认小数点后14位
        return Math.round((totalMonths + timeAsMonth) * 100000000000000.0) / 100000000000000.0;
    }

单元测试

java 复制代码
@Test
void monthsBetween() {
   /*
      select
           months_between(to_date('2025-01-07 01:12:41', 'yyyy-mm-dd hh24:mi:ss'), to_date('2025-03-09 11:22:48', 'yyyy-mm-dd hh24:mi:ss')) as "-2.07818361708483",
           months_between(to_date('2025-02-27 01:12:41', 'yyyy-mm-dd hh24:mi:ss'), to_date('2025-06-01 21:22:48', 'yyyy-mm-dd hh24:mi:ss')) as "-3.18839867084827",
           months_between(to_date('2025-03-17 01:12:41', 'yyyy-mm-dd hh24:mi:ss'), to_date('2025-05-01 00:00:00', 'yyyy-mm-dd hh24:mi:ss')) as "-1.48224275686977"
      from dual
     */
    log.info("计算月份差: {}", DateUtils.monthsBetween(DateUtils.parse("2025-01-07 01:12:41", "yyyy-MM-dd HH:mm:ss"), DateUtils.parse("2025-03-09 11:22:48", "yyyy-MM-dd HH:mm:ss")));
    log.info("计算月份差: {}", DateUtils.monthsBetween(DateUtils.parse("2025-02-27 01:12:41", "yyyy-MM-dd HH:mm:ss"), DateUtils.parse("2025-06-01 21:22:48", "yyyy-MM-dd HH:mm:ss")));
    log.info("计算月份差: {}", DateUtils.monthsBetween(DateUtils.parse("2025-03-17 01:12:41", "yyyy-MM-dd HH:mm:ss"), DateUtils.parse("2025-05-01 00:00:00", "yyyy-MM-dd HH:mm:ss")));
}
相关推荐
前端兰博31 分钟前
06-并发
java
程序员-珍34 分钟前
Android studio 突然打不开
android·java
重生之小比特1 小时前
【Java SE】字符串 - String类
java·开发语言·intellij-idea
mldong7 小时前
给若依加审批流,不用 Flowable
java·spring boot·架构
Wang's Blog9 小时前
Java框架快速入门: Spring Security+OAuth2之数据库和实体类的RBAC改造
java·数据库·spring
讳疾忌医丶9 小时前
深度拆解 RocksDB 内核:基于 C++17 的 FIFO 调度状态机与温度阶梯自愈设计
java·c++·算法·架构
郑州光合科技余经理9 小时前
国际版外卖系统:税率字段怎么和订单主流程解耦
android·java·开发语言·前端·后端·php·ai编程
毅炼9 小时前
Rover-Suite 开源发布:轻量服务注册中心与网关一体化方案
java·后端·系统架构·gateway
梦想平凡10 小时前
百游棋牌源代码开发搭建教程(十):隔离部署、备份恢复与双端验收
java·前端·javascript·数据库·源代码管理
sunshine22 girl11 小时前
Idea中如何搜索
java·ide·intellij-idea