Во-первых, я не помню, чтобы видел какое-либо полезное применение Calendar.roll
. Во-вторых, я не думаю, что функциональность очень хорошо описана в крайних случаях. И угловые случаи были бы интересными. Скользящий месяц на 13 месяцев был бы несложным без метода roll
. Возможно, подобные наблюдения являются причинами, по которым эта функциональность не предлагается java.time.
Вместо этого я считаю, что нам придется прибегнуть к более ручным способам прокатки. Для вашего первого примера:
LocalDate date = LocalDate.of(2019, Month.JULY, 22);
int newMonthValue = 1 + (date.getMonthValue() - 1 + 13) % 12;
date = date.with(ChronoField.MONTH_OF_YEAR, newMonthValue);
System.out.println(date);
Выход:
2019-08-22
Я использую тот факт, что в хронологии ISO в году всегда 12 месяцев. Поскольку %
всегда дает результат, основанный на 0, я вычитаю 1 из значения месяца, основанного на 1, перед операцией по модулю и добавляю его обратно после этого. И я предполагаю положительный результат. Если количество месяцев для броска может быть отрицательным, это становится немного сложнее (предоставлено читателю).
Я думаю, что для других полей в большинстве случаев будет работать аналогичный подход: найти наименьшее и максимально возможное значение поля с учетом больших полей и выполнить некоторую операцию по модулю.
В некоторых случаях это может стать проблемой. Например, когда заканчивается летнее время (DST) и часы переводятся назад с 3 до 2 часов ночи, то есть день длится 25 часов, как бы вы откатили 37 часов с 6 часов утра? Я уверен, что это можно сделать. И я также уверен, что функциональность не встроена.
В вашем примере со сменой недели месяца проявляется еще одно различие между старым и современным API: GregorianCalendar
не только определяет календарный день и время, но и определяет схему недели, состоящую из первого дня недели и минимальное количество дней в первую неделю. Вместо этого в java.time недельная схема определяется объектом WeekFields
. Таким образом, при прокрутке неделя месяца может быть однозначной в GregorianCalendar
, не зная схемы недели, это не так с LocalDate
или LocalDateTime
. Попытка может заключаться в том, чтобы принять недели ISO (начинаться в понедельник, а первая неделя — это та, в которой есть как минимум 4 дня нового месяца), но это не всегда может быть тем, что предполагал пользователь.
Неделя месяца и неделя года особенные, поскольку недели пересекают границы месяца и года. Вот моя попытка реализовать список недель и месяцев:
private static LocalDate rollWeekOfMonth(LocalDate date, int amount, WeekFields wf) {
LocalDate firstOfMonth = date.withDayOfMonth(1);
int firstWeekOfMonth = firstOfMonth.get(wf.weekOfMonth());
LocalDate lastOfMonth = date.with(TemporalAdjusters.lastDayOfMonth());
int lastWeekOfMonth = lastOfMonth.get(wf.weekOfMonth());
int weekCount = lastWeekOfMonth - firstWeekOfMonth + 1;
int newWeekOfMonth = firstWeekOfMonth
+ (date.get(wf.weekOfMonth()) - firstWeekOfMonth
+ amount % weekCount + weekCount)
% weekCount;
LocalDate result = date.with(wf.weekOfMonth(), newWeekOfMonth);
if (result.isBefore(firstOfMonth)) {
result = firstOfMonth;
} else if (result.isAfter(lastOfMonth)) {
result = lastOfMonth;
}
return result;
}
Попробуйте:
System.out.println(rollWeekOfMonth(LocalDate.of(1999, Month.JUNE, 6), -1, WeekFields.SUNDAY_START));
System.out.println(rollWeekOfMonth(LocalDate.of(1999, Month.JUNE, 6), -1, WeekFields.ISO));
Выход:
1999-06-01
1999-06-30
Объяснение: в документации, которую вы цитируете, предполагается, что воскресенье является первым днем недели (она заканчивается «где воскресенье — первый день недели»; вероятно, это было написано в США), поэтому до воскресенья 6 июня есть неделя. И переход на -1 неделю должен перейти на эту неделю раньше. Моя первая строка кода делает это.
В схеме недель ISO воскресенье 6 июня относится к неделе с понедельника 31 мая по воскресенье 6 июня, поэтому в июне перед этой неделей нет недели. Поэтому моя вторая строка кода переносится на последнюю неделю июня, с 28 июня по 4 июля. Поскольку мы не можем выйти за пределы июня, выбрано 30 июня.
Я не проверял, ведет ли он себя так же, как GregorianCalendar
. Для сравнения, реализация GregorianCalendar.roll
использует 52 строки кода для обработки случая WEEK_OF_MONTH
по сравнению с моими 20 строками. Либо я что-то упустил из виду, либо java.time в очередной раз демонстрирует свое превосходство.
Скорее, мое предложение для реального мира: сделайте ваши требования четкими и реализуйте их непосредственно поверх java.time, игнорируя поведение старого API. Как академическое упражнение, ваш вопрос веселый и интересный.
person
Ole V.V.
schedule
21.07.2019