]> granicus.if.org Git - icu/commitdiff
ICU-10544 Fixed some implementation problems in Calendar#add. When adding day or...
authorYoshito Umaoka <y.umaoka@gmail.com>
Tue, 25 Feb 2014 23:51:51 +0000 (23:51 +0000)
committerYoshito Umaoka <y.umaoka@gmail.com>
Tue, 25 Feb 2014 23:51:51 +0000 (23:51 +0000)
X-SVN-Rev: 35232

icu4j/main/classes/core/src/com/ibm/icu/util/Calendar.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/IBMCalendarTest.java

index 796c52a40c56795b920000afdc923c992e77c301..58df511e56d71834085011495084d692bcb1df07 100644 (file)
@@ -3064,10 +3064,10 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
         // a computed amount of millis to the current millis.  The only
         // wrinkle is with DST (and/or a change to the zone's UTC offset, which
         // we'll include with DST) -- for some fields, like the DAY_OF_MONTH,
-        // we don't want the HOUR to shift due to changes in DST.  If the
+        // we don't want the wall time to shift due to changes in DST.  If the
         // result of the add operation is to move from DST to Standard, or
         // vice versa, we need to adjust by an hour forward or back,
-        // respectively.  For such fields we set keepHourInvariant to true.
+        // respectively.  For such fields we set keepWallTimeInvariant to true.
 
         // We only adjust the DST for fields larger than an hour.  For
         // fields smaller than an hour, we cannot adjust for DST without
@@ -3082,7 +3082,7 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
         // <April 30>, rather than <April 31> => <May 1>.
 
         long delta = amount; // delta in ms
-        boolean keepHourInvariant = true;
+        boolean keepWallTimeInvariant = true;
 
         switch (field) {
         case ERA:
@@ -3144,22 +3144,22 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
         case HOUR_OF_DAY:
         case HOUR:
             delta *= ONE_HOUR;
-            keepHourInvariant = false;
+            keepWallTimeInvariant = false;
             break;
 
         case MINUTE:
             delta *= ONE_MINUTE;
-            keepHourInvariant = false;
+            keepWallTimeInvariant = false;
             break;
 
         case SECOND:
             delta *= ONE_SECOND;
-            keepHourInvariant = false;
+            keepWallTimeInvariant = false;
             break;
 
         case MILLISECOND:
         case MILLISECONDS_IN_DAY:
-            keepHourInvariant = false;
+            keepWallTimeInvariant = false;
             break;
 
         default:
@@ -3167,40 +3167,61 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
                                                ") not supported");
         }
 
-        // In order to keep the hour invariant (for fields where this is
+        // In order to keep the wall time invariant (for fields where this is
         // appropriate), check the combined DST & ZONE offset before and
         // after the add() operation. If it changes, then adjust the millis
         // to compensate.
         int prevOffset = 0;
-        int hour = 0;
-        if (keepHourInvariant) {
+        int prevWallTime = 0;
+        if (keepWallTimeInvariant) {
             prevOffset = get(DST_OFFSET) + get(ZONE_OFFSET);
-            hour = internalGet(HOUR_OF_DAY);
+            prevWallTime = get(MILLISECONDS_IN_DAY);
         }
 
         setTimeInMillis(getTimeInMillis() + delta);
 
-        if (keepHourInvariant) {
-            int newOffset = get(DST_OFFSET) + get(ZONE_OFFSET);
-            if (newOffset != prevOffset) {
-                // We have done an hour-invariant adjustment but the
-                // combined offset has changed. We adjust millis to keep
-                // the hour constant. In cases such as midnight after
-                // a DST change which occurs at midnight, there is the
-                // danger of adjusting into a different day. To avoid
-                // this we make the adjustment only if it actually
-                // maintains the hour.
-
-                // When the difference of the previous UTC offset and
-                // the new UTC offset exceeds 1 full day, we do not want
-                // to roll over/back the date. For now, this only happens
-                // in Samoa (Pacific/Apia) on Dec 30, 2011. See ticket:9452.
-                long adjAmount = (prevOffset - newOffset) % ONE_DAY;
-                if (adjAmount != 0) {
-                    long t = time;
-                    setTimeInMillis(time + adjAmount);
-                    if (get(HOUR_OF_DAY) != hour) {
-                        setTimeInMillis(t);
+        if (keepWallTimeInvariant) {
+            int newWallTime = get(MILLISECONDS_IN_DAY);
+            if (newWallTime != prevWallTime) {
+                // There is at least one zone transition between the base
+                // time and the result time. As the result, wall time has
+                // changed.
+                long t = internalGetTimeInMillis();
+                int newOffset = get(DST_OFFSET) + get(ZONE_OFFSET);
+                if (newOffset != prevOffset) {
+                    // When the difference of the previous UTC offset and
+                    // the new UTC offset exceeds 1 full day, we do not want
+                    // to roll over/back the date. For now, this only happens
+                    // in Samoa (Pacific/Apia) on Dec 30, 2011. See ticket:9452.
+                    long adjAmount = (prevOffset - newOffset) % ONE_DAY;
+                    if (adjAmount != 0) {
+                        setTimeInMillis(t + adjAmount);
+                        newWallTime = get(MILLISECONDS_IN_DAY);
+                    }
+                    if (newWallTime != prevWallTime) {
+                        // The result wall time or adjusted wall time was shifted because
+                        // the target wall time does not exist on the result date.
+                        switch (skippedWallTime) {
+                        case WALLTIME_FIRST:
+                            if (adjAmount > 0) {
+                                setTimeInMillis(t);
+                            }
+                            break;
+                        case WALLTIME_LAST:
+                            if (adjAmount < 0) {
+                                setTimeInMillis(t);
+                            }
+                            break;
+                        case WALLTIME_NEXT_VALID:
+                            long tmpT = adjAmount > 0 ? internalGetTimeInMillis() : t;
+                            Long immediatePrevTrans = getImmediatePreviousZoneTransition(tmpT);
+                            if (immediatePrevTrans != null) {
+                                setTimeInMillis(immediatePrevTrans);
+                            } else {
+                                throw new RuntimeException("Could not locate a time zone transition before " + tmpT);
+                            }
+                            break;
+                        }
                     }
                 }
             }
@@ -5047,7 +5068,7 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
      * millisecond time value <code>time</code>.
      * @stable ICU 2.0
      */
-   protected void computeTime() {
+    protected void computeTime() {
         if (!isLenient()) {
             validateFields();
         }
@@ -5126,26 +5147,11 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
                     // Adjust time to the next valid wall clock time.
                     // At this point, tmpTime is on or after the zone offset transition causing
                     // the skipped time range.
-                    if (zone instanceof BasicTimeZone) {
-                        TimeZoneTransition transition = ((BasicTimeZone)zone).getPreviousTransition(tmpTime, true);
-                        if (transition == null) {
-                            // Could not find any transitions
-                            throw new RuntimeException("Could not locate previous zone transition");
-                        }
-                        time = transition.getTime();
-                    } else {
-                        // Usually, it is enough to check past one hour because such transition is most
-                        // likely +1 hour shift. However, there is an example jumped +24 hour in the tz database.
-                        Long transitionT = getPreviousZoneTransitionTime(zone, tmpTime, 2*60*60*1000); // check last 2 hours
-                        if (transitionT == null) {
-                            transitionT = getPreviousZoneTransitionTime(zone, tmpTime, 30*60*60*1000); // try last 30 hours
-                            if (transitionT == null) {
-                                // Could not find any transitions in last 30 hours...
-                                throw new RuntimeException("Could not locate previous zone transition within 30 hours from " + tmpTime);
-                            }
-                        }
-                        time = transitionT.longValue();
+                    Long immediatePrevTransition = getImmediatePreviousZoneTransition(tmpTime);
+                    if (immediatePrevTransition == null) {
+                        throw new RuntimeException("Could not locate a time zone transition before " + tmpTime);
                     }
+                    time = immediatePrevTransition;
                 } else {
                     time = tmpTime;
                 }
@@ -5155,16 +5161,40 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
         }
     }
 
+    /**
+     * Find the previous zone transtion near the given time.
+     * 
+     * @param base The base time, inclusive.
+     * @return The time of the previous transition, or null if not found.
+     */
+    private Long getImmediatePreviousZoneTransition(long base) {
+        Long transitionTime = null;
+
+        if (zone instanceof BasicTimeZone) {
+            TimeZoneTransition transition = ((BasicTimeZone) zone).getPreviousTransition(base, true);
+            if (transition != null) {
+                transitionTime = transition.getTime();
+            }
+        } else {
+            // Usually, it is enough to check past one hour because such transition is most
+            // likely +1 hour shift. However, there is an example jumped +24 hour in the tz database.
+            transitionTime = getPreviousZoneTransitionTime(zone, base, 2 * 60 * 60 * 1000); // check last 2 hours
+            if (transitionTime == null) {
+                transitionTime = getPreviousZoneTransitionTime(zone, base, 30 * 60 * 60 * 1000); // try last 30 hours
+            }
+        }
+        return transitionTime;
+    }
+
    /**
     * Find the previous zone transition within the specified duration.
-    * Note: This method should not be used when TimeZone is a BasicTimeZone.
-    * {@link BasicTimeZone#getPreviousTransition(long, boolean)} is much more efficient.
+    * Note: This method is only used when TimeZone is NOT a BasicTimeZone.
     * @param tz The time zone.
     * @param base The base time, inclusive.
     * @param duration The range of time evaluated.
     * @return The time of the previous zone transition, or null if not available.
     */
-   private Long getPreviousZoneTransitionTime(TimeZone tz, long base, long duration) {
+   private static Long getPreviousZoneTransitionTime(TimeZone tz, long base, long duration) {
        assert duration > 0;
 
        long upper = base;
@@ -5196,7 +5226,7 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
     * @param lower The lower bound, exclusive.
     * @return The time of the previous zone transition, or null if not available.
     */
-   private Long findPreviousZoneTransitionTime(TimeZone tz, int upperOffset, long upper, long lower) {
+   private static Long findPreviousZoneTransitionTime(TimeZone tz, int upperOffset, long upper, long lower) {
        boolean onUnitTime = false;
        long mid = 0;
 
index 2bf4bd37fdd420f4434a2dadf39490b05ff6eda2..ac41ca063b1ba3fe19f9ac68c26ec0bff6e4504a 100644 (file)
@@ -1097,23 +1097,30 @@ public class IBMCalendarTest extends CalendarTest {
         private int hour;
         private int min;
         private int sec;
+        private int ms;
 
         CalFields(int year, int month, int day, int hour, int min, int sec) {
+            this(year, month, day, hour, min, sec, 0);
+        }
+
+        CalFields(int year, int month, int day, int hour, int min, int sec, int ms) {
             this.year = year;
             this.month = month;
             this.day = day;
             this.hour = hour;
             this.min = min;
             this.sec = sec;
+            this.ms = ms;
         }
 
         void setTo(Calendar cal) {
             cal.clear();
             cal.set(year,  month - 1, day, hour, min, sec);
+            cal.set(Calendar.MILLISECOND, ms);
         }
 
         public String toString() {
-            return String.format("%04d-%02d-%02d %02d:%02d:%02d", year, month, day, hour, min, sec);
+            return String.format("%04d-%02d-%02d %02d:%02d:%02d.%03d", year, month, day, hour, min, sec, ms);
         }
 
         public boolean equals(Object other) {
@@ -1124,11 +1131,22 @@ public class IBMCalendarTest extends CalendarTest {
                         && day == otr.day
                         && hour == otr.hour
                         && min == otr.min
-                        && sec == otr.sec);
+                        && sec == otr.sec
+                        && ms == otr.ms);
             }
             return false;
         }
  
+        boolean isEquivalentTo(Calendar cal) {
+            return year == cal.get(Calendar.YEAR)
+                    && month == cal.get(Calendar.MONTH) + 1
+                    && day == cal.get(Calendar.DAY_OF_MONTH)
+                    && hour == cal.get(Calendar.HOUR_OF_DAY)
+                    && min == cal.get(Calendar.MINUTE)
+                    && sec == cal.get(Calendar.SECOND)
+                    && ms == cal.get(Calendar.MILLISECOND);
+        }
+
         static CalFields createFrom(Calendar cal) {
             int year = cal.get(Calendar.YEAR);
             int month = cal.get(Calendar.MONTH) + 1;
@@ -1681,4 +1699,175 @@ public class IBMCalendarTest extends CalendarTest {
             }
         }
     }
+
+    public void TestAddAcrossZoneTransition() {
+        class TestData {
+            String zone;
+            CalFields base;
+            int deltaDays;
+            int skippedWTOpt;
+            CalFields expected;
+
+            TestData(String zone, CalFields base, int deltaDays, int skippedWTOpt, CalFields expected) {
+                this.zone = zone;
+                this.base = base;
+                this.deltaDays = deltaDays;
+                this.skippedWTOpt = skippedWTOpt;
+                this.expected = expected;
+            }
+        }
+
+        TestData[] data = new TestData[] {
+            // Add 1 day, from the date before DST transition
+            new TestData("America/Los_Angeles", new CalFields(2014, 3, 8, 1, 59, 59, 999), 1, Calendar.WALLTIME_FIRST,
+                                                new CalFields(2014, 3, 9, 1, 59, 59, 999)),
+
+            new TestData("America/Los_Angeles", new CalFields(2014, 3, 8, 1, 59, 59, 999), 1, Calendar.WALLTIME_LAST,
+                                                new CalFields(2014, 3, 9, 1, 59, 59, 999)),
+
+            new TestData("America/Los_Angeles", new CalFields(2014, 3, 8, 1, 59, 59, 999), 1, Calendar.WALLTIME_NEXT_VALID,
+                                                new CalFields(2014, 3, 9, 1, 59, 59, 999)),
+
+
+            new TestData("America/Los_Angeles", new CalFields(2014, 3, 8, 2, 0, 0, 0), 1, Calendar.WALLTIME_FIRST,
+                                                new CalFields(2014, 3, 9, 1, 0, 0, 0)),
+
+            new TestData("America/Los_Angeles", new CalFields(2014, 3, 8, 2, 0, 0, 0), 1, Calendar.WALLTIME_LAST,
+                                                new CalFields(2014, 3, 9, 3, 0, 0, 0)),
+
+            new TestData("America/Los_Angeles", new CalFields(2014, 3, 8, 2, 0, 0, 0), 1, Calendar.WALLTIME_NEXT_VALID,
+                                                new CalFields(2014, 3, 9, 3, 0, 0, 0)),
+
+
+            new TestData("America/Los_Angeles", new CalFields(2014, 3, 8, 2, 30, 0, 0), 1, Calendar.WALLTIME_FIRST,
+                                                new CalFields(2014, 3, 9, 1, 30, 0, 0)),
+
+            new TestData("America/Los_Angeles", new CalFields(2014, 3, 8, 2, 30, 0, 0), 1, Calendar.WALLTIME_LAST,
+                                                new CalFields(2014, 3, 9, 3, 30, 0, 0)),
+
+            new TestData("America/Los_Angeles", new CalFields(2014, 3, 8, 2, 30, 0, 0), 1, Calendar.WALLTIME_NEXT_VALID,
+                                                new CalFields(2014, 3, 9, 3, 0, 0, 0)),
+
+
+            new TestData("America/Los_Angeles", new CalFields(2014, 3, 8, 3, 0, 0, 0), 1, Calendar.WALLTIME_FIRST,
+                                                new CalFields(2014, 3, 9, 3, 0, 0, 0)),
+
+            new TestData("America/Los_Angeles", new CalFields(2014, 3, 8, 3, 0, 0, 0), 1, Calendar.WALLTIME_LAST,
+                                                new CalFields(2014, 3, 9, 3, 0, 0, 0)),
+
+            new TestData("America/Los_Angeles", new CalFields(2014, 3, 8, 3, 0, 0, 0), 1, Calendar.WALLTIME_NEXT_VALID,
+                                                new CalFields(2014, 3, 9, 3, 0, 0, 0)),
+
+
+            // Subtract 1 day, from one day after DST transition
+            new TestData("America/Los_Angeles", new CalFields(2014, 3, 10, 1, 59, 59, 999), -1, Calendar.WALLTIME_FIRST,
+                                                new CalFields(2014, 3, 9, 1, 59, 59, 999)),
+
+            new TestData("America/Los_Angeles", new CalFields(2014, 3, 10, 1, 59, 59, 999), -1, Calendar.WALLTIME_LAST,
+                                                new CalFields(2014, 3, 9, 1, 59, 59, 999)),
+
+            new TestData("America/Los_Angeles", new CalFields(2014, 3, 10, 1, 59, 59, 999), -1, Calendar.WALLTIME_NEXT_VALID,
+                                                new CalFields(2014, 3, 9, 1, 59, 59, 999)),
+
+
+            new TestData("America/Los_Angeles", new CalFields(2014, 3, 10, 2, 0, 0, 0), -1, Calendar.WALLTIME_FIRST,
+                                                new CalFields(2014, 3, 9, 1, 0, 0, 0)),
+
+            new TestData("America/Los_Angeles", new CalFields(2014, 3, 10, 2, 0, 0, 0), -1, Calendar.WALLTIME_LAST,
+                                                new CalFields(2014, 3, 9, 3, 0, 0, 0)),
+
+            new TestData("America/Los_Angeles", new CalFields(2014, 3, 10, 2, 0, 0, 0), -1, Calendar.WALLTIME_NEXT_VALID,
+                                                new CalFields(2014, 3, 9, 3, 0, 0, 0)),
+
+
+            new TestData("America/Los_Angeles", new CalFields(2014, 3, 10, 2, 30, 0, 0), -1, Calendar.WALLTIME_FIRST,
+                                                new CalFields(2014, 3, 9, 1, 30, 0, 0)),
+
+            new TestData("America/Los_Angeles", new CalFields(2014, 3, 10, 2, 30, 0, 0), -1, Calendar.WALLTIME_LAST,
+                                                new CalFields(2014, 3, 9, 3, 30, 0, 0)),
+
+            new TestData("America/Los_Angeles", new CalFields(2014, 3, 10, 2, 30, 0, 0), -1, Calendar.WALLTIME_NEXT_VALID,
+                                                new CalFields(2014, 3, 9, 3, 0, 0, 0)),
+
+
+            new TestData("America/Los_Angeles", new CalFields(2014, 3, 10, 3, 0, 0, 0), -1, Calendar.WALLTIME_FIRST,
+                                                new CalFields(2014, 3, 9, 3, 0, 0, 0)),
+
+            new TestData("America/Los_Angeles", new CalFields(2014, 3, 10, 3, 0, 0, 0), -1, Calendar.WALLTIME_LAST,
+                                                new CalFields(2014, 3, 9, 3, 0, 0, 0)),
+
+            new TestData("America/Los_Angeles", new CalFields(2014, 3, 10, 3, 0, 0, 0), -1, Calendar.WALLTIME_NEXT_VALID,
+                                                new CalFields(2014, 3, 9, 3, 0, 0, 0)),
+
+
+            // Test case for ticket#10544
+            new TestData("America/Santiago",    new CalFields(2013, 4, 27, 0, 0, 0, 0), 134, Calendar.WALLTIME_FIRST,
+                                                new CalFields(2013, 9, 7, 23, 0, 0, 0)),
+
+            new TestData("America/Santiago",    new CalFields(2013, 4, 27, 0, 0, 0, 0), 134, Calendar.WALLTIME_LAST,
+                                                new CalFields(2013, 9, 8, 1, 0, 0, 0)),
+
+            new TestData("America/Santiago",    new CalFields(2013, 4, 27, 0, 0, 0, 0), 134, Calendar.WALLTIME_NEXT_VALID,
+                                                new CalFields(2013, 9, 8, 1, 0, 0, 0)),
+
+
+            new TestData("America/Santiago",    new CalFields(2013, 4, 27, 0, 30, 0, 0), 134, Calendar.WALLTIME_FIRST,
+                                                new CalFields(2013, 9, 7, 23, 30, 0, 0)),
+
+            new TestData("America/Santiago",    new CalFields(2013, 4, 27, 0, 30, 0, 0), 134, Calendar.WALLTIME_LAST,
+                                                new CalFields(2013, 9, 8, 1, 30, 0, 0)),
+
+            new TestData("America/Santiago",    new CalFields(2013, 4, 27, 0, 30, 0, 0), 134, Calendar.WALLTIME_NEXT_VALID,
+                                                new CalFields(2013, 9, 8, 1, 0, 0, 0)),
+
+
+            // Extreme transition - Pacific/Apia completely skips 2011-12-30
+            new TestData("Pacific/Apia",        new CalFields(2011, 12, 29, 0, 0, 0, 0), 1, Calendar.WALLTIME_FIRST,
+                                                new CalFields(2011, 12, 31, 0, 0, 0, 0)),
+
+            new TestData("Pacific/Apia",        new CalFields(2011, 12, 29, 0, 0, 0, 0), 1, Calendar.WALLTIME_LAST,
+                                                new CalFields(2011, 12, 31, 0, 0, 0, 0)),
+
+            new TestData("Pacific/Apia",        new CalFields(2011, 12, 29, 0, 0, 0, 0), 1, Calendar.WALLTIME_NEXT_VALID,
+                                                new CalFields(2011, 12, 31, 0, 0, 0, 0)),
+
+
+            new TestData("Pacific/Apia",        new CalFields(2011, 12, 31, 12, 0, 0, 0), -1, Calendar.WALLTIME_FIRST,
+                                                new CalFields(2011, 12, 29, 12, 0, 0, 0)),
+
+            new TestData("Pacific/Apia",        new CalFields(2011, 12, 31, 12, 0, 0, 0), -1, Calendar.WALLTIME_LAST,
+                                                new CalFields(2011, 12, 29, 12, 0, 0, 0)),
+
+            new TestData("Pacific/Apia",        new CalFields(2011, 12, 31, 12, 0, 0, 0), -1, Calendar.WALLTIME_NEXT_VALID,
+                                                new CalFields(2011, 12, 29, 12, 0, 0, 0)),
+
+
+            // 30 minutes DST - Australia/Lord_Howe
+            new TestData("Australia/Lord_Howe", new CalFields(2013, 10, 5, 2, 15, 0, 0), 1, Calendar.WALLTIME_FIRST,
+                                                new CalFields(2013, 10, 6, 1, 45, 0, 0)),
+
+            new TestData("Australia/Lord_Howe", new CalFields(2013, 10, 5, 2, 15, 0, 0), 1, Calendar.WALLTIME_LAST,
+                                                new CalFields(2013, 10, 6, 2, 45, 0, 0)),
+
+            new TestData("Australia/Lord_Howe", new CalFields(2013, 10, 5, 2, 15, 0, 0), 1, Calendar.WALLTIME_NEXT_VALID,
+                                                new CalFields(2013, 10, 6, 2, 30, 0, 0)),
+        };
+
+        Calendar cal = Calendar.getInstance();
+        for (TestData d : data) {
+            cal.setTimeZone(TimeZone.getTimeZone(d.zone));
+            cal.setSkippedWallTimeOption(d.skippedWTOpt);
+            d.base.setTo(cal);
+            cal.add(Calendar.DATE, d.deltaDays);
+
+            if (!d.expected.isEquivalentTo(cal)) {
+                CalFields res = CalFields.createFrom(cal);
+                String optDisp = d.skippedWTOpt == Calendar.WALLTIME_FIRST ? "FIRST" :
+                    d.skippedWTOpt == Calendar.WALLTIME_LAST ? "LAST" : "NEXT_VALID";
+                errln("Error: base:" + d.base.toString() + ", tz:" + d.zone
+                        + ", delta:" + d.deltaDays + " day(s), opt:" + optDisp
+                        + ", result:" + res.toString() + " - expected:" + d.expected.toString());
+            }
+        }
+    }
 }