使用 java.time 将月份添加到每月的特定日期

Adding month to specific day of month with java.time

使用 LocalDateTimeLocalDate 将月份添加到指定日期的最干净的解决方案是什么?

很好的答案是 In java.time, how is the result of adding a month calculated? and Find next occurrence of a day-of-week in JSR-310 所以我希望这个问题可以用类似的干净解决方案来解决。

假设用户选择一个月中的某一天(从 1 到 31 中的任意一天)执行某个周期性任务。选择时间戳后生成并每月增加。仅存储当前月份的时间戳(没有上个月的轨迹,时间戳每月更新)和所选日期。

如果选择的日期是 31st 并且我们从 1 月 31 日 开始,使用 dateTime.plusMonths(1) 方法添加一个月得到 2016 年 2 月 29 日。如果再次使用 dateTime.plusMonths(1) 方法,结果是 3 月 29 日,而预期是 3月31日。但是,它适用于从 1 号到 28 号的日子。

一个月第 31 天的解决方法可以使用 dateTime.with(TemporalAdjusters.lastDayOfMonth()),但这不包括从 28 日到 30 日的日子,这也可能是一个月的最后几天。

lastDayOfMonth() 的示例 30 日1 月 30 日2 月 29 日(plusMonth 方法行为选择最后一天,如果前一天比上个月高),3 月 31 日(预期是 30 日 这个被选中月中的某天)。此方法不考虑一个月中选定的几天。


当然这个问题有很多潜在的解决方案,包括一些检查和计算月和日之间的差异,但我正在寻找一个解决方案,只有在可能的情况下才使用 java.time 功能。

澄清

我正在寻找一种方法来将日期移到下个月的选定日期。喜欢dateTime.plusMonths(1).withDayOfMonth(selectedDayOfMonth)。这将引发像 2 月 31 日这样的日期的异常,因为 withDayOfMonth 覆盖 plusMonths 对最后有效日期的调整。

使用plusMonth方法的迭代示例。在迭代中取前一个值 - 想象一下递归。

+-----------+------------------+---------------+
| iteration | Day-of-month: 31 |   Expected    |
+-----------+------------------+---------------+
|         1 | January 31st     | January 31st  |
|         2 | February 29th    | February 29th |
|         3 | March 29th       | March 31st    |
|         4 | April 29th       | April 30th    |
+-----------+------------------+---------------+

以及从 1 号到 28 号的另一个工作示例。

+-----------+-------------------+--------------+
| iteration | Day-of-month: 4th |   Expected   |
+-----------+-------------------+--------------+
|         1 | January 4th       | January 4th  |
|         2 | February 4th      | February 4th |
|         3 | March 4th         | March 4th    |
|         4 | April 4th         | April 4th    |
+-----------+-------------------+--------------+

闰年可以是月 29 日,平年则不行。

+-----------+--------------------+---------------+
| iteration | Day-of-month: 29th |   Expected    |
+-----------+--------------------+---------------+
|         1 | January 29th       | January 29th  |
|         2 | February 28th      | February 28th |
|         3 | March 28th         | March 29th    |
|         4 | April 28th         | April 29th    |
+-----------+--------------------+---------------+

你可以试试

myDate.plusDays(YearMonth.of(myDate.getYear, myDate.getMonth + 1).lengthOfMonth())

将下个月的天数添加到当前日期。这仍然会在月初导致一些奇怪的行为,比如 1 月 1 日 -> 1 月 29 日 -> 3 月 1 日,但它仍然会在可能的情况下始终自行恢复。

添加额外的检查,例如

myDate.plusDays(YearMonth.of(myDate.getYear, myDate.getMonth + (myDate.getDayOfMonth() > 15 ? 1 : 0)).lengthOfMonth())

会解决这个问题并使其更一致地工作。现在它将添加当月的天数,如果它在上半年,或者如果它在下半年,我认为应该工作的下个月的天数。

一个解决方案可能是取最后一天并添加一天:

firstDayOfNextMonth = lastDayOfThisMonth.plusDays(1);

然后加一个月:

firstDayOfNextNextMonth = firstDayOfNextMonth.plusMonths(1);

然后减去一天:

lastDayOfNextMonth = firstDayOfNextNextMonth.minusDays(1);

所以,你得到:

lastDayOfNextMonth = lastDayOfThisMonth.plusDays(1).plusMonths(1).minusDays(1);

您可以将其整合到一个方法中:

LocalDate addMonthToDate(LocalDate date) {
    return date.plusDays(1).plusMonths(1).minusDays(1);
}

YearMonth

如果您希望以月末为目标,请考虑月份而不是特定日期。

YearMonth class 代表任何特定的月份。

YearMonth ym = YearMonth.of( 2016 , 1 );  // January

您可以向该对象添加月份。

YearMonth nextMonth = ym.plusMonths( 1 );  // February

当您需要该月的月底日期时,请致电atEndOfMonth

LocalDate ld = ym.atEndOfMonth();

顺便说一下,Month 枚举有预定义的对象来表示一年中的十二个月,即一月到十二月。

YearMonth ym = YearMonth.of( 2016 , Month.JANUARY );

如果您想应用可能在某些月份(第 29-31 天)无效的特定日期,请尝试询问这样的日期。如果抛出异常,则回退到要求月底。

int dayOfMonth = 30;
LocalDate ld = null;
try {
    ld = ym.atDay( dayOfMonth );
} catch ( DateTimeException e ) {
    ld = ym.atEndOfMonth();
}

Of 执行 if( dayOfMonth > 28 ) 而不是捕获异常。

将月中的第几天设置为 min(selectedDayOfMonth, lastDayOfNextMonth)

public static LocalDate next(LocalDate current, int selectedDayOfMonth) {
    LocalDate next = current.plusMonths(1);
    return next.withDayOfMonth(Math.min(selectedDayOfMonth, next.lengthOfMonth()));
}

用法:

public static void test(int selectedDayOfMonth) {
    LocalDate date = LocalDate.of(2001, Month.JANUARY, selectedDayOfMonth);
    System.out.println(date);
    for (int i = 0; i < 5; i++) {
        date = next(date, selectedDayOfMonth);
        System.out.println(date);
    }
    System.out.println();
}

test(4) 的输出:

2001-01-04
2001-02-04
2001-03-04
2001-04-04
2001-05-04
2001-06-04

test(29) 的输出:

2001-01-29
2001-02-28
2001-03-29
2001-04-29
2001-05-29
2001-06-29

test(31) 的输出:

2001-01-31
2001-02-28
2001-03-31
2001-04-30
2001-05-31
2001-06-30

我不得不自己解决这个问题,虽然输入略有不同,但最终得到了以下解决方案。正如其他人指出的那样,YearMonth can be helpful in this case, but I think its true power is its use as a TemporalAdjuster。就我而言,我需要从初始日期开始保留每月的第几天:

public static LocalDate next(LocalDate initial, LocalDate current) {
    return initial.with(YearMonth.from(current.plusMonths(1)));
}

这会向前调整日期,同时保留初始日期中的日期,但如有必要,将使用该月的最后一天。

如果您只有一个月中的第几天,您可以从任何有 31 天的月份开始:

public static LocalDate next(LocalDate current, int selectedDayOfMonth) {
    return LocalDate.of(2020, 1, selectedDayOfMonth).with(YearMonth.from(current.plusMonths(1)));
}

或使用org.threeten的DayOfMonth:

public static LocalDate next(LocalDate current, int selectedDayOfMonth) {
    return DayOfMonth.of(selectedDayOfMonth).atYearMonth(YearMonth.from(current.plusMonths(1)));
}