Unverified 提交 bef4c05e authored 作者: Noel Grandin's avatar Noel Grandin 提交者: GitHub

Merge pull request #890 from katzyn/datetime

Implement conversions for TIMESTAMP WITH TIME ZONE
...@@ -510,6 +510,41 @@ public class DateTimeUtils { ...@@ -510,6 +510,41 @@ public class DateTimeUtils {
return ValueTimestamp.fromDateValueAndNanos(dateValue, nanos); return ValueTimestamp.fromDateValueAndNanos(dateValue, nanos);
} }
/**
* Calculates the time zone offset in minutes for the specified time zone, date
* value, and nanoseconds since midnight.
*
* @param tz
* time zone, or {@code null} for default
* @param dateValue
* date value
* @param timeNanos
* nanoseconds since midnight
* @return time zone offset in milliseconds
*/
public static int getTimeZoneOffsetMillis(TimeZone tz, long dateValue, long timeNanos) {
long msec = timeNanos / 1_000_000;
long utc = convertDateTimeValueToMillis(tz, dateValue, msec);
long local = absoluteDayFromDateValue(dateValue) * MILLIS_PER_DAY + msec;
return (int) (local - utc);
}
/**
* Calculates the milliseconds since epoch for the specified date value,
* nanoseconds since midnight, and time zone offset.
* @param dateValue
* date value
* @param timeNanos
* nanoseconds since midnight
* @param offsetMins
* time zone offset in minutes
* @return milliseconds since epoch in UTC
*/
public static long getMillis(long dateValue, long timeNanos, short offsetMins) {
return absoluteDayFromDateValue(dateValue) * MILLIS_PER_DAY
+ timeNanos / 1_000_000 - offsetMins * 60_000;
}
/** /**
* Calculate the milliseconds since 1970-01-01 (UTC) for the given date and * Calculate the milliseconds since 1970-01-01 (UTC) for the given date and
* time (in the specified timezone). * time (in the specified timezone).
...@@ -971,8 +1006,7 @@ public class DateTimeUtils { ...@@ -971,8 +1006,7 @@ public class DateTimeUtils {
* @return the timestamp * @return the timestamp
*/ */
public static Timestamp convertTimestampTimeZoneToTimestamp(long dateValue, long timeNanos, short offsetMins) { public static Timestamp convertTimestampTimeZoneToTimestamp(long dateValue, long timeNanos, short offsetMins) {
Timestamp ts = new Timestamp(absoluteDayFromDateValue(dateValue) * MILLIS_PER_DAY Timestamp ts = new Timestamp(getMillis(dateValue, timeNanos, offsetMins));
+ timeNanos / 1_000_000 - offsetMins * 60_000);
ts.setNanos((int) (timeNanos % 1_000_000_000)); ts.setNanos((int) (timeNanos % 1_000_000_000));
return ts; return ts;
} }
...@@ -1157,6 +1191,34 @@ public class DateTimeUtils { ...@@ -1157,6 +1191,34 @@ public class DateTimeUtils {
dateValueFromAbsoluteDay(absoluteDay), nanos); dateValueFromAbsoluteDay(absoluteDay), nanos);
} }
/**
* Converts local date value and nanoseconds to timestamp with time zone.
*
* @param dateValue
* date value
* @param timeNanos
* nanoseconds since midnight
* @return timestamp with time zone
*/
public static ValueTimestampTimeZone timestampTimeZoneFromLocalDateValueAndNanos(long dateValue, long timeNanos) {
int timeZoneOffset = DateTimeUtils.getTimeZoneOffsetMillis(null, dateValue, timeNanos);
int offsetMins = timeZoneOffset / 60_000;
int correction = timeZoneOffset % 60_000;
if (correction != 0) {
timeNanos -= correction;
if (timeNanos < 0) {
timeNanos += DateTimeUtils.NANOS_PER_DAY;
dateValue = DateTimeUtils
.dateValueFromAbsoluteDay(DateTimeUtils.absoluteDayFromDateValue(dateValue) - 1);
} else if (timeNanos >= DateTimeUtils.NANOS_PER_DAY) {
timeNanos -= DateTimeUtils.NANOS_PER_DAY;
dateValue = DateTimeUtils
.dateValueFromAbsoluteDay(DateTimeUtils.absoluteDayFromDateValue(dateValue) + 1);
}
}
return ValueTimestampTimeZone.fromDateValueAndNanos(dateValue, timeNanos, (short) offsetMins);
}
/** /**
* Calculate the absolute day from an encoded date value. * Calculate the absolute day from an encoded date value.
* *
......
...@@ -101,9 +101,13 @@ public class LocalDateTimeUtils { ...@@ -101,9 +101,13 @@ public class LocalDateTimeUtils {
private static final Method LOCAL_DATE_AT_START_OF_DAY; private static final Method LOCAL_DATE_AT_START_OF_DAY;
/** /**
* {@code java.sql.Timestamp.from(java.time.Instant)} or {@code null}. * {@code java.time.Instant#getEpochSecond()} or {@code null}.
*/ */
private static final Method TIMESTAMP_FROM; private static final Method INSTANT_GET_EPOCH_SECOND;
/**
* {@code java.time.Instant#getNano()} or {@code null}.
*/
private static final Method INSTANT_GET_NANO;
/** /**
* {@code java.sql.Timestamp.toInstant()} or {@code null}. * {@code java.sql.Timestamp.toInstant()} or {@code null}.
*/ */
...@@ -186,7 +190,8 @@ public class LocalDateTimeUtils { ...@@ -186,7 +190,8 @@ public class LocalDateTimeUtils {
LOCAL_DATE_GET_DAY_OF_MONTH = getMethod(LOCAL_DATE, "getDayOfMonth"); LOCAL_DATE_GET_DAY_OF_MONTH = getMethod(LOCAL_DATE, "getDayOfMonth");
LOCAL_DATE_AT_START_OF_DAY = getMethod(LOCAL_DATE, "atStartOfDay"); LOCAL_DATE_AT_START_OF_DAY = getMethod(LOCAL_DATE, "atStartOfDay");
TIMESTAMP_FROM = getMethod(Timestamp.class, "from", INSTANT); INSTANT_GET_EPOCH_SECOND = getMethod(INSTANT, "getEpochSecond");
INSTANT_GET_NANO = getMethod(INSTANT, "getNano");
TIMESTAMP_TO_INSTANT = getMethod(Timestamp.class, "toInstant"); TIMESTAMP_TO_INSTANT = getMethod(Timestamp.class, "toInstant");
LOCAL_TIME_PARSE = getMethod(LOCAL_TIME, "parse", CharSequence.class); LOCAL_TIME_PARSE = getMethod(LOCAL_TIME, "parse", CharSequence.class);
...@@ -214,7 +219,8 @@ public class LocalDateTimeUtils { ...@@ -214,7 +219,8 @@ public class LocalDateTimeUtils {
LOCAL_DATE_GET_MONTH_VALUE = null; LOCAL_DATE_GET_MONTH_VALUE = null;
LOCAL_DATE_GET_DAY_OF_MONTH = null; LOCAL_DATE_GET_DAY_OF_MONTH = null;
LOCAL_DATE_AT_START_OF_DAY = null; LOCAL_DATE_AT_START_OF_DAY = null;
TIMESTAMP_FROM = null; INSTANT_GET_EPOCH_SECOND = null;
INSTANT_GET_NANO = null;
TIMESTAMP_TO_INSTANT = null; TIMESTAMP_TO_INSTANT = null;
LOCAL_TIME_PARSE = null; LOCAL_TIME_PARSE = null;
LOCAL_DATE_TIME_PLUS_NANOS = null; LOCAL_DATE_TIME_PLUS_NANOS = null;
...@@ -485,7 +491,16 @@ public class LocalDateTimeUtils { ...@@ -485,7 +491,16 @@ public class LocalDateTimeUtils {
*/ */
public static Value instantToValue(Object instant) { public static Value instantToValue(Object instant) {
try { try {
return ValueTimestamp.get((Timestamp) TIMESTAMP_FROM.invoke(null, instant)); long epochSecond = (long) INSTANT_GET_EPOCH_SECOND.invoke(instant);
int nano = (int) INSTANT_GET_NANO.invoke(instant);
long absoluteDay = epochSecond / 86_400;
// Round toward negative infinity
if (epochSecond < 0 && (absoluteDay * 86_400 != epochSecond)) {
absoluteDay--;
}
long timeNanos = (epochSecond - absoluteDay * 86_400) * 1_000_000_000 + nano;
return ValueTimestampTimeZone.fromDateValueAndNanos(
DateTimeUtils.dateValueFromAbsoluteDay(absoluteDay), timeNanos, (short) 0);
} catch (IllegalAccessException e) { } catch (IllegalAccessException e) {
throw DbException.convert(e); throw DbException.convert(e);
} catch (InvocationTargetException e) { } catch (InvocationTargetException e) {
......
...@@ -1020,9 +1020,9 @@ public class DataType { ...@@ -1020,9 +1020,9 @@ public class DataType {
return Value.DATE; return Value.DATE;
} else if (LocalDateTimeUtils.LOCAL_TIME == x) { } else if (LocalDateTimeUtils.LOCAL_TIME == x) {
return Value.TIME; return Value.TIME;
} else if (LocalDateTimeUtils.LOCAL_DATE_TIME == x || LocalDateTimeUtils.INSTANT == x) { } else if (LocalDateTimeUtils.LOCAL_DATE_TIME == x) {
return Value.TIMESTAMP; return Value.TIMESTAMP;
} else if (LocalDateTimeUtils.OFFSET_DATE_TIME == x) { } else if (LocalDateTimeUtils.OFFSET_DATE_TIME == x || LocalDateTimeUtils.INSTANT == x) {
return Value.TIMESTAMP_TZ; return Value.TIMESTAMP_TZ;
} else { } else {
if (JdbcUtils.customDataTypesHandler != null) { if (JdbcUtils.customDataTypesHandler != null) {
......
...@@ -839,9 +839,12 @@ public abstract class Value { ...@@ -839,9 +839,12 @@ public abstract class Value {
case TIMESTAMP: case TIMESTAMP:
return ValueDate.fromDateValue( return ValueDate.fromDateValue(
((ValueTimestamp) this).getDateValue()); ((ValueTimestamp) this).getDateValue());
case TIMESTAMP_TZ: case TIMESTAMP_TZ: {
return ValueDate.fromDateValue( ValueTimestampTimeZone ts = (ValueTimestampTimeZone) this;
((ValueTimestampTimeZone) this).getDateValue()); long dateValue = ts.getDateValue(), timeNanos = ts.getTimeNanos();
long millis = DateTimeUtils.getMillis(dateValue, timeNanos, ts.getTimeZoneOffsetMins());
return ValueDate.fromMillis(millis);
}
case ENUM: case ENUM:
throw DbException.get( throw DbException.get(
ErrorCode.DATA_CONVERSION_ERROR_1, getString()); ErrorCode.DATA_CONVERSION_ERROR_1, getString());
...@@ -857,9 +860,12 @@ public abstract class Value { ...@@ -857,9 +860,12 @@ public abstract class Value {
case TIMESTAMP: case TIMESTAMP:
return ValueTime.fromNanos( return ValueTime.fromNanos(
((ValueTimestamp) this).getTimeNanos()); ((ValueTimestamp) this).getTimeNanos());
case TIMESTAMP_TZ: case TIMESTAMP_TZ: {
return ValueTime.fromNanos( ValueTimestampTimeZone ts = (ValueTimestampTimeZone) this;
((ValueTimestampTimeZone) this).getTimeNanos()); long dateValue = ts.getDateValue(), timeNanos = ts.getTimeNanos();
long millis = DateTimeUtils.getMillis(dateValue, timeNanos, ts.getTimeZoneOffsetMins());
return ValueTime.fromNanos(DateTimeUtils.nanosFromDate(millis) + timeNanos % 1_000_000);
}
case ENUM: case ENUM:
throw DbException.get( throw DbException.get(
ErrorCode.DATA_CONVERSION_ERROR_1, getString()); ErrorCode.DATA_CONVERSION_ERROR_1, getString());
...@@ -874,10 +880,33 @@ public abstract class Value { ...@@ -874,10 +880,33 @@ public abstract class Value {
case DATE: case DATE:
return ValueTimestamp.fromDateValueAndNanos( return ValueTimestamp.fromDateValueAndNanos(
((ValueDate) this).getDateValue(), 0); ((ValueDate) this).getDateValue(), 0);
case TIMESTAMP_TZ: case TIMESTAMP_TZ: {
return ValueTimestamp.fromDateValueAndNanos( ValueTimestampTimeZone ts = (ValueTimestampTimeZone) this;
((ValueTimestampTimeZone) this).getDateValue(), long dateValue = ts.getDateValue(), timeNanos = ts.getTimeNanos();
((ValueTimestampTimeZone) this).getTimeNanos()); long millis = DateTimeUtils.getMillis(dateValue, timeNanos, ts.getTimeZoneOffsetMins());
return ValueTimestamp.fromMillisNanos(millis, (int) (timeNanos % 1_000_000));
}
case ENUM:
throw DbException.get(
ErrorCode.DATA_CONVERSION_ERROR_1, getString());
}
break;
}
case TIMESTAMP_TZ: {
switch (getType()) {
case TIME: {
ValueTimestamp ts = DateTimeUtils.normalizeTimestamp(0, ((ValueTime) this).getNanos());
return DateTimeUtils.timestampTimeZoneFromLocalDateValueAndNanos(
ts.getDateValue(), ts.getTimeNanos());
}
case DATE:
return DateTimeUtils.timestampTimeZoneFromLocalDateValueAndNanos(
((ValueDate) this).getDateValue(), 0);
case TIMESTAMP: {
ValueTimestamp ts = (ValueTimestamp) this;
return DateTimeUtils.timestampTimeZoneFromLocalDateValueAndNanos(
ts.getDateValue(), ts.getTimeNanos());
}
case ENUM: case ENUM:
throw DbException.get( throw DbException.get(
ErrorCode.DATA_CONVERSION_ERROR_1, getString()); ErrorCode.DATA_CONVERSION_ERROR_1, getString());
......
...@@ -148,7 +148,7 @@ public class ValueTimestampTimeZone extends Value { ...@@ -148,7 +148,7 @@ public class ValueTimestampTimeZone extends Value {
@Override @Override
public Timestamp getTimestamp() { public Timestamp getTimestamp() {
throw new UnsupportedOperationException("unimplemented"); return DateTimeUtils.convertTimestampTimeZoneToTimestamp(dateValue, timeNanos, timeZoneOffsetMins);
} }
@Override @Override
......
...@@ -9,6 +9,7 @@ import java.io.ByteArrayInputStream; ...@@ -9,6 +9,7 @@ import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.StringReader; import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.BigInteger; import java.math.BigInteger;
...@@ -755,18 +756,26 @@ public class TestPreparedStatement extends TestBase { ...@@ -755,18 +756,26 @@ public class TestPreparedStatement extends TestBase {
if (!LocalDateTimeUtils.isJava8DateApiPresent()) { if (!LocalDateTimeUtils.isJava8DateApiPresent()) {
return; return;
} }
Method timestampToInstant = Timestamp.class.getMethod("toInstant"), Method timestampToInstant = Timestamp.class.getMethod("toInstant");
now = LocalDateTimeUtils.INSTANT.getMethod("now"); Method now = LocalDateTimeUtils.INSTANT.getMethod("now");
Method parse = LocalDateTimeUtils.INSTANT.getMethod("parse", CharSequence.class);
PreparedStatement prep = conn.prepareStatement("SELECT ?"); PreparedStatement prep = conn.prepareStatement("SELECT ?");
Object instant1 = now.invoke(null);
prep.setObject(1, instant1); testInstant8Impl(prep, timestampToInstant, now.invoke(null));
testInstant8Impl(prep, timestampToInstant, parse.invoke(null, "2000-01-15T12:13:14.123456789Z"));
testInstant8Impl(prep, timestampToInstant, parse.invoke(null, "1500-09-10T23:22:11.123456789Z"));
}
private void testInstant8Impl(PreparedStatement prep, Method timestampToInstant, Object instant)
throws SQLException, IllegalAccessException, InvocationTargetException {
prep.setObject(1, instant);
ResultSet rs = prep.executeQuery(); ResultSet rs = prep.executeQuery();
rs.next(); rs.next();
Object instant2 = rs.getObject(1, LocalDateTimeUtils.INSTANT); Object instant2 = rs.getObject(1, LocalDateTimeUtils.INSTANT);
assertEquals(instant1, instant2); assertEquals(instant, instant2);
Timestamp ts = rs.getTimestamp(1); Timestamp ts = rs.getTimestamp(1);
assertEquals(instant1, timestampToInstant.invoke(ts)); assertEquals(instant, timestampToInstant.invoke(ts));
assertFalse(rs.next()); assertFalse(rs.next());
rs.close(); rs.close();
...@@ -774,7 +783,7 @@ public class TestPreparedStatement extends TestBase { ...@@ -774,7 +783,7 @@ public class TestPreparedStatement extends TestBase {
rs = prep.executeQuery(); rs = prep.executeQuery();
rs.next(); rs.next();
instant2 = rs.getObject(1, LocalDateTimeUtils.INSTANT); instant2 = rs.getObject(1, LocalDateTimeUtils.INSTANT);
assertEquals(instant1, instant2); assertEquals(instant, instant2);
assertFalse(rs.next()); assertFalse(rs.next());
rs.close(); rs.close();
} }
......
...@@ -11,11 +11,16 @@ import java.sql.ResultSet; ...@@ -11,11 +11,16 @@ import java.sql.ResultSet;
import java.sql.ResultSetMetaData; import java.sql.ResultSetMetaData;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement; import java.sql.Statement;
import java.util.TimeZone;
import org.h2.api.TimestampWithTimeZone; import org.h2.api.TimestampWithTimeZone;
import org.h2.test.TestBase; import org.h2.test.TestBase;
import org.h2.util.DateTimeUtils;
import org.h2.util.LocalDateTimeUtils; import org.h2.util.LocalDateTimeUtils;
import org.h2.value.Value; import org.h2.value.Value;
import org.h2.value.ValueDate;
import org.h2.value.ValueTime;
import org.h2.value.ValueTimestamp;
import org.h2.value.ValueTimestampTimeZone; import org.h2.value.ValueTimestampTimeZone;
/** /**
...@@ -40,6 +45,7 @@ public class TestTimeStampWithTimeZone extends TestBase { ...@@ -40,6 +45,7 @@ public class TestTimeStampWithTimeZone extends TestBase {
test4(); test4();
test5(); test5();
testOrder(); testOrder();
testConversions();
deleteDb(getTestName()); deleteDb(getTestName());
} }
...@@ -186,4 +192,39 @@ public class TestTimeStampWithTimeZone extends TestBase { ...@@ -186,4 +192,39 @@ public class TestTimeStampWithTimeZone extends TestBase {
conn.close(); conn.close();
} }
private void testConversionsImpl(String timeStr, boolean testReverse) {
ValueTimestamp ts = ValueTimestamp.parse(timeStr);
ValueDate d = (ValueDate) ts.convertTo(Value.DATE);
ValueTime t = (ValueTime) ts.convertTo(Value.TIME);
ValueTimestampTimeZone tstz = ValueTimestampTimeZone.parse(timeStr);
assertEquals(ts, tstz.convertTo(Value.TIMESTAMP));
assertEquals(d, tstz.convertTo(Value.DATE));
assertEquals(t, tstz.convertTo(Value.TIME));
assertEquals(ts.getTimestamp(), tstz.getTimestamp());
if (testReverse) {
assertEquals(0, tstz.compareTo(ts.convertTo(Value.TIMESTAMP_TZ), null));
assertEquals(d.convertTo(Value.TIMESTAMP).convertTo(Value.TIMESTAMP_TZ),
d.convertTo(Value.TIMESTAMP_TZ));
assertEquals(t.convertTo(Value.TIMESTAMP).convertTo(Value.TIMESTAMP_TZ),
t.convertTo(Value.TIMESTAMP_TZ));
}
}
private void testConversions() {
TimeZone current = TimeZone.getDefault();
try {
for (String id : TimeZone.getAvailableIDs()) {
TimeZone.setDefault(TimeZone.getTimeZone(id));
DateTimeUtils.resetCalendar();
testConversionsImpl("2017-12-05 23:59:30.987654321-12:00", true);
testConversionsImpl("2000-01-02 10:20:30.123456789+07:30", true);
boolean testReverse = !"Africa/Monrovia".equals(id);
testConversionsImpl("1960-04-06 12:13:14.777666555+12:00", testReverse);
}
} finally {
TimeZone.setDefault(current);
DateTimeUtils.resetCalendar();
}
}
} }
...@@ -765,4 +765,4 @@ jacoco xdata invokes sourcefiles classfiles duplication crypto stacktraces prt d ...@@ -765,4 +765,4 @@ jacoco xdata invokes sourcefiles classfiles duplication crypto stacktraces prt d
interpolated thead interpolated thead
die weekdiff osx subprocess dow proleptic microsecond microseconds divisible cmp denormalized suppressed saturated mcs die weekdiff osx subprocess dow proleptic microsecond microseconds divisible cmp denormalized suppressed saturated mcs
london dfs weekdays intermittent looked london dfs weekdays intermittent looked msec tstz africa monrovia
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论