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")));
}
相关推荐
想不到ID了9 分钟前
第八篇: 登录注册功能实现
java·javascript
码语智行25 分钟前
shp文件生成
java
plainGeekDev33 分钟前
AlertDialog → DialogFragment
android·java·kotlin
薛定谔的悦40 分钟前
光伏-储能-负荷联合预测:给 EMS 装上“预知能力“
java·数据库·人工智能·python·储能
大菜菜小个子40 分钟前
template<typename T>使用
java·开发语言·算法
Refrain_zc41 分钟前
Android开发: 拒绝 Activity 重建!onConfigurationChanged 实现平板横竖屏无缝切换
java
方也_arkling1 小时前
【Java-Day15】API篇-ArrayList集合
java·开发语言
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第89题】【Mysql篇】第19题:Hash 索引和 B+ 树索引的区别?它们在使用方面的区别?
java·数据库·mysql·面试·哈希算法
我是一颗柠檬1 小时前
【Java后端技术亮点】动态路由权限(按钮级权限),细粒度控制到按钮级别
java·开发语言·后端·状态模式
Fanfanaas1 小时前
C++ 继承
java·开发语言·jvm·c++·学习·算法