Что эквивалентно Calendar.roll в java.time?

Я изучал старый Calendar API, чтобы понять, насколько он плох, и обнаружил, что Calendar имеет roll. В отличие от метода add, roll не изменяет значения больших полей календаря.

Например, экземпляр календаря c представляет дату 2019-08-31. Вызов c.roll(Calendar.MONTH, 13) добавляет 13 к полю месяца, но не меняет год, поэтому результат будет 2019-09-30. Обратите внимание, что день месяца меняется, потому что это меньшее поле.

Связано

Я пытался найти такой метод в современном java.time API. Я думал, что такой метод должен быть в LocalDate или LocalDateTime, но ничего подобного не нашел.

Поэтому я попытался написать свой собственный метод roll:

public static LocalDateTime roll(LocalDateTime ldt, TemporalField unit, long amount) {
    LocalDateTime newLdt = ldt.plus(amount, unit.getBaseUnit());
    return ldt.with(unit, newLdt.get(unit));
}

Однако это работает только для некоторых случаев, но не для других. Например, это не работает для случая, описанного в документация здесь:

Рассмотрим GregorianCalendar, первоначально установленный на воскресенье 6 июня 1999 года. Вызов roll(Calendar.WEEK_OF_MONTH, -1) устанавливает календарь на вторник 1 июня 1999 года, тогда как вызов add(Calendar.WEEK_OF_MONTH, -1) устанавливает календарь на воскресенье 30 мая. , 1999. Это связано с тем, что правило пересчета накладывает дополнительное ограничение: МЕСЯЦ не должен меняться, когда пересчитывается WEEK_OF_MONTH. В сочетании с правилом добавления 1 результирующая дата должна быть между вторником 1 июня и субботой 5 июня. Согласно правилу добавления 2, DAY_OF_WEEK, инвариант при изменении WEEK_OF_MONTH, устанавливается на вторник, ближайшее возможное значение к воскресенью (где Воскресенье - первый день недели).

Мой код:

System.out.println(roll(
        LocalDate.of(1999, 6, 6).atStartOfDay(),
        ChronoField.ALIGNED_WEEK_OF_MONTH, -1
));

выводит 1999-07-04T00:00, тогда как использование Calendar:

Calendar c = new GregorianCalendar(1999, 5, 6);
c.roll(Calendar.WEEK_OF_MONTH, -1);
System.out.println(c.getTime().toInstant());

выводит 1999-05-31T23:00:00Z, что соответствует 1999-06-01 в моем часовом поясе.

Что является эквивалентом roll в java.time API? Если его нет, как я могу написать метод для его имитации?


person Sweeper    schedule 21.07.2019    source источник


Ответы (2)


Во-первых, я не помню, чтобы видел какое-либо полезное применение 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

TL;DR

Эквивалента нет.

Подумайте, действительно ли вам нужно поведение roll из java.util.Calendar:

    /**
     * Adds or subtracts (up/down) a single unit of time on the given time
     * field without changing larger fields. For example, to roll the current
     * date up by one day, you can achieve it by calling:
     * roll(Calendar.DATE, true).
     * When rolling on the year or Calendar.YEAR field, it will roll the year
     * value in the range between 1 and the value returned by calling
     * getMaximum(Calendar.YEAR).
     * When rolling on the month or Calendar.MONTH field, other fields like
     * date might conflict and, need to be changed. For instance,
     * rolling the month on the date 01/31/96 will result in 02/29/96.
     * When rolling on the hour-in-day or Calendar.HOUR_OF_DAY field, it will
     * roll the hour value in the range between 0 and 23, which is zero-based.
     *
     * @param field the time field.
     * @param up indicates if the value of the specified time field is to be
     * rolled up or rolled down. Use true if rolling up, false otherwise.
     * @see Calendar#add(int,int)
     * @see Calendar#set(int,int)
     */
    public void roll(int field, boolean up);
person zingi    schedule 02.08.2019