From 2e381f32782481e133fa6f342d74391fb9abdda7 Mon Sep 17 00:00:00 2001 From: Roland Mesde Date: Tue, 11 Nov 2025 09:45:34 -0800 Subject: [PATCH 1/3] Backport ced3f13f4e036513444d1fea3958be11741c2b8e --- .../classes/java/util/GregorianCalendar.java | 8 +- .../jdk/java/util/Calendar/RollHoursTest.java | 139 ++++++++++++++++++ 2 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 test/jdk/java/util/Calendar/RollHoursTest.java diff --git a/src/java.base/share/classes/java/util/GregorianCalendar.java b/src/java.base/share/classes/java/util/GregorianCalendar.java index 86ec76def53..29720769a72 100644 --- a/src/java.base/share/classes/java/util/GregorianCalendar.java +++ b/src/java.base/share/classes/java/util/GregorianCalendar.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -1202,8 +1202,10 @@ public void roll(int field, int amount) { d.setHours(hourOfDay); time = calsys.getTime(d); - // If we stay on the same wall-clock time, try the next or previous hour. - if (internalGet(HOUR_OF_DAY) == d.getHours()) { + // If the rolled amount is not a full HOUR/HOUR_OF_DAY (12/24-hour) cycle and + // if we stay on the same wall-clock time, try the next or previous hour. + if (((field == HOUR_OF_DAY && amount % 24 != 0) || (field == HOUR && amount % 12 != 0)) + && internalGet(HOUR_OF_DAY) == d.getHours()) { hourOfDay = getRolledValue(rolledValue, amount > 0 ? +1 : -1, min, max); if (field == HOUR && internalGet(AM_PM) == PM) { hourOfDay += 12; diff --git a/test/jdk/java/util/Calendar/RollHoursTest.java b/test/jdk/java/util/Calendar/RollHoursTest.java new file mode 100644 index 00000000000..2eb85caab85 --- /dev/null +++ b/test/jdk/java/util/Calendar/RollHoursTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8367901 + * @summary Ensure hour rolling is correct. Particularly, when the HOUR/HOUR_OF_DAY + * amount rolled would cause the calendar to originate on the same hour as before + * the call. + * @run junit RollHoursTest + */ + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.FieldSource; + +import java.text.DateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.TimeZone; +import java.util.stream.IntStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RollHoursTest { + + // Should trigger multiple full HOUR/HOUR_OF_DAY cycles + private static final List hours = + IntStream.rangeClosed(-55, 55).boxed().toList(); + // Various calendars to test against + private static final List calendars = List.of( + // GMT, independent of daylight saving time transitions + new GregorianCalendar(TimeZone.getTimeZone("GMT")), + // Daylight saving observing calendars + new GregorianCalendar(TimeZone.getTimeZone("America/Chicago")), + new GregorianCalendar(TimeZone.getTimeZone("America/Chicago")), + new GregorianCalendar(TimeZone.getTimeZone("America/Los_Angeles")), + new GregorianCalendar(TimeZone.getTimeZone("America/Los_Angeles")) + ); + + // Reset the times of each calendar. These calendars provide testing under + // daylight saving transitions (or the lack thereof) and different AM/PM hours. + @BeforeEach + void clear() { + // Reset all calendars each iteration for clean slate + calendars.forEach(Calendar::clear); + + // Basic test, independent of daylight saving transitions + calendars.get(0).set(2005, 8, 20, 12, 10, 25); + + // Transition to daylight saving time (CST/CDT) --- + // Day of transition: 03/13/2016 (Sunday) + // Most interesting test case due to 2 AM skip + calendars.get(1).set(2016, 2, 13, 3, 30, 55); + // Day before transition: 03/12/2016 (Saturday) + calendars.get(2).set(2016, 2, 12, 15, 20, 45); + + // Transition back to standard time (PST/PDT) ---- + // Day of transition: 11/06/2016 (Sunday) + calendars.get(3).set(2016, 10, 6, 4, 15, 20); + // Day before transition: 11/05/2016 (Saturday) + calendars.get(4).set(2016, 10, 5, 12, 25, 30); + } + + // Rolling the HOUR_OF_DAY field. + @ParameterizedTest + @FieldSource("hours") + void HourOfDayTest(int hoursToRoll) { + for (var cal : calendars) { + var savedTime = cal.getTime(); + var savedHour = cal.get(Calendar.HOUR_OF_DAY); + cal.roll(Calendar.HOUR_OF_DAY, hoursToRoll); + assertEquals(getExpectedHour(hoursToRoll, savedHour, 24, cal, savedTime), + cal.get(Calendar.HOUR_OF_DAY), + getMessage(cal.getTimeZone(), savedTime, hoursToRoll)); + } + } + + // Rolling the HOUR field. + @ParameterizedTest + @FieldSource("hours") + void HourTest(int hoursToRoll) { + for (var cal : calendars) { + var savedTime = cal.getTime(); + var savedHour = cal.get(Calendar.HOUR_OF_DAY); + cal.roll(Calendar.HOUR, hoursToRoll); + assertEquals(getExpectedHour(hoursToRoll, savedHour, 12, cal, savedTime), + cal.get(Calendar.HOUR), + getMessage(cal.getTimeZone(), savedTime, hoursToRoll)); + } + } + + // Gets the expected hour after rolling by X hours. Supports 12/24-hour cycle. + // Special handling for non-existent 2 AM case on transition day. + private static int getExpectedHour(int roll, int hour, int hourCycle, Calendar cal, Date oldDate) { + // For example with HOUR_OF_DAY at 15 and a 24-hour cycle + // For rolling forwards 50 hours -> (50 + 15) % 24 = 17 + // For hour backwards 50 hours -> (24 + (15 - 50) % 24) % 24 + // -> (24 - 11) % 24 = 13 + var result = (roll >= 0 ? (hour + roll) : (hourCycle + (hour + roll) % hourCycle)) % hourCycle; + + // 2 AM does not exist on transition day. Calculate normalized value accordingly + if (cal.getTimeZone().inDaylightTime(oldDate) && cal.get(Calendar.MONTH) == Calendar.MARCH && result == 2) { + return roll > 0 ? result + 1 : result - 1; + } else { + // Normal return value + return result; + } + } + + // Get a TimeZone adapted error message + private static String getMessage(TimeZone tz, Date date, int hoursToRoll) { + var fmt = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL); + fmt.setTimeZone(tz); + return fmt.format(date) + " incorrectly rolled " + hoursToRoll; + } +} From d7e28735aeb3f485d979547f531e393543e717c4 Mon Sep 17 00:00:00 2001 From: Roland Mesde Date: Tue, 11 Nov 2025 12:32:42 -0800 Subject: [PATCH 2/3] Update RollHoursTest.java Modify for JDK 17 --- .../jdk/java/util/Calendar/RollHoursTest.java | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/test/jdk/java/util/Calendar/RollHoursTest.java b/test/jdk/java/util/Calendar/RollHoursTest.java index 2eb85caab85..7affeb906c4 100644 --- a/test/jdk/java/util/Calendar/RollHoursTest.java +++ b/test/jdk/java/util/Calendar/RollHoursTest.java @@ -32,9 +32,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.FieldSource; +import org.junit.jupiter.params.provider.MethodSource; import java.text.DateFormat; +import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; @@ -47,10 +48,12 @@ public class RollHoursTest { // Should trigger multiple full HOUR/HOUR_OF_DAY cycles - private static final List hours = - IntStream.rangeClosed(-55, 55).boxed().toList(); + private static List hours() { + return IntStream.rangeClosed(-55, 55).boxed().collect(java.util.stream.Collectors.toList()); + } + // Various calendars to test against - private static final List calendars = List.of( + private static final List calendars = Arrays.asList( // GMT, independent of daylight saving time transitions new GregorianCalendar(TimeZone.getTimeZone("GMT")), // Daylight saving observing calendars @@ -86,11 +89,11 @@ void clear() { // Rolling the HOUR_OF_DAY field. @ParameterizedTest - @FieldSource("hours") + @MethodSource("hours") void HourOfDayTest(int hoursToRoll) { - for (var cal : calendars) { - var savedTime = cal.getTime(); - var savedHour = cal.get(Calendar.HOUR_OF_DAY); + for (Calendar cal : calendars) { + Date savedTime = cal.getTime(); + int savedHour = cal.get(Calendar.HOUR_OF_DAY); cal.roll(Calendar.HOUR_OF_DAY, hoursToRoll); assertEquals(getExpectedHour(hoursToRoll, savedHour, 24, cal, savedTime), cal.get(Calendar.HOUR_OF_DAY), @@ -100,11 +103,11 @@ void HourOfDayTest(int hoursToRoll) { // Rolling the HOUR field. @ParameterizedTest - @FieldSource("hours") + @MethodSource("hours") void HourTest(int hoursToRoll) { - for (var cal : calendars) { - var savedTime = cal.getTime(); - var savedHour = cal.get(Calendar.HOUR_OF_DAY); + for (Calendar cal : calendars) { + Date savedTime = cal.getTime(); + int savedHour = cal.get(Calendar.HOUR_OF_DAY); cal.roll(Calendar.HOUR, hoursToRoll); assertEquals(getExpectedHour(hoursToRoll, savedHour, 12, cal, savedTime), cal.get(Calendar.HOUR), @@ -119,7 +122,7 @@ private static int getExpectedHour(int roll, int hour, int hourCycle, Calendar c // For rolling forwards 50 hours -> (50 + 15) % 24 = 17 // For hour backwards 50 hours -> (24 + (15 - 50) % 24) % 24 // -> (24 - 11) % 24 = 13 - var result = (roll >= 0 ? (hour + roll) : (hourCycle + (hour + roll) % hourCycle)) % hourCycle; + int result = (roll >= 0 ? (hour + roll) : (hourCycle + (hour + roll) % hourCycle)) % hourCycle; // 2 AM does not exist on transition day. Calculate normalized value accordingly if (cal.getTimeZone().inDaylightTime(oldDate) && cal.get(Calendar.MONTH) == Calendar.MARCH && result == 2) { @@ -132,7 +135,7 @@ private static int getExpectedHour(int roll, int hour, int hourCycle, Calendar c // Get a TimeZone adapted error message private static String getMessage(TimeZone tz, Date date, int hoursToRoll) { - var fmt = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL); + DateFormat fmt = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL); fmt.setTimeZone(tz); return fmt.format(date) + " incorrectly rolled " + hoursToRoll; } From 7be27ce4b0c52aa190f0d8a751320c3b3642f4ed Mon Sep 17 00:00:00 2001 From: Roland Mesde Date: Tue, 11 Nov 2025 12:56:49 -0800 Subject: [PATCH 3/3] Update RollHoursTest.java Fix formatting --- test/jdk/java/util/Calendar/RollHoursTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jdk/java/util/Calendar/RollHoursTest.java b/test/jdk/java/util/Calendar/RollHoursTest.java index 7affeb906c4..504e0348429 100644 --- a/test/jdk/java/util/Calendar/RollHoursTest.java +++ b/test/jdk/java/util/Calendar/RollHoursTest.java @@ -51,7 +51,7 @@ public class RollHoursTest { private static List hours() { return IntStream.rangeClosed(-55, 55).boxed().collect(java.util.stream.Collectors.toList()); } - + // Various calendars to test against private static final List calendars = Arrays.asList( // GMT, independent of daylight saving time transitions