[CALCITE-1690] Timestamp literals cannot express precision above millisecond

Switch preferred time zone from GMT to UTC.

Close apache/calcite-avatica#9
diff --git a/core/src/main/java/org/apache/calcite/avatica/util/DateTimeUtils.java b/core/src/main/java/org/apache/calcite/avatica/util/DateTimeUtils.java
index 406dcfd..6cf52b6 100644
--- a/core/src/main/java/org/apache/calcite/avatica/util/DateTimeUtils.java
+++ b/core/src/main/java/org/apache/calcite/avatica/util/DateTimeUtils.java
@@ -16,10 +16,12 @@
  */
 package org.apache.calcite.avatica.util;
 
+import java.text.DateFormat;
 import java.text.NumberFormat;
 import java.text.ParsePosition;
 import java.text.SimpleDateFormat;
 import java.util.Calendar;
+import java.util.Date;
 import java.util.Locale;
 import java.util.TimeZone;
 
@@ -92,7 +94,7 @@
   public static final Calendar ZERO_CALENDAR;
 
   static {
-    ZERO_CALENDAR = Calendar.getInstance(DateTimeUtils.GMT_ZONE, Locale.ROOT);
+    ZERO_CALENDAR = Calendar.getInstance(DateTimeUtils.UTC_ZONE, Locale.ROOT);
     ZERO_CALENDAR.setTimeInMillis(0);
   }
 
@@ -106,54 +108,52 @@
    * less than 31, etc.
    *
    * @param s       string to be parsed
-   * @param pattern {@link SimpleDateFormat} pattern (not null)
+   * @param dateFormat Date format
    * @param tz      time zone in which to interpret string. Defaults to the Java
    *                default time zone
    * @param pp      position to start parsing from
    * @return a Calendar initialized with the parsed value, or null if parsing
    * failed. If returned, the Calendar is configured to the GMT time zone.
    */
-  private static Calendar parseDateFormat(
-      String s,
-      String pattern,
-      TimeZone tz,
-      ParsePosition pp) {
-    assert pattern != null;
-    SimpleDateFormat df = new SimpleDateFormat(pattern, Locale.ROOT);
+  private static Calendar parseDateFormat(String s, DateFormat dateFormat,
+      TimeZone tz, ParsePosition pp) {
     if (tz == null) {
       tz = DEFAULT_ZONE;
     }
     Calendar ret = Calendar.getInstance(tz, Locale.ROOT);
-    df.setCalendar(ret);
-    df.setLenient(false);
+    dateFormat.setCalendar(ret);
+    dateFormat.setLenient(false);
 
-    java.util.Date d = df.parse(s, pp);
+    final Date d = dateFormat.parse(s, pp);
     if (null == d) {
       return null;
     }
     ret.setTime(d);
-    ret.setTimeZone(GMT_ZONE);
+    ret.setTimeZone(UTC_ZONE);
     return ret;
   }
 
+  @Deprecated // to be removed before 2.0
+  public static Calendar parseDateFormat(String s, String pattern,
+      TimeZone tz) {
+    return parseDateFormat(s, new SimpleDateFormat(pattern, Locale.ROOT), tz);
+  }
+
   /**
    * Parses a string using {@link SimpleDateFormat} and a given pattern. The
    * entire string must match the pattern specified.
    *
    * @param s       string to be parsed
-   * @param pattern {@link SimpleDateFormat}  pattern
+   * @param dateFormat Date format
    * @param tz      time zone in which to interpret string. Defaults to the Java
    *                default time zone
    * @return a Calendar initialized with the parsed value, or null if parsing
-   * failed. If returned, the Calendar is configured to the GMT time zone.
+   * failed. If returned, the Calendar is configured to the UTC time zone.
    */
-  public static Calendar parseDateFormat(
-      String s,
-      String pattern,
+  public static Calendar parseDateFormat(String s, DateFormat dateFormat,
       TimeZone tz) {
-    assert pattern != null;
     ParsePosition pp = new ParsePosition(0);
-    Calendar ret = parseDateFormat(s, pattern, tz, pp);
+    Calendar ret = parseDateFormat(s, dateFormat, tz, pp);
     if (pp.getIndex() != s.length()) {
       // Didn't consume entire string - not good
       return null;
@@ -161,6 +161,16 @@
     return ret;
   }
 
+  @Deprecated // to be removed before 2.0
+  public static PrecisionTime parsePrecisionDateTimeLiteral(
+      String s,
+      String pattern,
+      TimeZone tz) {
+    assert pattern != null;
+    return parsePrecisionDateTimeLiteral(s,
+        new SimpleDateFormat(pattern, Locale.ROOT), tz, 3);
+  }
+
   /**
    * Parses a string using {@link SimpleDateFormat} and a given pattern, and
    * if present, parses a fractional seconds component. The fractional seconds
@@ -169,20 +179,17 @@
    * seconds precision (to obtain milliseconds).
    *
    * @param s       string to be parsed
-   * @param pattern {@link SimpleDateFormat}  pattern
+   * @param dateFormat Date format
    * @param tz      time zone in which to interpret string. Defaults to the
    *                local time zone
    * @return a {@link DateTimeUtils.PrecisionTime PrecisionTime} initialized
    * with the parsed value, or null if parsing failed. The PrecisionTime
    * contains a GMT Calendar and a precision.
    */
-  public static PrecisionTime parsePrecisionDateTimeLiteral(
-      String s,
-      String pattern,
-      TimeZone tz) {
-    assert pattern != null;
-    ParsePosition pp = new ParsePosition(0);
-    Calendar cal = parseDateFormat(s, pattern, tz, pp);
+  public static PrecisionTime parsePrecisionDateTimeLiteral(String s,
+      DateFormat dateFormat, TimeZone tz, int maxPrecision) {
+    final ParsePosition pp = new ParsePosition(0);
+    final Calendar cal = parseDateFormat(s, dateFormat, tz, pp);
     if (cal == null) {
       return null; // Invalid date/time format
     }
@@ -191,6 +198,7 @@
     // the decimal as milliseconds. That means 12:00:00.9 has 9
     // milliseconds and 12:00:00.9999 has 9999 milliseconds.
     int p = 0;
+    String secFraction = "";
     if (pp.getIndex() < s.length()) {
       // Check to see if rest is decimal portion
       if (s.charAt(pp.getIndex()) != '.') {
@@ -202,7 +210,7 @@
 
       // Parse decimal portion
       if (pp.getIndex() < s.length()) {
-        String secFraction = s.substring(pp.getIndex());
+        secFraction = s.substring(pp.getIndex());
         if (!secFraction.matches("\\d+")) {
           return null;
         }
@@ -215,22 +223,29 @@
 
         // Determine precision - only support prec 3 or lower
         // (milliseconds) Higher precisions are quietly rounded away
-        p = Math.min(
-            3,
-            secFraction.length());
+        p = secFraction.length();
+        if (maxPrecision >= 0) {
+          // If there is a maximum precision, ignore subsequent digits
+          p = Math.min(maxPrecision, p);
+          secFraction = secFraction.substring(0, p);
+        }
 
         // Calculate milliseconds
-        int ms =
-            (int) Math.round(
-                num.longValue()
-                * Math.pow(10, 3 - secFraction.length()));
+        String millis = secFraction;
+        if (millis.length() > 3) {
+          millis = secFraction.substring(0, 3);
+        }
+        while (millis.length() < 3) {
+          millis = millis + "0";
+        }
+
+        int ms = Integer.valueOf(millis);
         cal.add(Calendar.MILLISECOND, ms);
       }
     }
 
     assert pp.getIndex() == s.length();
-    PrecisionTime ret = new PrecisionTime(cal, p);
-    return ret;
+    return new PrecisionTime(cal, secFraction, p);
   }
 
   /**
@@ -1014,10 +1029,12 @@
    */
   public static class PrecisionTime {
     private final Calendar cal;
+    private final String fraction;
     private final int precision;
 
-    public PrecisionTime(Calendar cal, int precision) {
+    public PrecisionTime(Calendar cal, String fraction, int precision) {
       this.cal = cal;
+      this.fraction = fraction;
       this.precision = precision;
     }
 
@@ -1028,6 +1045,10 @@
     public int getPrecision() {
       return precision;
     }
+
+    public String getFraction() {
+      return fraction;
+    }
   }
 }
 
diff --git a/core/src/test/java/org/apache/calcite/avatica/util/DateTimeUtilsTest.java b/core/src/test/java/org/apache/calcite/avatica/util/DateTimeUtilsTest.java
index 1ac1a90..f433884 100644
--- a/core/src/test/java/org/apache/calcite/avatica/util/DateTimeUtilsTest.java
+++ b/core/src/test/java/org/apache/calcite/avatica/util/DateTimeUtilsTest.java
@@ -18,6 +18,10 @@
 
 import org.junit.Test;
 
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Locale;
+
 import static org.apache.calcite.avatica.util.DateTimeUtils.EPOCH_JULIAN;
 import static org.apache.calcite.avatica.util.DateTimeUtils.addMonths;
 import static org.apache.calcite.avatica.util.DateTimeUtils.dateStringToUnixDate;
@@ -44,6 +48,7 @@
 import static org.hamcrest.CoreMatchers.anyOf;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
@@ -521,6 +526,43 @@
     assertThat(unixTimestamp(2016, 2, 29, 0, 0, 0), is(y2016 + day));
     assertThat(unixTimestamp(2016, 3, 1, 0, 0, 0), is(y2016 + day + day));
   }
+
+  @Test public void testParse() {
+    final SimpleDateFormat formatD =
+        new SimpleDateFormat(DateTimeUtils.DATE_FORMAT_STRING, Locale.ROOT);
+    final Calendar c =
+        DateTimeUtils.parseDateFormat("1234-04-12", formatD,
+            DateTimeUtils.UTC_ZONE);
+    assertThat(c, notNullValue());
+    assertThat(c.get(Calendar.YEAR), is(1234));
+    assertThat(c.get(Calendar.MONTH), is(Calendar.APRIL));
+    assertThat(c.get(Calendar.DAY_OF_MONTH), is(12));
+
+    final SimpleDateFormat formatTs =
+        new SimpleDateFormat(DateTimeUtils.TIMESTAMP_FORMAT_STRING,
+            Locale.ROOT);
+    final DateTimeUtils.PrecisionTime pt =
+        DateTimeUtils.parsePrecisionDateTimeLiteral(
+            "1234-04-12 01:23:45.06789", formatTs, DateTimeUtils.UTC_ZONE, -1);
+    assertThat(pt, notNullValue());
+    assertThat(pt.getCalendar().get(Calendar.YEAR), is(1234));
+    assertThat(pt.getCalendar().get(Calendar.MONTH), is(Calendar.APRIL));
+    assertThat(pt.getCalendar().get(Calendar.DAY_OF_MONTH), is(12));
+    assertThat(pt.getCalendar().get(Calendar.HOUR_OF_DAY), is(1));
+    assertThat(pt.getCalendar().get(Calendar.MINUTE), is(23));
+    assertThat(pt.getCalendar().get(Calendar.SECOND), is(45));
+    assertThat(pt.getCalendar().get(Calendar.MILLISECOND), is(67));
+    assertThat(pt.getFraction(), is("06789"));
+    assertThat(pt.getPrecision(), is(5));
+
+    // as above, but limit to 2 fractional digits
+    final DateTimeUtils.PrecisionTime pt2 =
+        DateTimeUtils.parsePrecisionDateTimeLiteral(
+            "1234-04-12 01:23:45.06789", formatTs, DateTimeUtils.UTC_ZONE, 2);
+    assertThat(pt2, notNullValue());
+    assertThat(pt2.getCalendar().get(Calendar.MILLISECOND), is(60));
+    assertThat(pt2.getFraction(), is("06"));
+  }
 }
 
 // End DateTimeUtilsTest.java