Minimal fix for Calendar.setTimeZone.
The interpretation of Calendar's competing member variables when the time,
time zone, and fields[] don't agree is confusing, and led to
Calendar.setTimeZone having no effect. I've improved the documentation
based on my reverse-engineering and fixed this bug. I noticed a lot of
other code that looks incorrect though, and I've raised http://b/2419810
suggesting that we switch to icu4j's calendars in the longer term.
Bug: http://code.google.com/p/android/issues/detail?id=6184
diff --git a/luni/src/main/java/java/util/Calendar.java b/luni/src/main/java/java/util/Calendar.java
index 7f2e92d..4d7ede9 100644
--- a/luni/src/main/java/java/util/Calendar.java
+++ b/luni/src/main/java/java/util/Calendar.java
@@ -285,36 +285,48 @@
* @see GregorianCalendar
* @see TimeZone
*/
-public abstract class Calendar implements Serializable, Cloneable,
- Comparable<Calendar> {
+public abstract class Calendar implements Serializable, Cloneable, Comparable<Calendar> {
private static final long serialVersionUID = -1807547505821590642L;
/**
- * Set to {@code true} when the calendar fields have been set from the time, set to
- * {@code false} when a field is changed and the fields must be recomputed.
+ * True iff the values in {@code fields[]} correspond to {@code time}. Despite the name, this
+ * is effectively "are the values in fields[] up-to-date?" --- {@code fields[]} may contain
+ * non-zero values and {@code isSet[]} may contain {@code true} values even when
+ * {@code areFieldsSet} is false.
+ * Accessing the fields via {@code get} will ensure the fields are up-to-date.
*/
protected boolean areFieldsSet;
/**
- * An integer array of calendar fields. The length is {@code FIELD_COUNT}.
+ * Contains broken-down field values for the current value of {@code time} if
+ * {@code areFieldsSet} is true, or stale data corresponding to some previous value otherwise.
+ * Accessing the fields via {@code get} will ensure the fields are up-to-date.
+ * The array length is always {@code FIELD_COUNT}.
*/
protected int[] fields;
/**
- * A boolean array. Each element indicates if the corresponding field has
- * been set. The length is {@code FIELD_COUNT}.
+ * Whether the corresponding element in {@code field[]} has been set. Initially, these are all
+ * false. The first time the fields are computed, these are set to true and remain set even if
+ * the data becomes stale: you <i>must</i> check {@code areFieldsSet} if you want to know
+ * whether the value is up-to-date.
+ * Note that {@code isSet} is <i>not</i> a safe alternative to accessing this array directly,
+ * and will likewise return stale data!
+ * The array length is always {@code FIELD_COUNT}.
*/
protected boolean[] isSet;
/**
- * Set to {@code true} when the time has been set, set to {@code false} when a field is
- * changed and the time must be recomputed.
+ * Whether {@code time} corresponds to the values in {@code fields[]}. If false, {@code time}
+ * is out-of-date with respect to changes {@code fields[]}.
+ * Accessing the time via {@code getTimeInMillis} will always return the correct value.
*/
protected boolean isTimeSet;
/**
- * The time in milliseconds since January 1, 1970.
+ * A time in milliseconds since January 1, 1970. See {@code isTimeSet}.
+ * Accessing the time via {@code getTimeInMillis} will always return the correct value.
*/
protected long time;
@@ -1116,7 +1128,16 @@
}
/**
- * Returns whether the specified field is set.
+ * Returns whether the specified field is set. Note that the interpretation of "is set" is
+ * somewhat technical. In particular, it does <i>not</i> mean that the field's value is up
+ * to date. If you want to know whether a field contains an up-to-date value, you must also
+ * check {@code areFieldsSet}, making this method somewhat useless unless you're a subclass,
+ * in which case you can access the {@code isSet} array directly.
+ * <p>
+ * A field remains "set" from the first time its value is computed until it's cleared by one
+ * of the {@code clear} methods. Thus "set" does not mean "valid". You probably want to call
+ * {@code get} -- which will update fields as necessary -- rather than try to make use of
+ * this method.
*
* @param field
* a {@code Calendar} field number.
diff --git a/luni/src/main/java/java/util/GregorianCalendar.java b/luni/src/main/java/java/util/GregorianCalendar.java
index 7339151..846ac3e 100644
--- a/luni/src/main/java/java/util/GregorianCalendar.java
+++ b/luni/src/main/java/java/util/GregorianCalendar.java
@@ -591,15 +591,14 @@
@Override
protected void computeFields() {
- int zoneOffset = getTimeZone().getRawOffset();
-
- if(!isSet[ZONE_OFFSET]) {
- fields[ZONE_OFFSET] = zoneOffset;
- }
+ TimeZone timeZone = getTimeZone();
+ int dstOffset = timeZone.inDaylightTime(new Date(time)) ? timeZone.getDSTSavings() : 0;
+ int zoneOffset = timeZone.getRawOffset();
+ fields[DST_OFFSET] = dstOffset;
+ fields[ZONE_OFFSET] = zoneOffset;
int millis = (int) (time % 86400000);
int savedMillis = millis;
- int dstOffset = fields[DST_OFFSET];
// compute without a change in daylight saving time
int offset = zoneOffset + dstOffset;
long newTime = time + offset;
@@ -610,6 +609,8 @@
newTime = 0x8000000000000000L;
}
+ // FIXME: I don't think this caching ever really gets used, because it requires that the
+ // time zone doesn't use daylight savings (ever). So unless you're somewhere like Taiwan...
if (isCached) {
if (millis < 0) {
millis += 86400000;
@@ -636,11 +637,11 @@
fields[AM_PM] = fields[HOUR_OF_DAY] > 11 ? 1 : 0;
fields[HOUR] = fields[HOUR_OF_DAY] % 12;
+ // FIXME: this has to be wrong; useDaylightTime doesn't mean what they think it means!
long newTimeAdjusted = newTime;
- if (getTimeZone().useDaylightTime()) {
+ if (timeZone.useDaylightTime()) {
// BEGIN android-changed: removed unnecessary cast
- int dstSavings = (/* (SimpleTimeZone) */ getTimeZone())
- .getDSTSavings();
+ int dstSavings = timeZone.getDSTSavings();
// END android-changed
newTimeAdjusted += (dstOffset == 0) ? dstSavings : -dstSavings;
}
@@ -665,7 +666,7 @@
if (!isCached
&& newTime != 0x7fffffffffffffffL
&& newTime != 0x8000000000000000L
- && (!getTimeZone().useDaylightTime() || getTimeZone() instanceof SimpleTimeZone)) {
+ && (!timeZone.useDaylightTime() || timeZone instanceof SimpleTimeZone)) {
int cacheMillis = 0;
cachedFields[0] = fields[YEAR];
diff --git a/luni/src/test/java/java/util/AllTests.java b/luni/src/test/java/java/util/AllTests.java
index 774d48b..07fee27 100644
--- a/luni/src/test/java/java/util/AllTests.java
+++ b/luni/src/test/java/java/util/AllTests.java
@@ -22,6 +22,7 @@
public class AllTests {
public static final Test suite() {
TestSuite suite = tests.TestSuiteFactory.createTestSuite();
+ suite.addTestSuite(java.util.CalendarTest.class);
suite.addTestSuite(java.util.CurrencyTest.class);
suite.addTestSuite(java.util.DateTest.class);
suite.addTestSuite(java.util.FormatterTest.class);
diff --git a/luni/src/test/java/java/util/CalendarTest.java b/luni/src/test/java/java/util/CalendarTest.java
new file mode 100644
index 0000000..f9f6fbe
--- /dev/null
+++ b/luni/src/test/java/java/util/CalendarTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package java.util;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+public class CalendarTest extends junit.framework.TestCase {
+ // http://code.google.com/p/android/issues/detail?id=6184
+ public void test_setTimeZone() {
+ // The specific time zones don't matter; they just have to be different so we can see that
+ // get(Calendar.ZONE_OFFSET) returns the zone offset of the time zone passed to setTimeZone.
+ Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.US);
+ assertEquals(0, cal.get(Calendar.ZONE_OFFSET));
+ TimeZone tz = java.util.TimeZone.getTimeZone("GMT+7");
+ cal.setTimeZone(tz);
+ assertEquals(25200000, cal.get(Calendar.ZONE_OFFSET));
+ }
+}