CAPS LOCK CAPS LOCK - 3 months ago 12
Java Question

Adding month to specific day of month with java.time

What is the most clean solution for adding a month to specified day using

LocalDateTime
or
LocalDate
?

Nice answers are In java.time, how is the result of adding a month calculated? and Find next occurrence of a day-of-week in JSR-310 so I hope that this problem can be solved with similar clean solution.

Lets say that user selects a day of month (any from 1 to 31) for some periodic task. After selection timestamp is generated and is increased each month. Only current month's timestamp (there is no track of previous month, timestamp is updated each month) and selected day of month are stored.

If selected day is 31st and we start with January 31st adding a month with
dateTime.plusMonths(1)
method gives February 29th in 2016. If
dateTime.plusMonths(1)
method is used again the result is March 29th while expected is March 31st. However, it works for days from 1st to 28th.

A workaround for 31st day of month could be using
dateTime.with(TemporalAdjusters.lastDayOfMonth())
but this does not cover days from 28th to 30th which can also be last days of month.

Example for
lastDayOfMonth()
for selected day of month 30th: January 30th, February 29th (plusMonth method behaviour selects last day if previous day is higher in previous month), March 31st (expected is 30th this is selected day of month). This method does not take into account selected days of month.




Sure this problem has many potential solutions that include some checks and calculating differences between months and days but I am looking for a solution using java.time capabilities only if this is possible.

Clarification



I am looking for a way to move a date to the next month with selected day of month. Like
dateTime.plusMonths(1).withDayOfMonth(selectedDayOfMonth)
. This will throw an exception for dates like February 31st as
withDayOfMonth
overrides
plusMonths
's adjustment to the last valid date.

Example of iteration of using
plusMonth
method. In iteration value from previous one is taken - imagine recursion.

+-----------+------------------+---------------+
| 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 |
+-----------+------------------+---------------+


And another working example for day of month from 1st to 28th.

+-----------+-------------------+--------------+
| 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 |
+-----------+-------------------+--------------+


29th day of month is ok for leap years but not for common years.

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

Answer

Set the day of month to min(selectedDayOfMonth, lastDayOfNextMonth)

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

Usage:

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();
}

Output for test(4):

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

Output for test(29):

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

Output for test(31):

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