java.time 中的 Calendar.roll 相当于什么?
What is the equivalent of Calendar.roll in java.time?
我正在研究旧的Calendar
API 看看它有多糟糕,我发现Calendar
有一个roll
方法。与 add
方法不同,roll
不会更改较大日历字段的值。
例如,日历实例 c
表示日期 2019-08-31。调用 c.roll(Calendar.MONTH, 13)
会将 13 添加到月份字段,但不会更改年份,因此结果为 2019-09-30。请注意,月份的日期会发生变化,因为它是一个较小的字段。
Related
我试图在现代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));
}
但是,这仅适用于某些情况,而对其他情况无效。例如,它不适用于 documentation here:
中描述的情况
Consider a GregorianCalendar originally set to Sunday June 6, 1999.
Calling roll(Calendar.WEEK_OF_MONTH, -1) sets the calendar to Tuesday
June 1, 1999, whereas calling add(Calendar.WEEK_OF_MONTH, -1) sets the
calendar to Sunday May 30, 1999. This is because the roll rule imposes
an additional constraint: The MONTH must not change when the
WEEK_OF_MONTH is rolled. Taken together with add rule 1, the resultant
date must be between Tuesday June 1 and Saturday June 5. According to
add rule 2, the DAY_OF_WEEK, an invariant when changing the
WEEK_OF_MONTH, is set to Tuesday, the closest possible value to Sunday
(where Sunday is the first day of the week).
我的代码:
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
。
java.time
API 中 roll
的等价物是什么?如果没有,我怎么写一个方法来模仿它?
首先,我不记得曾见过 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);
输出:
2019-08-22
我使用的事实是,在 ISO 年表中,一年中总是有 12 个月。由于 %
总是给出基于 0 的结果,因此我在模运算之前从基于 1 的月份值中减去 1,然后将其加回去我假设滚动为正。如果要滚动的月数可能是负数,它会稍微复杂一些(留给 reader)。
对于其他字段,我认为类似的方法适用于大多数情况:在给定较大字段的情况下,找到字段的最小和最大可能值,然后进行一些模运算。
在某些情况下这可能会成为一个挑战。例如,当夏令时 (DST) 结束并且时钟从凌晨 3 点向后拨到凌晨 2 点时,一天是 25 小时,您如何从早上 6 点开始滚动 37 小时?我相信这是可以做到的。而且我也确定该功能不是内置的。
对于滚动一周的示例,旧式和现代式 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 月 6 日星期日。按 -1 周滚动应该滚动到本周之前。我的第一行代码就是这样做的。
在 ISO 周计划中,6 月 6 日星期日属于从 5 月 31 日星期一到 6 月 6 日星期日的一周,因此 6 月没有本周之前的一周。因此,我的第二行代码滚动到 6 月的最后一周,即 6 月 28 日到 7 月 4 日。由于我们不能离开 6 月,因此选择 6 月 30 日。
我没有测试它的行为是否与GregorianCalendar
相同。为了比较,GregorianCalendar.roll
实现使用 52 行代码来处理 WEEK_OF_MONTH
情况,而我的是 20 行。要么我遗漏了一些东西,要么java.time再次显示它的优越性。
相反,我对现实世界的建议是:明确您的要求并直接在 java.time 之上实现它们,而忽略旧 API 的表现。作为一个学术练习,你的问题很有趣。
TL;DR
没有等价物。
想想你是否真的需要java.util.Calendar
的roll
的行为:
/**
* 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);
我正在研究旧的Calendar
API 看看它有多糟糕,我发现Calendar
有一个roll
方法。与 add
方法不同,roll
不会更改较大日历字段的值。
例如,日历实例 c
表示日期 2019-08-31。调用 c.roll(Calendar.MONTH, 13)
会将 13 添加到月份字段,但不会更改年份,因此结果为 2019-09-30。请注意,月份的日期会发生变化,因为它是一个较小的字段。
Related
我试图在现代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));
}
但是,这仅适用于某些情况,而对其他情况无效。例如,它不适用于 documentation here:
中描述的情况Consider a GregorianCalendar originally set to Sunday June 6, 1999. Calling roll(Calendar.WEEK_OF_MONTH, -1) sets the calendar to Tuesday June 1, 1999, whereas calling add(Calendar.WEEK_OF_MONTH, -1) sets the calendar to Sunday May 30, 1999. This is because the roll rule imposes an additional constraint: The MONTH must not change when the WEEK_OF_MONTH is rolled. Taken together with add rule 1, the resultant date must be between Tuesday June 1 and Saturday June 5. According to add rule 2, the DAY_OF_WEEK, an invariant when changing the WEEK_OF_MONTH, is set to Tuesday, the closest possible value to Sunday (where Sunday is the first day of the week).
我的代码:
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
。
java.time
API 中 roll
的等价物是什么?如果没有,我怎么写一个方法来模仿它?
首先,我不记得曾见过 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);
输出:
2019-08-22
我使用的事实是,在 ISO 年表中,一年中总是有 12 个月。由于 %
总是给出基于 0 的结果,因此我在模运算之前从基于 1 的月份值中减去 1,然后将其加回去我假设滚动为正。如果要滚动的月数可能是负数,它会稍微复杂一些(留给 reader)。
对于其他字段,我认为类似的方法适用于大多数情况:在给定较大字段的情况下,找到字段的最小和最大可能值,然后进行一些模运算。
在某些情况下这可能会成为一个挑战。例如,当夏令时 (DST) 结束并且时钟从凌晨 3 点向后拨到凌晨 2 点时,一天是 25 小时,您如何从早上 6 点开始滚动 37 小时?我相信这是可以做到的。而且我也确定该功能不是内置的。
对于滚动一周的示例,旧式和现代式 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 月 6 日星期日。按 -1 周滚动应该滚动到本周之前。我的第一行代码就是这样做的。
在 ISO 周计划中,6 月 6 日星期日属于从 5 月 31 日星期一到 6 月 6 日星期日的一周,因此 6 月没有本周之前的一周。因此,我的第二行代码滚动到 6 月的最后一周,即 6 月 28 日到 7 月 4 日。由于我们不能离开 6 月,因此选择 6 月 30 日。
我没有测试它的行为是否与GregorianCalendar
相同。为了比较,GregorianCalendar.roll
实现使用 52 行代码来处理 WEEK_OF_MONTH
情况,而我的是 20 行。要么我遗漏了一些东西,要么java.time再次显示它的优越性。
相反,我对现实世界的建议是:明确您的要求并直接在 java.time 之上实现它们,而忽略旧 API 的表现。作为一个学术练习,你的问题很有趣。
TL;DR
没有等价物。
想想你是否真的需要java.util.Calendar
的roll
的行为:
/**
* 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);