Добавление месяца к определенному дню месяца с помощью java.time

Какое наиболее чистое решение для добавления месяца к указанному дню с помощью LocalDateTime или LocalDate?

Хорошие ответы: В java.time как рассчитывается результат добавления месяца? и Найти следующее вхождение дня недели в JSR-310, поэтому я надеюсь, что эту проблему можно решить аналогичным чистым решением.

Допустим, пользователь выбирает день месяца (от 1 до 31) для некоторой периодической задачи. После выбора создается временная метка, которая увеличивается каждый месяц. Сохраняются только отметка времени текущего месяца (предыдущий месяц не отслеживается, отметка времени обновляется каждый месяц) и выбранный день месяца.

Если выбран день 31-е, и мы начинаем с 31 января, добавление месяца с dateTime.plusMonths(1) методом дает 29 февраля в 2016 году. Если dateTime.plusMonths(1) метод используется снова результат - 29 марта, а ожидаемый - 31 марта. Однако он работает с 1 по 28 число.

Обходной путь для 31-го числа месяца можно использовать с помощью dateTime.with(TemporalAdjusters.lastDayOfMonth()), но это не распространяется на дни с 28-го по 30-е, которые также могут быть последними днями месяца.

Пример для lastDayOfMonth() для выбранного дня месяца 30-го: 30 января, 29 февраля (поведение метода plusMonth выбирает последний день, если предыдущий день выше предыдущего месяц), 31 марта (ожидается 30-е, выбранный день месяца). Этот метод не учитывает выбранные дни месяца.


Конечно, у этой проблемы есть много потенциальных решений, которые включают некоторые проверки и вычисление разницы между месяцами и днями, но я ищу решение, использующее возможности java.time, только если это возможно.

Разъяснение

Я ищу способ перенести дату на следующий месяц с выбранным днем ​​месяца. Вроде dateTime.plusMonths(1).withDayOfMonth(selectedDayOfMonth). Это вызовет исключение для таких дат, как 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    |
+-----------+--------------------+---------------+

person CAPS LOCK    schedule 19.08.2016    source источник
comment
dateTime.plusMonths(1).with(lastDayOfMonth()) вероятно делает то, что вы хотите.   -  person assylias    schedule 20.08.2016
comment
@assylias: Это 31-е число, последний день месяца. Что, если выбранный день месяца находится между 29 и 30 числами? Пример последовательности для выбранного дня месяца - 30-го: 30 января, 29 февраля, 31 марта, где должно быть 30 марта.   -  person CAPS LOCK    schedule 20.08.2016
comment
Этот вопрос довольно запутанный. Возможно добавление списка примеров данных и желаемых результатов мировой помощи. Сосредоточьтесь на том, что вы хотите, а не на препятствиях.   -  person Basil Bourque    schedule 20.08.2016
comment
@CAPSLOCK Я неправильно понял ваш вопрос.   -  person assylias    schedule 22.08.2016


Ответы (5)


Установите день месяца на 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
person binoternary    schedule 20.08.2016
comment
Это оно. Довольно чисто. - person CAPS LOCK; 20.08.2016

Вы могли бы попробовать

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

Добавляет количество дней следующего месяца к текущей дате. Это все равно вызовет какое-то странное поведение в начале месяца, хотя, например, 1 января -> 29 января -> 1 марта, но оно все равно всегда будет восстанавливаться, когда это возможно.

Добавление дополнительной проверки, например

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

исправит это и заставит работать более стабильно. Теперь он добавит количество дней в текущем месяце, если он находится в первой половине, или количество дней в следующем месяце, если он во второй половине, что, я думаю, должно работать.

person theLambGoat    schedule 19.08.2016

Одно из решений - взять последний день и добавить день:

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);
}
person ryvantage    schedule 20.08.2016
comment
Это касается 31-го числа последнего дня месяца. Что делать, если выбранный день месяца между 28 и 30 числами? - person CAPS LOCK; 20.08.2016

YearMonth

Если вы хотите, чтобы вашей целью был конец месяца, думайте о месяце, а не о конкретной дате.

Класс YearMonth представляет любой конкретный месяц.

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

Вы можете добавить к этому объекту месяцы.

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

Если вам нужна дата конца этого месяца, спросите об этом, позвонив по телефону _ 5_.

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

Сделать if( dayOfMonth > 28 ), а не перехватить исключение.

person Basil Bourque    schedule 20.08.2016
comment
Конец месяца - это не цель, а скорее проблема, которая не позволяет увеличить объект даты в течение месяца без потери выбранного дня месяца. Я обновил вопрос, поскольку он может быть недостаточно ясным. - person CAPS LOCK; 20.08.2016

Мне просто нужно было решить эту проблему самому, хотя и с немного другим вводом, и в итоге я получил решение, приведенное ниже. Как отмечали другие, YearMonth может быть полезным в этом случае, но я думаю, что его истинная сила заключается в использовании в качестве 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)));
}

Или используйте DayOfMonth от org.threeten.:

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