Calling new Date(...) with a time that falls in the 'missing hour' of
a daylight savings time transition currently behaves differently in
(Sun's JDK) Java versus (some browsers') Javascript.  In Java, the time
is moved forward by an hour, while in some browsers it is moved
backward, resulting in a confusing change to the date in locations
such as Brazil where the transition occurs at midnight.  This patch
attempts to enforce the Java behavior.

Review by: bobv



git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@6674 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/super/com/google/gwt/emul/java/util/Date.java b/user/super/com/google/gwt/emul/java/util/Date.java
index 98bd244..918b5d2 100644
--- a/user/super/com/google/gwt/emul/java/util/Date.java
+++ b/user/super/com/google/gwt/emul/java/util/Date.java
@@ -105,7 +105,7 @@
       int min, int sec) /*-{
     return Date.UTC(year + 1900, month, date, hrs, min, sec);
   }-*/;
-
+  
   /**
    * JavaScript Date instance.
    */
@@ -206,7 +206,7 @@
 
   public native int getYear() /*-{
     this.@java.util.Date::checkJsDate()();
-    return this.@java.util.Date::jsdate.getFullYear()-1900;
+    return this.@java.util.Date::jsdate.getFullYear() - 1900;
   }-*/;
 
   @Override
@@ -216,27 +216,36 @@
 
   public native void setDate(int date) /*-{
     this.@java.util.Date::checkJsDate()();
+    var hours = this.@java.util.Date::jsdate.getHours()
     this.@java.util.Date::jsdate.setDate(date);
+    this.@java.util.Date::fixDaylightSavings(I)(hours);
   }-*/;
 
   public native void setHours(int hours) /*-{
     this.@java.util.Date::checkJsDate()();
     this.@java.util.Date::jsdate.setHours(hours);
+    this.@java.util.Date::fixDaylightSavings(I)(hours);
   }-*/;
 
   public native void setMinutes(int minutes) /*-{
     this.@java.util.Date::checkJsDate()();
+    var hours = this.@java.util.Date::jsdate.getHours() + minutes / 60;
     this.@java.util.Date::jsdate.setMinutes(minutes);
+    this.@java.util.Date::fixDaylightSavings(I)(hours);
   }-*/;
 
   public native void setMonth(int month) /*-{
     this.@java.util.Date::checkJsDate()();
+    var hours = this.@java.util.Date::jsdate.getHours();
     this.@java.util.Date::jsdate.setMonth(month);
+    this.@java.util.Date::fixDaylightSavings(I)(hours);
   }-*/;
 
   public native void setSeconds(int seconds) /*-{
     this.@java.util.Date::checkJsDate()();
+    var hours = this.@java.util.Date::jsdate.getHours() + seconds / (60 * 60);
     this.@java.util.Date::jsdate.setSeconds(seconds);
+    this.@java.util.Date::fixDaylightSavings(I)(hours);
   }-*/;
 
   public void setTime(long time) {
@@ -245,7 +254,9 @@
 
   public native void setYear(int year) /*-{
     this.@java.util.Date::checkJsDate()();
+    var hours = this.@java.util.Date::jsdate.getHours()
     this.@java.util.Date::jsdate.setFullYear(year + 1900);
+    this.@java.util.Date::fixDaylightSavings(I)(hours);
   }-*/;
 
   public native String toGMTString() /*-{
@@ -306,6 +317,57 @@
           + this.@java.util.Date::jsdate);
     }
   }-*/;
+  
+  /*
+   * Some browsers have the following behavior:
+   * 
+   * // Assume a U.S. time zone with daylight savings
+   * // Set a non-existent time: 2:00 am Sunday March 8, 2009
+   * var date = new Date(2009, 2, 8, 2, 0, 0);
+   * var hours = date.getHours(); // returns 1
+   * 
+   * The equivalent Java code will return 3. To compensate, we determine the
+   * amount of daylight savings adjustment by comparing the time zone offsets
+   * for the requested time and a time one day later, and add the adjustment to
+   * the hours and minutes of the requested time.
+   */
+
+  /**
+   * Detects if the requested time falls into a non-existent time range due to
+   * local time advancing into daylight savings time. If so, push the requested
+   * time forward out of the non-existent range.
+   */
+  @SuppressWarnings("unused") // called by JSNI
+  private native void fixDaylightSavings(int hours) /*-{
+    if ((this.@java.util.Date::jsdate.getHours() % 24) != (hours % 24)) {
+      // Find the change in time zone offset between the current
+      // time and the same time the following day
+      var d = new Date();
+      d.setTime(this.@java.util.Date::jsdate.getTime());
+      var noff = d.getTimezoneOffset();
+      d.setDate(d.getDate() + 1);
+      var loff = d.getTimezoneOffset();
+      var timeDiff = noff - loff;
+      
+      // If the time zone offset is changing, advance the hours and
+      // minutes from the initially requested time by the change amount
+      if (timeDiff > 0) {
+        var year = this.@java.util.Date::jsdate.getYear() + 1900;
+        var month = this.@java.util.Date::jsdate.getMonth();
+        var day = this.@java.util.Date::jsdate.getDate();
+        var badHours = this.@java.util.Date::jsdate.getHours();
+        var minute = this.@java.util.Date::jsdate.getMinutes();
+        var second = this.@java.util.Date::jsdate.getSeconds();
+        if (badHours + timeDiff / 60 >= 24) {
+          day++;
+        }
+        var newTime = new Date(year, month, day,
+            hours + timeDiff / 60,
+            minute + timeDiff % 60, second);
+        this.@java.util.Date::jsdate.setTime(newTime.getTime());
+      }
+    }
+  }-*/;
 
   private native double getTime0() /*-{
     this.@java.util.Date::checkJsDate()();
@@ -326,6 +388,9 @@
     this.@java.util.Date::checkJsDate()();
     this.@java.util.Date::jsdate.setFullYear(year + 1900, month, date);
     this.@java.util.Date::jsdate.setHours(hrs, min, sec, 0);
+    
+    // Set the expected hour.
+    this.@java.util.Date::fixDaylightSavings(I)(hrs);
   }-*/;
 
   private native void setTime0(double time) /*-{
diff --git a/user/test/com/google/gwt/emultest/java/util/DateTest.java b/user/test/com/google/gwt/emultest/java/util/DateTest.java
index 8c48a96..65f4671 100644
--- a/user/test/com/google/gwt/emultest/java/util/DateTest.java
+++ b/user/test/com/google/gwt/emultest/java/util/DateTest.java
@@ -18,6 +18,7 @@
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.junit.client.GWTTestCase;
 
+import java.util.ArrayList;
 import java.util.Date;
 
 /**
@@ -639,6 +640,218 @@
     long a2 = accum2.UTC(arg30, arg31, arg32, arg33, arg34, arg35);
   }
 
+  // Month and date of days with time shifts
+  private ArrayList<Integer> timeShiftMonth = new ArrayList<Integer>();
+  private ArrayList<Integer> timeShiftDate = new ArrayList<Integer>();
+  
+  private boolean containsTimeShift(Date start, int days) {
+    long startTime = start.getTime();
+    Date end = new Date();
+    end.setTime(startTime);
+    end.setDate(start.getDate() + days);
+    long endTime = end.getTime();
+    return (endTime - startTime) != ((long) days * 24 * 60 * 60 * 1000);
+  }
+
+  private void findTimeShift(Date start, int days) {
+    assertTrue(days != 0);
+
+    // Found a shift day
+    if (days == 1) {
+      timeShiftMonth.add(start.getMonth());
+      timeShiftDate.add(start.getDate());
+      return;
+    }
+    
+    // Recurse over the first half of the period
+    if (containsTimeShift(start, days / 2)) {
+      findTimeShift(start, days / 2);
+    }
+    
+    // Recurse over the second half of the period
+    Date mid = new Date();
+    mid.setTime(start.getTime());
+    mid.setDate(start.getDate() + days / 2);
+    if (containsTimeShift(mid, days - days / 2)) {
+      findTimeShift(mid, days - days / 2);
+    }
+  }
+
+  private void findTimeShifts(int year) {
+    timeShiftMonth.clear();
+    timeShiftDate.clear();
+    Date start = new Date(year - 1900, 0, 1, 12, 0, 0);
+    Date end = new Date(year + 1 - 1900, 0, 1, 12, 0, 0);
+    int days = (int) ((end.getTime() - start.getTime()) /
+        (24 * 60 * 60 * 1000));
+    findTimeShift(start, days);
+  }
+
+  private boolean findClockBackwardTime(int year, int[] monthDayHour) {
+    findTimeShifts(year);
+    int numShifts = timeShiftMonth.size();
+    for (int i = 0; i < numShifts; i++) {
+      int month = timeShiftMonth.get(i);
+      int day = timeShiftDate.get(i);
+
+      long start = new Date(year - 1900, month, day, 0, 30, 0).getTime();
+      long end = new Date(year - 1900, month, day + 1, 23, 30, 0).getTime();
+      int lastHour = -1;
+      for (long time = start; time < end; time += 60 * 60 * 1000) {
+        Date d = new Date();
+        d.setTime(time);
+        int hour = d.getHours();
+        if (hour == lastHour) {
+          monthDayHour[0] = d.getMonth();
+          monthDayHour[1] = d.getDate();
+          monthDayHour[2] = d.getHours();
+          return true;
+        }
+        lastHour = hour;
+      }
+    }
+
+    return false;
+  }
+
+  private boolean findClockForwardTime(int year, int[] monthDayHour) {
+    findTimeShifts(year);
+    int numShifts = timeShiftMonth.size();
+    for (int i = 0; i < numShifts; i++) {
+      int month = timeShiftMonth.get(i);
+      int startDay = timeShiftDate.get(i);
+      
+      for (int day = startDay; day <= startDay + 1; day++) {
+        for (int hour = 0; hour < 24; hour++) {
+          Date d = new Date(year - 1900, month, day, hour, 0, 0);
+          int h = d.getHours();
+          if ((h % 24) == ((hour + 1) % 24)) {
+            monthDayHour[0] = month;
+            monthDayHour[1] = day;
+            monthDayHour[2] = hour;
+            return true;
+          }
+        }
+      }
+    }
+
+    return false;
+  }
+  
+  public void testClockBackwardTime() {
+    int[] monthDayHour = new int[3];
+    if (!findClockBackwardTime(2009, monthDayHour)) {
+      return;
+    }
+    
+    Date d;
+    int month = monthDayHour[0];
+    int day = monthDayHour[1];
+    int hour = monthDayHour[2];
+    
+    // Check that this is the later of the two times having the
+    // same hour:minute:second
+    d = new Date(2009 - 1900, month, day, hour, 30, 0);
+    assertEquals(hour, d.getHours());
+    d.setTime(d.getTime() - 60 * 60 * 1000);
+    assertEquals(hour, d.getHours());
+  }
+  
+  public void testClockForwardTime() {
+    int[] monthDayHour = new int[3];
+    if (!findClockForwardTime(2009, monthDayHour)) {
+      return;
+    }
+    
+    Date d;
+    int month = monthDayHour[0];
+    int day = monthDayHour[1];
+    int hour = monthDayHour[2];
+    
+    d = new Date(2009 - 1900, month, day, hour, 0, 0);
+    assertEquals(hour + 1, d.getHours());
+    
+    // Test year change -- assume the previous year changes on a different day
+    d = new Date(2008 - 1900, month, day, hour, 0, 0);
+    assertEquals(hour, d.getHours());
+    d.setYear(2009 - 1900);
+    assertEquals(hour + 1, d.getHours());
+    
+    // Test month change
+    d = new Date(2009 - 1900, month + 1, day, hour, 0, 0);
+    assertEquals(hour, d.getHours());
+    d.setMonth(month);
+    assertEquals(3, d.getHours());
+    
+    // Test day change
+    d = new Date(2009 - 1900, month, day + 1, hour, 0, 0);
+    assertEquals(hour, d.getHours());
+    d.setDate(day);
+    assertEquals(hour + 1, d.getHours());
+    
+    // Test hour setting
+    d = new Date(2009 - 1900, month, day, hour + 2, 0, 0);
+    assertEquals(hour + 2, d.getHours());
+    d.setHours(hour);
+    assertEquals(hour + 1, d.getHours());
+    
+    // Test changing hour by minutes = +- 60
+    d = new Date(2009 - 1900, month, day, hour + 2, 0, 0);
+    assertEquals(hour + 2, d.getHours());
+    d.setMinutes(-60);
+    assertEquals(hour + 1, d.getHours());
+
+    d = new Date(2009 - 1900, month, day, hour - 1, 0, 0);
+    assertEquals(hour - 1, d.getHours());
+    d.setMinutes(60);
+    assertEquals(hour + 1, d.getHours());
+    
+    // Test changing hour by minutes = +- 120
+    d = new Date(2009 - 1900, month, day, hour + 2, 0, 0);
+    assertEquals(hour + 2, d.getHours());
+    d.setMinutes(-120);
+    assertEquals(hour + 1, d.getHours());
+    
+    d = new Date(2009 - 1900, month, day, hour - 2, 0, 0);
+    assertEquals(hour - 2, d.getHours());
+    d.setMinutes(120);
+    assertEquals(hour + 1, d.getHours());
+    
+    // Test changing hour by seconds = +- 3600
+    d = new Date(2009 - 1900, month, day, hour + 2, 0, 0);
+    assertEquals(hour + 2, d.getHours());
+    d.setSeconds(-3600);
+    assertEquals(hour + 1, d.getHours());
+
+    d = new Date(2009 - 1900, month, day, hour - 1, 0, 0);
+    assertEquals(hour - 1, d.getHours());
+    d.setSeconds(3600);
+    assertEquals(hour + 1, d.getHours());
+    
+    // Test changing hour by seconds = +- 7200
+    d = new Date(2009 - 1900, month, day, hour + 2, 0, 0);
+    assertEquals(hour + 2, d.getHours());
+    d.setSeconds(-7200);
+    assertEquals(hour + 1, d.getHours());
+
+    d = new Date(2009 - 1900, month, day, hour - 2, 0, 0);
+    assertEquals(hour - 2, d.getHours());
+    d.setSeconds(7200);
+    assertEquals(hour + 1, d.getHours());
+    
+    d = new Date(2009 - 1900, month, day, hour + 2, 0, 0);
+    d.setHours(hour);
+    d.setMinutes(30);
+    assertEquals(hour + 1, d.getHours());
+    assertEquals(30, d.getMinutes());
+    
+    d = new Date(2009 - 1900, month, day, hour + 2, 0, 0);
+    d.setMinutes(30);
+    d.setHours(hour);
+    assertEquals(hour + 1, d.getHours());
+    assertEquals(30, d.getMinutes());
+  }
+
   Date create() {
     return (Date) theDate.clone();
   }