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")));
}
相关推荐
2301_7965125212 分钟前
Rust编程学习 - 为什么说Cow 代表的是Copy-On-Write, 即“写时复制技术”,它是一种高效的 资源管理手段
java·学习·rust
编啊编程啊程15 分钟前
【029】智能停车计费系统
java·数据库·spring boot·spring·spring cloud·kafka
hashiqimiya18 分钟前
springboot后端的接口headers
java·spring boot·后端
懒羊羊不懒@22 分钟前
JavaSe—集合框架、Collection集合
java·开发语言
霸道流氓气质27 分钟前
Java中Stream使用示例-对实体List分组且保留原数据顺序并对分组后的每组内的数据进行部分业务逻辑修改操作
java·list
Leon-Ning Liu1 小时前
Oracle数据库常用视图:dba_datapump_jobs
数据库·oracle·dba
java1234_小锋1 小时前
Spring事件监听的核心机制是什么?
java·spring·面试
数据库生产实战1 小时前
Oracle 19C RAC下TRUNCATE TABLE的REUSE STORAGE选项作用和风险浅析!
数据库·oracle
星释2 小时前
Rust 练习册 16:Trait 作为返回类型
java·网络·rust