Skip to content

Commit a324fa2

Browse files
Justin Lunaotoj
authored andcommitted
8225641: Calendar.roll(int field) does not work correctly for WEEK_OF_YEAR
Reviewed-by: naoto
1 parent 3399fbf commit a324fa2

File tree

2 files changed

+188
-2
lines changed

2 files changed

+188
-2
lines changed

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

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 1996, 2022, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 1996, 2023, 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
@@ -1307,7 +1307,15 @@ public void roll(int field, int amount) {
13071307
woy = min;
13081308
}
13091309
}
1310-
set(field, getRolledValue(woy, amount, min, max));
1310+
int newWeekOfYear = getRolledValue(woy, amount, min, max);
1311+
// Final check to ensure that the first week has the
1312+
// current DAY_OF_WEEK. Only make a check for
1313+
// rolling up into week 1, as the existing checks
1314+
// sufficiently handle rolling down into week 1.
1315+
if (newWeekOfYear == 1 && isInvalidWeek1() && amount > 0) {
1316+
newWeekOfYear++;
1317+
}
1318+
set(field, newWeekOfYear);
13111319
return;
13121320
}
13131321

@@ -2972,6 +2980,54 @@ private boolean isCutoverYear(int normalizedYear) {
29722980
return normalizedYear == cutoverYear;
29732981
}
29742982

2983+
/**
2984+
* {@return {@code true} if the first week of the current year is minimum
2985+
* and the {@code DAY_OF_WEEK} does not exist in that week}
2986+
*
2987+
* This method is used to check the validity of a {@code WEEK_OF_YEAR} and
2988+
* {@code DAY_OF_WEEK} combo when WEEK_OF_YEAR is rolled to a value of 1.
2989+
* This prevents other methods from calling complete() with an invalid combo.
2990+
*/
2991+
private boolean isInvalidWeek1() {
2992+
// Calculate the DAY_OF_WEEK for Jan 1 of the current YEAR
2993+
long jan1Fd = gcal.getFixedDate(internalGet(YEAR), 1, 1, null);
2994+
int jan1Dow = BaseCalendar.getDayOfWeekFromFixedDate(jan1Fd);
2995+
// Calculate how many days are in the first week
2996+
int daysInFirstWeek;
2997+
if (getFirstDayOfWeek() <= jan1Dow) {
2998+
// Add wrap around days
2999+
daysInFirstWeek = 7 - jan1Dow + getFirstDayOfWeek();
3000+
} else {
3001+
daysInFirstWeek = getFirstDayOfWeek() - jan1Dow;
3002+
}
3003+
// Calculate the end day of the first week
3004+
int endDow = getFirstDayOfWeek() - 1 == 0
3005+
? 7 : getFirstDayOfWeek() - 1;
3006+
// If the week is a valid minimum, check if the DAY_OF_WEEK does not exist
3007+
return daysInFirstWeek >= getMinimalDaysInFirstWeek() &&
3008+
!dayInMinWeek(internalGet(DAY_OF_WEEK), jan1Dow, endDow);
3009+
}
3010+
3011+
/**
3012+
* Given the first day and last day of a week, this method determines
3013+
* if the specified day exists in the minimum week.
3014+
* This method expects all parameters to be passed in as DAY_OF_WEEK values.
3015+
* For example, dayInMinWeek(4, 6, 3) returns false since Wednesday
3016+
* is not between the minimum week given by [Friday, Saturday,
3017+
* Sunday, Monday, Tuesday].
3018+
*/
3019+
private boolean dayInMinWeek (int day, int startDay, int endDay) {
3020+
if (endDay >= startDay) {
3021+
// dayInMinWeek(6, 3, 5), check that 6 is
3022+
// between 3 4 5
3023+
return (day >= startDay && day <= endDay);
3024+
} else {
3025+
// dayInMinWeek(4, 6, 3), check that 4 is
3026+
// between 6 7 1 2 3
3027+
return (day >= startDay || day <= endDay);
3028+
}
3029+
}
3030+
29753031
/**
29763032
* Returns the fixed date of the first day of the year (usually
29773033
* January 1) before the specified date.
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright (c) 2023, 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 8225641
27+
* @summary Test the behavior of GregorianCalendar.roll(WEEK_OF_YEAR)
28+
* when the last week is rolled into the first week of the same year
29+
* @run junit RollFromLastToFirstWeek
30+
*/
31+
32+
33+
import java.util.*;
34+
import java.util.stream.Stream;
35+
import static java.util.Calendar.*;
36+
import static org.junit.jupiter.api.Assertions.fail;
37+
import org.junit.jupiter.params.ParameterizedTest;
38+
import org.junit.jupiter.params.provider.MethodSource;
39+
import org.junit.jupiter.params.provider.Arguments;
40+
41+
/**
42+
* Test to validate the behavior of GregorianCalendar.roll(WEEK_OF_YEAR, +1)
43+
* when rolling from the last week of a year into the first week of the same year.
44+
* This only test the implementation of the Gregorian Calendar roll.
45+
*
46+
* Rolling from the last week of a year into the first week of the same year
47+
* could cause a WEEK_OF_YEAR with a non-existent DAY_OF_WEEK combination.
48+
* The associated fix ensures that a final check is made, so that the first
49+
* week is incremented to prevent this.
50+
*/
51+
public class RollFromLastToFirstWeek {
52+
private static final Builder GREGORIAN_BUILDER = new Builder()
53+
.setCalendarType("gregory");
54+
55+
@ParameterizedTest
56+
@MethodSource("rollUpCalProvider")
57+
public void rollUpTest(Calendar calendar, String[] validDates){
58+
if (calendar instanceof GregorianCalendar) {
59+
testRoll(calendar, validDates);
60+
} else {
61+
fail(String.format("Calendar is not Gregorian: %s", calendar));
62+
}
63+
}
64+
65+
private void testRoll(Calendar calendar, String[] validDates) {
66+
String originalDate = longDateString(calendar);
67+
calendar.roll(Calendar.WEEK_OF_YEAR, 1);
68+
String rolledDate = longDateString(calendar);
69+
if (!Arrays.asList(validDates).contains(rolledDate)) {
70+
fail(String.format("""
71+
{$$$ Failed: Rolled: "%s" by 1 week, where the first day of the week
72+
is: %s with a minimum week length of: %s and was expecting one of: "%s", but got: "%s"},
73+
""", originalDate, calendar.getFirstDayOfWeek(),
74+
calendar.getMinimalDaysInFirstWeek(), Arrays.toString(validDates), rolledDate));
75+
} else {
76+
System.out.printf("""
77+
{$$$ Passed: Rolled: "%s" by 1 week where the first day of the week
78+
is: %s with a minimum week length of: %s and successfully got: "%s"},
79+
""", originalDate, calendar.getFirstDayOfWeek(),
80+
calendar.getMinimalDaysInFirstWeek(), rolledDate);
81+
}
82+
}
83+
84+
// This implicitly tests the Iso8601 calendar as
85+
// MinWeek = 4 and FirstDayOfWeek = Monday is included in the provider
86+
private static Stream<Arguments> rollUpCalProvider() {
87+
ArrayList<Arguments> calList = new ArrayList<Arguments>();
88+
// Week 1, Week 2 are all potential dates to roll into
89+
// Depends on first day of week / min days in week
90+
String[][] validDates = {
91+
{"Wednesday, 2 January 2019", "Wednesday, 9 January 2019"},
92+
{"Thursday, 3 January 2019" , "Thursday, 10 January 2019"},
93+
{"Friday, 4 January 2019" , "Friday, 11 January 2019"},
94+
{"Saturday, 5 January 2019" , "Saturday, 12 January 2019"},
95+
{"Sunday, 6 January 2019" , "Sunday, 13 January 2019"},
96+
{"Monday, 7 January 2019" , "Monday, 14 January 2019"},
97+
{"Tuesday, 1 January 2019" , "Tuesday, 8 January 2019"}
98+
};
99+
int date = 0;
100+
// Test all days at the end of the year that roll into week 1
101+
for (int dayOfMonth = 25; dayOfMonth <= 31; dayOfMonth++) {
102+
for (int weekLength = 1; weekLength <= 7; weekLength++) {
103+
// Sunday .. Monday -> Saturday
104+
for (int firstDay = SUNDAY; firstDay <= SATURDAY; firstDay++) {
105+
calList.add(Arguments.of(buildCalendar(firstDay, weekLength,
106+
dayOfMonth, DECEMBER, 2019), validDates[date]));
107+
}
108+
}
109+
date++;
110+
}
111+
return calList.stream();
112+
}
113+
114+
private static Calendar buildCalendar(int firstDayOfWeek,
115+
int minimumWeekLength, int dayOfMonth,
116+
int month, int year) {
117+
return GREGORIAN_BUILDER
118+
.setWeekDefinition(firstDayOfWeek, minimumWeekLength)
119+
.setDate(year, month, dayOfMonth)
120+
.build();
121+
}
122+
123+
private static String longDateString(Calendar calendar) {
124+
return String.format("%s, %s %s %s",
125+
calendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG, Locale.ENGLISH),
126+
calendar.get(Calendar.DAY_OF_MONTH),
127+
calendar.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.ENGLISH),
128+
calendar.get(YEAR));
129+
}
130+
}

0 commit comments

Comments
 (0)