7
votes

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 28th         | March 29th    |
|         4 | April 28th         | April 29th    |
+-----------+--------------------+---------------+
5
dateTime.plusMonths(1).with(lastDayOfMonth()) probably does what you want.assylias
@assylias: That covers 31st being last day of month. What if selected day of month is between 29th and 30th? Example sequence for selected day of month being 30th: January 30th, February 29th, March 31st where it should be March 30th.CAPS LOCK
This Question is quite confused. Perhaps adding a list of example data and desired results world help. Focus on what you want, not the obstacles.Basil Bourque
@CAPSLOCK I had misunderstood your question.assylias

5 Answers

10
votes

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
0
votes

You could try

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

Adds the number of days of the next month to the current date. This would still cause some weird behavior in the early part of the month though like Jan 1 -> Jan 29 -> Mar 1, but it will still always restore itself when possible.

Adding in an additional check like

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

would fix that and make it work more consistently. Now it will add the number of days in the current month if it's in the first half or the number of days in the next month if it's in the second half which should work I think.

-1
votes

One solution could be to take the last day and add a day:

firstDayOfNextMonth = lastDayOfThisMonth.plusDays(1);

then add a month:

firstDayOfNextNextMonth = firstDayOfNextMonth.plusMonths(1);

then subtract a day:

lastDayOfNextMonth = firstDayOfNextNextMonth.minusDays(1);

So, you get:

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

You could roll this into a method:

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

YearMonth

If you want the end of month as your goal, think in terms of the month rather than the particular date.

The YearMonth class represents any particular month.

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

You can add months to that object.

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

When you need the date of the end of that month, ask for it by calling atEndOfMonth.

LocalDate ld = ym.atEndOfMonth();

By the way, the Month enum has predefined objects to represent each of the twelve months of the year, January–December.

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

If you want to apply a particular day-of-month that might not be valid for some months (days 29–31), try asking for such a date. If exception thrown then fall back to asking for end of month.

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

Of do an if( dayOfMonth > 28 ) rather than catch exception.

-1
votes

I just had to solve this problem myself, though with a slightly different input, and ended up with the solution below. As others have pointed out, YearMonth can be helpful in this case, but I think its true power is its use as a TemporalAdjuster. In my case, I needed to preserve the day of the month from an initial date:

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

This adjusts the date forward while preserving the day of month from the initial date, but will use the last day of the month instead if necessary.

If you only have the day of the month to go by, you can start from any month with 31 days:

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

Or use org.threeten's DayOfMonth:

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