提交 e0af493c authored 作者: Noel Grandin's avatar Noel Grandin

fix timezone conversion in 'TIMESTAMP WITH TIMEZONE' datatype

上级 5a038869
......@@ -5,31 +5,65 @@
*/
package org.h2.api;
import java.sql.Timestamp;
import java.io.Serializable;
import org.h2.util.DateTimeUtils;
import org.h2.util.StringUtils;
/**
* Extends java.sql.Timestamp to add our time zone information.
* How we expose "DATETIME WITH TIMEZONE" in our ResultSets.
*/
public class TimestampWithTimeZone extends Timestamp {
public class TimestampWithTimeZone implements Serializable, Cloneable {
/**
* The serial version UID.
*/
private static final long serialVersionUID = 4413229090646777107L;
/**
* A bit field with bits for the year, month, and day (see DateTimeUtils for
* encoding)
*/
private final long dateValue;
/**
* The nanoseconds since midnight.
*/
private final long timeNanos;
/**
* Time zone offset from UTC in minutes, range of -12hours to +12hours
*/
private final short timeZoneOffsetMins;
public TimestampWithTimeZone(long timeMillis, int nanos, short timeZoneOffsetMins) {
super(timeMillis);
setNanos(nanos);
public TimestampWithTimeZone(long dateValue, long timeNanos, short timeZoneOffsetMins) {
this.dateValue = dateValue;
this.timeNanos = timeNanos;
this.timeZoneOffsetMins = timeZoneOffsetMins;
}
/**
* The timezone offset in minutes.
* @return the year-month-day bit field
*/
public long getYMD() {
return dateValue;
}
public long getYear() {
return DateTimeUtils.yearFromDateValue(dateValue);
}
public long getMonth() {
return DateTimeUtils.monthFromDateValue(dateValue);
}
public long getDay() {
return DateTimeUtils.dayFromDateValue(dateValue);
}
public long getNanosSinceMidnight() {
return timeNanos;
}
/**
* The time zone offset in minutes.
*
* @return the offset
*/
......@@ -37,6 +71,66 @@ public class TimestampWithTimeZone extends Timestamp {
return timeZoneOffsetMins;
}
@Override
public String toString() {
StringBuilder buff = new StringBuilder();
int y = DateTimeUtils.yearFromDateValue(dateValue);
int month = DateTimeUtils.monthFromDateValue(dateValue);
int d = DateTimeUtils.dayFromDateValue(dateValue);
if (y > 0 && y < 10000) {
StringUtils.appendZeroPadded(buff, 4, y);
} else {
buff.append(y);
}
buff.append('-');
StringUtils.appendZeroPadded(buff, 2, month);
buff.append('-');
StringUtils.appendZeroPadded(buff, 2, d);
buff.append(' ');
long nanos = timeNanos;
long ms = nanos / 1000000;
nanos -= ms * 1000000;
long s = ms / 1000;
ms -= s * 1000;
long min = s / 60;
s -= min * 60;
long h = min / 60;
min -= h * 60;
StringUtils.appendZeroPadded(buff, 2, h);
buff.append(':');
StringUtils.appendZeroPadded(buff, 2, min);
buff.append(':');
StringUtils.appendZeroPadded(buff, 2, s);
buff.append('.');
int start = buff.length();
StringUtils.appendZeroPadded(buff, 3, ms);
if (nanos > 0) {
StringUtils.appendZeroPadded(buff, 6, nanos);
}
for (int i = buff.length() - 1; i > start; i--) {
if (buff.charAt(i) != '0') {
break;
}
buff.deleteCharAt(i);
}
short tz = timeZoneOffsetMins;
if (tz < 0) {
buff.append('-');
tz = (short) -tz;
} else {
buff.append('+');
}
int hours = tz / 60;
tz -= hours * 60;
int mins = tz;
StringUtils.appendZeroPadded(buff, 2, hours);
if (mins != 0) {
buff.append(':');
StringUtils.appendZeroPadded(buff, 2, mins);
}
return buff.toString();
}
@Override
public int hashCode() {
return 31 * super.hashCode() + timeZoneOffsetMins;
......@@ -47,13 +141,16 @@ public class TimestampWithTimeZone extends Timestamp {
if (this == obj) {
return true;
}
if (!super.equals(obj)) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
TimestampWithTimeZone other = (TimestampWithTimeZone) obj;
if (dateValue != other.dateValue) {
return false;
}
if (timeNanos != other.timeNanos) {
return false;
}
if (timeZoneOffsetMins != other.timeZoneOffsetMins) {
return false;
}
......
......@@ -20,6 +20,7 @@ import org.h2.message.DbException;
import org.h2.result.Row;
import org.h2.schema.Schema;
import org.h2.schema.Sequence;
import org.h2.util.DateTimeUtils;
import org.h2.util.MathUtils;
import org.h2.util.StringUtils;
import org.h2.value.DataType;
......@@ -298,8 +299,10 @@ public class Column {
} else if (dt.type == Value.TIMESTAMP_UTC) {
value = ValueTimestampUtc.fromMillis(session.getTransactionStart());
} else if (dt.type == Value.TIMESTAMP_TZ) {
value = ValueTimestampTimeZone.fromMillis(
session.getTransactionStart(), (short) 0);
long ms = session.getTransactionStart();
value = ValueTimestampTimeZone.fromDateValueAndNanos(
DateTimeUtils.dateValueFromDate(ms),
DateTimeUtils.nanosFromDate(ms), (short) 0);
} else if (dt.type == Value.TIME) {
value = ValueTime.fromNanos(0);
} else if (dt.type == Value.DATE) {
......
......@@ -15,7 +15,6 @@ import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
import org.h2.api.ErrorCode;
import org.h2.message.DbException;
import org.h2.value.Value;
......@@ -683,6 +682,18 @@ public class DateTimeUtils {
return new Date(millis);
}
/**
* Convert a date value to millis, using the supplied timezone.
*
* @param dateValue the date value
* @return the date
*/
public static long convertDateValueToMillis(TimeZone tz, long dateValue) {
return getMillis(tz,
yearFromDateValue(dateValue),
monthFromDateValue(dateValue),
dayFromDateValue(dateValue), 0, 0, 0, 0);
}
/**
* Convert a date value / time value to a timestamp, using the default
* timezone.
......
......@@ -561,7 +561,7 @@ public class DataType {
break;
}
case Value.TIMESTAMP_TZ: {
TimestampWithTimeZone value = (TimestampWithTimeZone) rs.getTimestamp(columnIndex);
TimestampWithTimeZone value = (TimestampWithTimeZone) rs.getObject(columnIndex);
v = value == null ? (Value) ValueNull.INSTANCE :
ValueTimestampTimeZone.get(value);
break;
......
/*
* Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (http://h2database.com/html/license.html).
* Initial Developer: H2 Group
* Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, and the
* EPL 1.0 (http://h2database.com/html/license.html). Initial Developer: H2
* Group
*/
package org.h2.value;
......@@ -25,14 +25,16 @@ import org.h2.util.StringUtils;
*/
public class ValueTimestampTimeZone extends Value {
private static final TimeZone GMT_TIMEZONE = TimeZone.getTimeZone("GMT");
/**
* The precision in digits.
*/
public static final int PRECISION = 30;
/**
* The display size of the textual representation of a timestamp.
* Example: 2001-01-01 23:59:59.000 +10:00
* The display size of the textual representation of a timestamp. Example:
* 2001-01-01 23:59:59.000 +10:00
*/
static final int DISPLAY_SIZE = 30;
......@@ -58,10 +60,11 @@ public class ValueTimestampTimeZone extends Value {
private ValueTimestampTimeZone(long dateValue, long timeNanos,
short timeZoneOffsetMins) {
if (timeNanos < 0 || timeNanos >= 24L * 60 * 60 * 1000 * 1000 * 1000) {
throw new IllegalArgumentException("timeNanos out of range " +
timeNanos);
throw new IllegalArgumentException(
"timeNanos out of range " + timeNanos);
}
if (timeZoneOffsetMins < (-12 * 60) || timeZoneOffsetMins >= (12 * 60)) {
if (timeZoneOffsetMins < (-12 * 60)
|| timeZoneOffsetMins >= (12 * 60)) {
throw new IllegalArgumentException(
"timeZoneOffsetMins out of range " + timeZoneOffsetMins);
}
......@@ -92,43 +95,11 @@ public class ValueTimestampTimeZone extends Value {
* @return the value
*/
public static ValueTimestampTimeZone get(TimestampWithTimeZone timestamp) {
long ms = timestamp.getTime();
long nanos = timestamp.getNanos() % 1000000;
long dateValue = DateTimeUtils.dateValueFromDate(ms);
nanos += DateTimeUtils.nanosFromDate(ms);
return fromDateValueAndNanos(dateValue, nanos,
return fromDateValueAndNanos(timestamp.getYMD(),
timestamp.getNanosSinceMidnight(),
timestamp.getTimeZoneOffsetMins());
}
/**
* Get or create a timestamp value for the given date/time in millis.
*
* @param ms the milliseconds
* @param nanos the nanoseconds
* @param timeZoneOffsetMins the timezone offset in minutes
* @return the value
*/
public static ValueTimestampTimeZone fromMillisNanos(long ms, int nanos,
short timeZoneOffsetMins) {
long dateValue = DateTimeUtils.dateValueFromDate(ms);
long timeNanos = nanos + DateTimeUtils.nanosFromDate(ms);
return fromDateValueAndNanos(dateValue, timeNanos, timeZoneOffsetMins);
}
/**
* Get or create a timestamp value for the given date/time in millis.
*
* @param ms the milliseconds
* @param timeZoneOffsetMins the timezone offset in minutes
* @return the value
*/
public static ValueTimestampTimeZone fromMillis(long ms,
short timeZoneOffsetMins) {
long dateValue = DateTimeUtils.dateValueFromDate(ms);
long nanos = DateTimeUtils.nanosFromDate(ms);
return fromDateValueAndNanos(dateValue, nanos, timeZoneOffsetMins);
}
/**
* Parse a string to a ValueTimestamp. This method supports the format
* +/-year-month-day hour:minute:seconds.fractional and an optional timezone
......@@ -141,8 +112,8 @@ public class ValueTimestampTimeZone extends Value {
try {
return parseTry(s);
} catch (Exception e) {
throw DbException.get(ErrorCode.INVALID_DATETIME_CONSTANT_2,
e, "TIMESTAMP WITH TIMEZONE", s);
throw DbException.get(ErrorCode.INVALID_DATETIME_CONSTANT_2, e,
"TIMESTAMP WITH TIMEZONE", s);
}
}
......@@ -194,7 +165,8 @@ public class ValueTimestampTimeZone extends Value {
}
}
if (tz != null) {
long millis = DateTimeUtils.convertDateValueToDate(dateValue).getTime();
long millis = DateTimeUtils
.convertDateValueToMillis(GMT_TIMEZONE, dateValue);
tzMinutes = (short) (tz.getOffset(millis) / 1000 / 60);
}
}
......@@ -234,10 +206,7 @@ public class ValueTimestampTimeZone extends Value {
@Override
public Timestamp getTimestamp() {
Timestamp ts = DateTimeUtils.convertDateValueToTimestamp(dateValue,
timeNanos);
return new TimestampWithTimeZone(ts.getTime(), ts.getNanos(),
getTimeZoneOffsetMins());
throw new UnsupportedOperationException("unimplemented");
}
@Override
......@@ -340,19 +309,20 @@ public class ValueTimestampTimeZone extends Value {
return false;
}
ValueTimestampTimeZone x = (ValueTimestampTimeZone) other;
return dateValue == x.dateValue && timeNanos == x.timeNanos &&
timeZoneOffsetMins == x.timeZoneOffsetMins;
return dateValue == x.dateValue && timeNanos == x.timeNanos
&& timeZoneOffsetMins == x.timeZoneOffsetMins;
}
@Override
public int hashCode() {
return (int) (dateValue ^ (dateValue >>> 32) ^ timeNanos ^
(timeNanos >>> 32) ^ timeZoneOffsetMins);
return (int) (dateValue ^ (dateValue >>> 32) ^ timeNanos
^ (timeNanos >>> 32) ^ timeZoneOffsetMins);
}
@Override
public Object getObject() {
return getTimestamp();
return new TimestampWithTimeZone(dateValue, timeNanos,
timeZoneOffsetMins);
}
@Override
......@@ -363,15 +333,13 @@ public class ValueTimestampTimeZone extends Value {
@Override
public Value add(Value v) {
throw DbException
.getUnsupportedException(
throw DbException.getUnsupportedException(
"manipulating TIMESTAMP WITH TIMEZONE values is unsupported");
}
@Override
public Value subtract(Value v) {
throw DbException
.getUnsupportedException(
throw DbException.getUnsupportedException(
"manipulating TIMESTAMP WITH TIMEZONE values is unsupported");
}
......
......@@ -679,6 +679,23 @@ public abstract class TestBase {
}
}
/**
* Check if two values are equal, and if not throw an exception.
*
* @param expected the expected value
* @param actual the actual value
* @throws AssertionError if the values are not equal
*/
public void assertEquals(Object expected, Object actual) {
if (expected == null || actual == null) {
assertTrue(expected == actual);
return;
}
if (!expected.equals(actual)) {
fail(" expected: " + expected + " actual: " + actual);
}
}
/**
* Check if two readers are equal, and if not throw an exception.
*
......
......@@ -5,11 +5,10 @@
*/
package org.h2.test.jaqu;
import static java.sql.Date.valueOf;
import org.h2.jaqu.Db;
import org.h2.test.TestBase;
import static java.sql.Date.valueOf;
/**
* Tests the Db.update() function.
*
......@@ -58,7 +57,7 @@ public class UpdateTest extends TestBase {
Product p2 = new Product();
Product pChang2 = db.from(p2).where(p2.productName).is("Chang")
.selectFirst();
assertEquals(19.5, pChang2.unitPrice);
assertEquals((Double)19.5, pChang2.unitPrice);
assertEquals(16, pChang2.unitsInStock.intValue());
// undo update
......@@ -96,7 +95,7 @@ public class UpdateTest extends TestBase {
Product p2 = new Product();
Product pChang2 = db.from(p2).where(p2.productName).is("Chang")
.selectFirst();
assertEquals(19.5, pChang2.unitPrice);
assertEquals((Double)19.5, pChang2.unitPrice);
assertEquals(16, pChang2.unitsInStock.intValue());
// undo update
......@@ -137,7 +136,7 @@ public class UpdateTest extends TestBase {
// confirm the data was properly updated
Product revised = db.from(p).where(p.productId).is(1).selectFirst();
assertEquals("updated", revised.productName);
assertEquals(original.unitPrice + 3.14, revised.unitPrice);
assertEquals((Double)(original.unitPrice + 3.14), revised.unitPrice);
assertEquals(original.unitsInStock + 2, revised.unitsInStock.intValue());
// restore the data
......
......@@ -15,7 +15,6 @@ import java.util.Map;
import java.util.Random;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.h2.mvstore.DataUtils;
import org.h2.mvstore.MVMap;
import org.h2.mvstore.MVStore;
......@@ -68,7 +67,7 @@ public class TestStreamStore extends TestBase {
if (max == -1) {
assertTrue(map.isEmpty());
} else {
assertEquals(map.lastKey(), max);
assertEquals(map.lastKey(), (Long)max);
}
}
}
......
......@@ -9,8 +9,7 @@ import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import org.h2.api.TimestampWithTimeZone;
import org.h2.test.TestBase;
/**
......@@ -41,11 +40,8 @@ public class TestTimeStampWithTimeZone extends TestBase {
ResultSet rs = stat.executeQuery("select t1 from test");
rs.next();
assertEquals("1970-01-01 12:00:00.0+00:15", rs.getString(1));
Timestamp ts = rs.getTimestamp(1);
// TODO currently fails:
//assertTrue("" + ts,
// new TimestampWithTimeZone(36000000, 00, (short) 15).equals(
// ts));
TimestampWithTimeZone ts = (TimestampWithTimeZone) rs.getObject(1);
assertEquals(new TimestampWithTimeZone(1008673L, 43200000000000L, (short) 15), ts);
conn.close();
}
......
......@@ -167,7 +167,9 @@ public class TestValueMemory extends TestBase implements DataHandler {
case Value.TIMESTAMP_UTC:
return ValueTimestampUtc.fromMillis(random.nextLong());
case Value.TIMESTAMP_TZ:
return ValueTimestampTimeZone.fromMillis(random.nextLong(), (short) 0);
return ValueTimestampTimeZone.fromDateValueAndNanos(
random.nextLong(), random.nextLong(),
(short) random.nextInt());
case Value.BYTES:
return ValueBytes.get(randomBytes(random.nextInt(1000)));
case Value.STRING:
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论