1. 概述

计算两个日期之间的月份间隔是常见的编程任务。Java标准库和第三方库都提供了相应的类和方法来实现这个功能。

本教程将深入探讨如何使用传统Date API、Date Time APIJoda-Time库来计算Java中两个日期之间的月份间隔。

2. 日期值的影响

计算月份间隔时,日期中的"日"值会影响最终结果。默认情况下,Java标准库和Joda-Time库都会考虑日值:

如果结束日期的日值小于开始日期的日值,最后一个月不会被计入完整月份

LocalDate startDate = LocalDate.parse("2023-05-31");
LocalDate endDate = LocalDate.parse("2023-11-28");

上述代码中,结束日期的日值(28)小于开始日期的日值(31),因此最后一个月不会被计入。

如果结束日期的日值大于或等于开始日期的日值,最后一个月会被计入完整月份

LocalDate startDate = LocalDate.parse("2023-05-15");
LocalDate endDate = LocalDate.parse("2023-11-20");

在某些场景下,我们可能需要完全忽略日值,只计算月份差值。后续章节将展示如何实现这两种计算方式。

3. 使用传统Date API

使用传统Date API时,需要自定义方法计算月份间隔。我们可以通过Calendar类结合Date类来实现。

3.1. 忽略日值

下面是一个忽略日值的计算方法:

int monthsBetween(Date startDate, Date endDate) {
    if (startDate == null || endDate == null) {
        throw new IllegalArgumentException("Both startDate and endDate must be provided");
    }

    Calendar startCalendar = Calendar.getInstance();
    startCalendar.setTime(startDate);
    int startDateTotalMonths = 12 * startCalendar.get(Calendar.YEAR) 
      + startCalendar.get(Calendar.MONTH);

    Calendar endCalendar = Calendar.getInstance();
    endCalendar.setTime(endDate);
    int endDateTotalMonths = 12 * endCalendar.get(Calendar.YEAR) 
      + endCalendar.get(Calendar.MONTH);

    return endDateTotalMonths - startDateTotalMonths;
}

核心逻辑:

  1. 将年份转换为月份数并加上当前月份值
  2. 计算两个日期的总月份差值

单元测试示例:

@Test
void whenCalculatingMonthsBetweenUsingLegacyDateApi_thenReturnMonthsDifference() throws ParseException {
    MonthInterval monthDifference = new MonthInterval();
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    
    Date startDate = sdf.parse("2016-05-31");
    Date endDate = sdf.parse("2016-11-30");
    int monthsBetween = monthDifference.monthsBetween(startDate, endDate);
    
    assertEquals(6, monthsBetween);
}

3.2. 考虑日值

考虑日值的计算方法:

int monthsBetweenWithDayValue(Date startDate, Date endDate) {
    if (startDate == null || endDate == null) {
        throw new IllegalArgumentException("Both startDate and endDate must be provided");
    }
    
    Calendar startCalendar = Calendar.getInstance();
    startCalendar.setTime(startDate);
    
    int startDateDayOfMonth = startCalendar.get(Calendar.DAY_OF_MONTH);
    int startDateTotalMonths = 12 * startCalendar.get(Calendar.YEAR) 
      + startCalendar.get(Calendar.MONTH);
        
    Calendar endCalendar = Calendar.getInstance();
    endCalendar.setTime(endDate);
    
    int endDateDayOfMonth = endCalendar.get(Calendar.DAY_OF_MONTH);
    int endDateTotalMonths = 12 * endCalendar.get(Calendar.YEAR) 
      + endCalendar.get(Calendar.MONTH);

    return (startDateDayOfMonth > endDateDayOfMonth) 
      ? (endDateTotalMonths - startDateTotalMonths) - 1 
      : (endDateTotalMonths - startDateTotalMonths);
}

关键点:

  • 比较开始日期和结束日期的日值
  • 如果开始日值大于结束日值,结果月份需要减1

单元测试示例:

@Test
void whenCalculatingMonthsBetweenUsingLegacyDateApiDayValueConsidered_thenReturnMonthsDifference() throws ParseException {
    MonthInterval monthDifference = new MonthInterval();
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    
    Date startDate = sdf.parse("2016-05-31");
    Date endDate = sdf.parse("2016-11-28");
    int monthsBetween = monthDifference.monthsBetweenWithDayValue(startDate, endDate);
    
    assertEquals(5, monthsBetween);
}

由于结束日值(28)小于开始日值(31),最后一个月未被计入。

4. 使用Date Time API

Java 8引入的Date Time API提供了更优雅的解决方案,主要通过Period类和ChronoUnit枚举实现。

4.1. Period类

Period.between()方法默认考虑日值:

@Test
void whenCalculatingMonthsBetweenUsingPeriodClass_thenReturnMonthsDifference() {
    Period diff = Period.between(LocalDate.parse("2023-05-25"), LocalDate.parse("2023-11-23"));
    assertEquals(5, diff.getMonths());
}

⚠️ 由于结束日值(23)小于开始日值(25),结果为5个月而非6个月。

要忽略日值,可将日期调整为每月第一天:

@Test
void whenCalculatingMonthsBetweenUsingPeriodClassAndAdjsutingDatesToFirstDayOfTheMonth_thenReturnMonthsDifference() {
    Period diff = Period.between(LocalDate.parse("2023-05-25")
      .withDayOfMonth(1), LocalDate.parse("2023-11-23")
      .withDayOfMonth(1));
    assertEquals(6, diff.getMonths());
}

4.2. ChronoUnit枚举

ChronoUnit.MONTHS.between()同样默认考虑日值:

@Test
void whenCalculatingMonthsBetweenUsingChronoUnitEnum_thenReturnMonthsDifference() {
    long monthsBetween = ChronoUnit.MONTHS.between(
      LocalDate.parse("2023-05-25"), 
      LocalDate.parse("2023-11-23")
    );
    assertEquals(5, monthsBetween);
}

忽略日值的两种方式:

  1. 调整为每月第一天:

    @Test
    void whenCalculatingMonthsBetweenUsingChronoUnitEnumdSetTimeToFirstDayOfMonth_thenReturnMonthsDifference() {
     long monthsBetween = ChronoUnit.MONTHS.between(LocalDate.parse("2023-05-25")
       .withDayOfMonth(1), LocalDate.parse("2023-11-23")
       .withDayOfMonth(1));
     assertEquals(6, monthsBetween);
    }
    
  2. 使用YearMonth.from()

    @Test
    void whenCalculatingMonthsBetweenUsingChronoUnitAndYearMonth_thenReturnMonthsDifference() {
     long diff = ChronoUnit.MONTHS.between(
       YearMonth.from(LocalDate.parse("2023-05-25")), 
       LocalDate.parse("2023-11-23")
     );
     assertEquals(6, diff);
    }
    

5. 使用Joda-Time库

Joda-Time库提供了Months.monthsBetween()方法。首先添加依赖:

<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
    <version>2.12.5</version>
</dependency>

默认考虑日值的计算:

@Test
void whenCalculatingMonthsBetweenUsingJodaTime_thenReturnMonthsDifference() {
    DateTime firstDate = new DateTime(2023, 5, 25, 0, 0);
    DateTime secondDate = new DateTime(2023, 11, 23, 0, 0);
    int monthsBetween = Months.monthsBetween(firstDate, secondDate).getMonths();
    assertEquals(5, monthsBetween);
}

忽略日值的方法:

@Test
void whenCalculatingMonthsBetweenUsingJodaTimeSetTimeToFirstDayOfMonth_thenReturnMonthsDifference() {
    DateTime firstDate = new DateTime(2023, 5, 25, 0, 0).withDayOfMonth(1);
    DateTime secondDate = new DateTime(2023, 11, 23, 0, 0).withDayOfMonth(1);
        
    int monthsBetween = Months.monthsBetween(firstDate, secondDate).getMonths();
    assertEquals(6, monthsBetween);
}

6. 总结

本文介绍了三种计算日期月份差的方法:

方法 优点 缺点
传统Date API 兼容旧系统 代码繁琐,易出错
Date Time API 简洁高效,无依赖 需Java 8+
Joda-Time 功能强大 需额外依赖

推荐使用Date Time API,因为它:

  • 提供了清晰的API设计
  • 无需外部依赖
  • 支持多种计算方式

是否考虑日值取决于具体业务需求:

  • 计算完整月份间隔时考虑日值
  • 计算月份差值时忽略日值

完整示例代码可在GitHub获取。


原始标题:Calculate Months Between Two Dates in Java