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")));
}
相关推荐
吴声子夜歌4 小时前
Java面试——算法
java·算法·面试
evans在进步4 小时前
LeetCode 64:最小路径和——Java 原地动态规划详解
java·leetcode·动态规划
凤山老林5 小时前
Spring Boot 3.x AOT 编译实战:从原理、踩坑到生产落地
java·spring boot·后端
博傅5 小时前
Spring 核心原理
java·后端·spring
yurenpai(27届找实习中)6 小时前
从零读懂 AI 智能客服(一):模块职责与 SSE 聊天链路(后端架构)
java·人工智能·架构·langchain4j
陈皮波比茶6 小时前
Swagger
java
小闫BI设源码6 小时前
Elasticsearch面试必看:如何让客户端精准选择节点高效执行请求?
java·elasticsearch·面试宝典·深入解析
摇滚侠7 小时前
《SpringBoot 3:入门与应用实战》第 5 章 使用 Spring Boot 阅读笔记 9
java·spring boot·笔记
进阶的小名7 小时前
Spring AI 2.0 探索:多 OpenAI-Compatible 模型接入,以及下一代 Session 记忆管理
java·人工智能·后端·gpt·spring·ai·chatgpt
Lucifer三思而后行7 小时前
Oracle 23ai/26ai 新功能:一条命令把 PDB 用户设为只读
oracle·pdb