我正在研究旧的Calendar API,以了解它的严重程度,然后发现Calendar具有 roll 方法。与add方法不同,roll不会更改较大日历字段的值。

例如,日历实例c表示日期2019-08-31。调用c.roll(Calendar.MONTH, 13)将13添加到month字段,但不更改年份,因此结果为2019-09-30。请注意,月份的日期会更改,因为它是一个较小的字段。

Related

我试图在现代java.time API中找到这种方法。我以为这样的方法必须在LocalDateLocalDateTime中,但是我什么也没找到。

因此,我尝试编写自己的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));
}

但是,这仅适用于某些情况,不适用于其他情况。例如,它不适用于documentation here中描述的情况:



我的代码:
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 API中的等效java.time是什么?如果没有,我该如何编写一种模仿它的方法?

最佳答案

首先,我不记得曾经看过Calendar.roll的任何有用的应用程序。其次,我认为在极端情况下不能很好地指定功能。而最极端的情况将是有趣的情况。如果没有roll方法,将月份滚动13个月将不会很困难。可能类似的观察结果是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);

输出:



我使用的事实是,在ISO年表中,一年总是有12个月。由于%始终给出从0开始的结果,因此我在进行模运算之前从基于1的月份值中减去1,然后再加回去。如果滚动的月份数可能为负,那么它将变得稍微复杂一些(留给读者阅读)。

对于其他字段,我认为在大多数情况下也可以使用类似的方法:给定较大的字段,找到字段的最小和最大可能值,并进行模运算。

在某些情况下,这可能会成为挑战。例如,当夏令时(DST)结束并且时钟从3 AM倒退到2 AM,因此一天是25小时长,您将如何从6 AM开始滚动37小时?我确定可以做到。而且我也确信该功能未内置。

在您滚动一个月的星期的示例中,旧API和现代API之间的另一个区别开始起作用:GregorianCalendar不仅定义了日历的日期和时间,还定义了一个星期方案,该方案由一周的第一天和一个星期几组成。第一周的最少天数。在java.time中,周方案由WeekFields对象定义。因此,在滚动一个月中的星期几时,GregorianCalendar可能是明确的,但不知道星期方案,它与LocalDateLocalDateTime无关。可以尝试假定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));

输出:



说明:您引用的文档假定星期日是一周的第一天(以“星期日是一周的第一天结尾”;它可能是在美国写的),因此在6月6日星期日之前还有一周。滚动-1周应进入本周之前。我的第一行代码就是这样做的。

在ISO周计划中,6月6日星期日属于从5月31日星期一到6月6日星期日的星期,因此6月在这个星期之前没有星期。因此,我的第二行代码进入6月28日的6月的最后一周到7月4日。由于我们不能选择6月30日,所以我们不去讨论。

我尚未测试它的行为是否与GregorianCalendar相同。为了进行比较,GregorianCalendar.roll实现使用52条代码行来处理WEEK_OF_MONTH情况,而我的代码行是20行。我要么没有考虑任何事情,要么java.time再次显示了它的优越性。

相反,我对现实世界的建议是:明确您的需求,并直接在java.time之上实现它们,而忽略旧API的行为方式。作为一项学术练习,您的问题是一个有趣而有趣的问题。

关于java - 在java.time中,Calendar.roll等效于什么?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/57326072/

10-10 19:27