What is the equivalent of Calendar.roll in java.time?

我的未来我决定 提交于 2019-12-01 22:29:32

First, I cannot remember having seen any useful application of Calendar.roll. Second, I don’t think that the functionality is very well specified in corner cases. And the corner cases would be the interesting ones. Rolling month by 13 months would not be hard without the rollmethod. It may be that similar observations are the reasons why this functionality is not offered by java.time.

Instead I believe that we would have to resort to more manual ways of rolling. For your first example:

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

Output:

2019-08-22

I am using the fact that in the ISO chronology there are always 12 months in the year. Since % always gives a 0-based result, I subtract 1 from the 1-based month value before the modulo operation and add it back in afterwards And I am assuming a positive roll. If the number of months to roll may be negative, it gets slightly more complicated (left to the reader).

For other fields I think that a similar approach will work for most cases: Find the smallest and the largest possible value of the field given the larger fields and do some modulo operation.

It may become a challenge in some cases. For example, when summer time (DST) ends and the clock is turned backward from 3 to 2 AM, so the day is 25 hours long, how would you roll 37 hours from 6 AM? I’m sure it can be done. And I am also sure that the functionality is not built in.

For your example with rolling the week of month, another difference between the old and the modern API comes into play: a GregorianCalendar not only defines a calendar day and time, it also defines a week scheme consisting of a first day of the week and a minimum number of days in the first week. In java.time the week scheme is defined by a WeekFields object instead. So while rolling the week of month may be unambiguous in GregorianCalendar, without knowing the week scheme it isn’t with LocalDate or LocalDateTime. An attempt may be to assume ISO weeks (start on Monday, and the first week is the on that has at least 4 days of the new month in it), but it may not always be what a user had intended.

Week of month and week of year are special since weeks cross month and year boundaries. Here’s my attempt to implement a roll of week of month:

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

Try it out:

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

Output:

1999-06-01
1999-06-30

Explanation: The documentation you quote assumes that Sunday is the first day of the week (it ends “where Sunday is the first day of the week”; it was probably written in the USA) so there is a week before Sunday June 6. And rolling by -1 week should roll into this week before. My first line of code does that.

In the ISO week scheme, Sunday June 6 belong to the week from Monday May 31 through Sunday June 6, so in June there is no week before this week. Therefore my second line of code rolls into the last week of June, June 28 through July 4. Since we cannot go outside June, June 30 is chosen.

I have not tested whether it behaves the same as GregorianCalendar. For comparison,the GregorianCalendar.roll implementation uses 52 code lines to handle the WEEK_OF_MONTH case, compared to my 20 lines. Either I have left something out of consideration, or java.time once again shows it superiority.

Rather my suggestion for the real world is: make your requirements clear and implement them directly on top of java.time, ignoring how the old API behaved. As an academic exercise, your question is a fun and interesting one.

TL;DR

There is no equivalent.

Think about whether you really need the behavior of roll of 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);
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!