Skip to content

Commit ced3f13

Browse files
author
Justin Lu
committed
8367901: Calendar.roll(hour, 24) returns wrong result
Reviewed-by: naoto, iris
1 parent 2f74e14 commit ced3f13

File tree

2 files changed

+144
-3
lines changed

2 files changed

+144
-3
lines changed

src/java.base/share/classes/java/util/GregorianCalendar.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 1996, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -1214,8 +1214,10 @@ public void roll(int field, int amount) {
12141214
d.setHours(hourOfDay);
12151215
time = calsys.getTime(d);
12161216

1217-
// If we stay on the same wall-clock time, try the next or previous hour.
1218-
if (internalGet(HOUR_OF_DAY) == d.getHours()) {
1217+
// If the rolled amount is not a full HOUR/HOUR_OF_DAY (12/24-hour) cycle and
1218+
// if we stay on the same wall-clock time, try the next or previous hour.
1219+
if (((field == HOUR_OF_DAY && amount % 24 != 0) || (field == HOUR && amount % 12 != 0))
1220+
&& internalGet(HOUR_OF_DAY) == d.getHours()) {
12191221
hourOfDay = getRolledValue(rolledValue, amount > 0 ? +1 : -1, min, max);
12201222
if (field == HOUR && internalGet(AM_PM) == PM) {
12211223
hourOfDay += 12;
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
/*
25+
* @test
26+
* @bug 8367901
27+
* @summary Ensure hour rolling is correct. Particularly, when the HOUR/HOUR_OF_DAY
28+
* amount rolled would cause the calendar to originate on the same hour as before
29+
* the call.
30+
* @run junit RollHoursTest
31+
*/
32+
33+
import org.junit.jupiter.api.BeforeEach;
34+
import org.junit.jupiter.params.ParameterizedTest;
35+
import org.junit.jupiter.params.provider.FieldSource;
36+
37+
import java.text.DateFormat;
38+
import java.util.Calendar;
39+
import java.util.Date;
40+
import java.util.GregorianCalendar;
41+
import java.util.List;
42+
import java.util.TimeZone;
43+
import java.util.stream.IntStream;
44+
45+
import static org.junit.jupiter.api.Assertions.assertEquals;
46+
47+
public class RollHoursTest {
48+
49+
// Should trigger multiple full HOUR/HOUR_OF_DAY cycles
50+
private static final List<Integer> hours =
51+
IntStream.rangeClosed(-55, 55).boxed().toList();
52+
// Various calendars to test against
53+
private static final List<Calendar> calendars = List.of(
54+
// GMT, independent of daylight saving time transitions
55+
new GregorianCalendar(TimeZone.getTimeZone("GMT")),
56+
// Daylight saving observing calendars
57+
new GregorianCalendar(TimeZone.getTimeZone("America/Chicago")),
58+
new GregorianCalendar(TimeZone.getTimeZone("America/Chicago")),
59+
new GregorianCalendar(TimeZone.getTimeZone("America/Los_Angeles")),
60+
new GregorianCalendar(TimeZone.getTimeZone("America/Los_Angeles"))
61+
);
62+
63+
// Reset the times of each calendar. These calendars provide testing under
64+
// daylight saving transitions (or the lack thereof) and different AM/PM hours.
65+
@BeforeEach
66+
void clear() {
67+
// Reset all calendars each iteration for clean slate
68+
calendars.forEach(Calendar::clear);
69+
70+
// Basic test, independent of daylight saving transitions
71+
calendars.get(0).set(2005, 8, 20, 12, 10, 25);
72+
73+
// Transition to daylight saving time (CST/CDT) ---
74+
// Day of transition: 03/13/2016 (Sunday)
75+
// Most interesting test case due to 2 AM skip
76+
calendars.get(1).set(2016, 2, 13, 3, 30, 55);
77+
// Day before transition: 03/12/2016 (Saturday)
78+
calendars.get(2).set(2016, 2, 12, 15, 20, 45);
79+
80+
// Transition back to standard time (PST/PDT) ----
81+
// Day of transition: 11/06/2016 (Sunday)
82+
calendars.get(3).set(2016, 10, 6, 4, 15, 20);
83+
// Day before transition: 11/05/2016 (Saturday)
84+
calendars.get(4).set(2016, 10, 5, 12, 25, 30);
85+
}
86+
87+
// Rolling the HOUR_OF_DAY field.
88+
@ParameterizedTest
89+
@FieldSource("hours")
90+
void HourOfDayTest(int hoursToRoll) {
91+
for (var cal : calendars) {
92+
var savedTime = cal.getTime();
93+
var savedHour = cal.get(Calendar.HOUR_OF_DAY);
94+
cal.roll(Calendar.HOUR_OF_DAY, hoursToRoll);
95+
assertEquals(getExpectedHour(hoursToRoll, savedHour, 24, cal, savedTime),
96+
cal.get(Calendar.HOUR_OF_DAY),
97+
getMessage(cal.getTimeZone(), savedTime, hoursToRoll));
98+
}
99+
}
100+
101+
// Rolling the HOUR field.
102+
@ParameterizedTest
103+
@FieldSource("hours")
104+
void HourTest(int hoursToRoll) {
105+
for (var cal : calendars) {
106+
var savedTime = cal.getTime();
107+
var savedHour = cal.get(Calendar.HOUR_OF_DAY);
108+
cal.roll(Calendar.HOUR, hoursToRoll);
109+
assertEquals(getExpectedHour(hoursToRoll, savedHour, 12, cal, savedTime),
110+
cal.get(Calendar.HOUR),
111+
getMessage(cal.getTimeZone(), savedTime, hoursToRoll));
112+
}
113+
}
114+
115+
// Gets the expected hour after rolling by X hours. Supports 12/24-hour cycle.
116+
// Special handling for non-existent 2 AM case on transition day.
117+
private static int getExpectedHour(int roll, int hour, int hourCycle, Calendar cal, Date oldDate) {
118+
// For example with HOUR_OF_DAY at 15 and a 24-hour cycle
119+
// For rolling forwards 50 hours -> (50 + 15) % 24 = 17
120+
// For hour backwards 50 hours -> (24 + (15 - 50) % 24) % 24
121+
// -> (24 - 11) % 24 = 13
122+
var result = (roll >= 0 ? (hour + roll) : (hourCycle + (hour + roll) % hourCycle)) % hourCycle;
123+
124+
// 2 AM does not exist on transition day. Calculate normalized value accordingly
125+
if (cal.getTimeZone().inDaylightTime(oldDate) && cal.get(Calendar.MONTH) == Calendar.MARCH && result == 2) {
126+
return roll > 0 ? result + 1 : result - 1;
127+
} else {
128+
// Normal return value
129+
return result;
130+
}
131+
}
132+
133+
// Get a TimeZone adapted error message
134+
private static String getMessage(TimeZone tz, Date date, int hoursToRoll) {
135+
var fmt = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL);
136+
fmt.setTimeZone(tz);
137+
return fmt.format(date) + " incorrectly rolled " + hoursToRoll;
138+
}
139+
}

0 commit comments

Comments
 (0)