[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