aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java
diff options
context:
space:
mode:
authorJames Ahlborn <jtahlborn@yahoo.com>2018-12-11 02:07:56 +0000
committerJames Ahlborn <jtahlborn@yahoo.com>2018-12-11 02:07:56 +0000
commitabf32c90b101a0c1f76d89e8cdf80cd24e72f6c8 (patch)
treef7815eaf54d4389173c269e62b2ff391caf30613 /src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java
parent6626d6b28f21d550e5bde637ed3b9cea1806ab40 (diff)
downloadjackcess-abf32c90b101a0c1f76d89e8cdf80cd24e72f6c8.tar.gz
jackcess-abf32c90b101a0c1f76d89e8cdf80cd24e72f6c8.zip
initial support for LocalDateTime and Temporal types
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1235 f203690c-595d-4dc9-a70b-905162fa7fd2
Diffstat (limited to 'src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java')
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java356
1 files changed, 304 insertions, 52 deletions
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java
index 8fa1906..f5b7d5b 100644
--- a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java
@@ -32,11 +32,23 @@ import java.nio.charset.Charset;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.SQLException;
+import java.time.DateTimeException;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.temporal.Temporal;
+import java.time.temporal.TemporalAccessor;
+import java.time.temporal.TemporalQueries;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
+import java.util.TimeZone;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -44,6 +56,7 @@ import java.util.regex.Pattern;
import com.healthmarketscience.jackcess.Column;
import com.healthmarketscience.jackcess.ColumnBuilder;
import com.healthmarketscience.jackcess.DataType;
+import com.healthmarketscience.jackcess.DateTimeType;
import com.healthmarketscience.jackcess.InvalidValueException;
import com.healthmarketscience.jackcess.PropertyMap;
import com.healthmarketscience.jackcess.Table;
@@ -79,8 +92,9 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
/**
* Access stores numeric dates in days. Java stores them in milliseconds.
*/
- private static final long MILLISECONDS_PER_DAY =
- (24L * 60L * 60L * 1000L);
+ private static final long MILLISECONDS_PER_DAY = (24L * 60L * 60L * 1000L);
+ private static final long SECONDS_PER_DAY = (24L * 60L * 60L);
+ private static final long NANOS_PER_SECOND = 1_000_000_000L;
/**
* Access starts counting dates at Dec 30, 1899 (note, this strange date
@@ -91,6 +105,16 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
static final long MILLIS_BETWEEN_EPOCH_AND_1900 =
25569L * MILLISECONDS_PER_DAY;
+ static final LocalDate BASE_LD = LocalDate.of(1899, 12, 30);
+ static final LocalTime BASE_LT = LocalTime.of(0, 0);
+ static final LocalDateTime BASE_LDT = LocalDateTime.of(BASE_LD, BASE_LT);
+
+ private static final DateTimeFactory DEF_DATE_TIME_FACTORY =
+ new DefaultDateTimeFactory();
+
+ private static final DateTimeFactory LDT_DATE_TIME_FACTORY =
+ new LDTDateTimeFactory();
+
/**
* mask for the fixed len bit
* @usage _advanced_field_
@@ -455,8 +479,16 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
return getDatabase().getCharset();
}
- protected Calendar getCalendar() {
- return getDatabase().getCalendar();
+ protected TimeZone getTimeZone() {
+ return getDatabase().getTimeZone();
+ }
+
+ protected ZoneId getZoneId() {
+ return getDatabase().getZoneId();
+ }
+
+ protected DateTimeFactory getDateTimeFactory() {
+ return getDatabase().getDateTimeFactory();
}
public boolean isAppendOnly() {
@@ -881,45 +913,43 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
/**
* Decodes a date value.
*/
- private Date readDateValue(ByteBuffer buffer)
- {
- // seems access stores dates in the local timezone. guess you just hope
- // you read it in the same timezone in which it was written!
+ private Object readDateValue(ByteBuffer buffer) {
long dateBits = buffer.getLong();
- long time = fromDateDouble(Double.longBitsToDouble(dateBits));
- return new DateExt(time, dateBits);
+ return getDateTimeFactory().fromDateBits(this, dateBits);
}
/**
* Returns a java long time value converted from an access date double.
* @usage _advanced_method_
*/
- public long fromDateDouble(double value)
- {
- return fromDateDouble(value, getCalendar());
+ public long fromDateDouble(double value) {
+ return fromDateDouble(value, getTimeZone());
}
/**
* Returns a java long time value converted from an access date double.
* @usage _advanced_method_
*/
- public static long fromDateDouble(double value, DatabaseImpl db)
- {
- return fromDateDouble(value, db.getCalendar());
+ public static long fromDateDouble(double value, DatabaseImpl db) {
+ return fromDateDouble(value, db.getTimeZone());
}
/**
* Returns a java long time value converted from an access date double.
* @usage _advanced_method_
*/
- public static long fromDateDouble(double value, Calendar c)
- {
+ @Deprecated
+ public static long fromDateDouble(double value, Calendar c) {
+ // FIXME, remove me
+ return fromDateDouble(value, c.getTimeZone());
+ }
+
+ public static long fromDateDouble(double value, TimeZone tz) {
long localTime = fromLocalDateDouble(value);
- return localTime - getFromLocalTimeZoneOffset(localTime, c);
+ return localTime - getFromLocalTimeZoneOffset(localTime, tz);
}
- static long fromLocalDateDouble(double value)
- {
+ public static long fromLocalDateDouble(double value) {
long datePart = ((long)value) * MILLISECONDS_PER_DAY;
// the fractional part of the double represents the time. it is always
@@ -927,29 +957,49 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
// _not_ the time distance from zero (as one would expect with "normal"
// numbers). therefore, we need to do a little number logic to convert
// the absolute time fraction into a normal distance from zero number.
- long timePart = Math.round((Math.abs(value) % 1.0) *
- (double)MILLISECONDS_PER_DAY);
+ long timePart = Math.round((Math.abs(value) % 1.0d) *
+ MILLISECONDS_PER_DAY);
long time = datePart + timePart;
- time -= MILLIS_BETWEEN_EPOCH_AND_1900;
- return time;
+ return time - MILLIS_BETWEEN_EPOCH_AND_1900;
}
+ public static LocalDateTime ldtFromLocalDateDouble(double value) {
+ Duration dateTimeOffset = durationFromLocalDateDouble1900(value);
+ return BASE_LDT.plus(dateTimeOffset);
+ }
+
+ private static Duration durationFromLocalDateDouble1900(double value) {
+ long dateSeconds = ((long)value) * SECONDS_PER_DAY;
+
+ // the fractional part of the double represents the time. it is always
+ // a positive fraction of the day (even if the double is negative),
+ // _not_ the time distance from zero (as one would expect with "normal"
+ // numbers). therefore, we need to do a little number logic to convert
+ // the absolute time fraction into a normal distance from zero number.
+
+ double secondsDouble = (Math.abs(value) % 1.0d) * SECONDS_PER_DAY;
+ long timeSeconds = (long)secondsDouble;
+ long timeNanos = Math.round((secondsDouble % 1.0d) * NANOS_PER_SECOND);
+
+ return Duration.ofSeconds(dateSeconds + timeSeconds, timeNanos);
+ }
+
+
+
/**
* Writes a date value.
*/
private void writeDateValue(ByteBuffer buffer, Object value)
+ throws InvalidValueException
{
if(value == null) {
buffer.putDouble(0d);
} else if(value instanceof DateExt) {
-
// this is a Date value previously read from readDateValue(). use the
// original bits to store the value so we don't lose any precision
buffer.putLong(((DateExt)value).getDateBits());
-
} else {
-
buffer.putDouble(toDateDouble(value));
}
}
@@ -960,8 +1010,13 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
* @usage _advanced_method_
*/
public double toDateDouble(Object value)
+ throws InvalidValueException
{
- return toDateDouble(value, getCalendar());
+ try {
+ return toDateDouble(value, getTimeZone(), getZoneId());
+ } catch(IllegalArgumentException iae) {
+ throw new InvalidValueException(withErrorContext(iae.getMessage()), iae);
+ }
}
/**
@@ -971,25 +1026,100 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
*/
public static double toDateDouble(Object value, DatabaseImpl db)
{
- return toDateDouble(value, db.getCalendar());
+ return toDateDouble(value, db.getTimeZone(), db.getZoneId());
}
/**
- * Returns an access date double converted from a java Date/Calendar/Number
- * time value.
+ * Returns an access date double converted from a java
+ * Date/Calendar/Number/Temporal time value.
* @usage _advanced_method_
*/
- public static double toDateDouble(Object value, Calendar c)
+ @Deprecated
+ public static double toDateDouble(Object value, Calendar c) {
+ // FIXME remove me
+ return toDateDouble(value, c.getTimeZone());
+ }
+
+ public static double toDateDouble(Object value, TimeZone tz)
{
+ return toDateDouble(value, tz, null);
+ }
+
+ /**
+ * Returns an access date double converted from a java
+ * Date/Calendar/Number/Temporal time value.
+ * @usage _advanced_method_
+ */
+ public static double toDateDouble(Object value, TimeZone tz, ZoneId zoneId)
+ {
+ if(value instanceof TemporalAccessor) {
+ return toDateDouble(toLocalDateTime((Temporal)value, tz, zoneId));
+ }
+
// seems access stores dates in the local timezone. guess you just
// hope you read it in the same timezone in which it was written!
long time = toDateLong(value);
- time += getToLocalTimeZoneOffset(time, c);
+ time += getToLocalTimeZoneOffset(time, tz);
return toLocalDateDouble(time);
}
- static double toLocalDateDouble(long time)
- {
+ private static LocalDateTime toLocalDateTime(
+ TemporalAccessor value, TimeZone tz, ZoneId zoneId) {
+
+ // handle some common Temporal types
+ if(value instanceof LocalDateTime) {
+ return (LocalDateTime)value;
+ }
+ if(value instanceof ZonedDateTime) {
+ // if the temporal value has a timezone, convert it to this db's timezone
+ return ((ZonedDateTime)value).withZoneSameInstant(
+ getZoneId(tz, zoneId)).toLocalDateTime();
+ }
+ if(value instanceof Instant) {
+ return LocalDateTime.ofInstant((Instant)value, getZoneId(tz, zoneId));
+ }
+ if(value instanceof LocalDate) {
+ return ((LocalDate)value).atTime(BASE_LT);
+ }
+ if(value instanceof LocalTime) {
+ return ((LocalTime)value).atDate(BASE_LD);
+ }
+
+ // generic handling for many other Temporal types
+ try {
+
+ LocalDate ld = value.query(TemporalQueries.localDate());
+ if(ld == null) {
+ ld = BASE_LD;
+ }
+ LocalTime lt = value.query(TemporalQueries.localTime());
+ if(lt == null) {
+ lt = BASE_LT;
+ }
+ ZoneId zone = value.query(TemporalQueries.zone());
+ if(zone != null) {
+ // the Temporal has a zone, see if it is the right zone. if not,
+ // adjust it
+ zoneId = getZoneId(tz, zoneId);
+ if(!zoneId.equals(zone)) {
+ return ZonedDateTime.of(ld, lt, zone).withZoneSameInstant(zoneId)
+ .toLocalDateTime();
+ }
+ }
+
+ return LocalDateTime.of(ld, lt);
+
+ } catch(DateTimeException | ArithmeticException e) {
+ throw new IllegalArgumentException(
+ "Unsupported temporal type " + value.getClass(), e);
+ }
+ }
+
+ private static ZoneId getZoneId(TimeZone tz, ZoneId zoneId) {
+ return ((zoneId != null) ? zoneId : tz.toZoneId());
+ }
+
+ static double toLocalDateDouble(long time) {
time += MILLIS_BETWEEN_EPOCH_AND_1900;
if(time < 0L) {
@@ -1003,11 +1133,36 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
return time / (double)MILLISECONDS_PER_DAY;
}
+ public static double toDateDouble(LocalDateTime ldt) {
+ Duration dateTimeOffset = Duration.between(BASE_LDT, ldt);
+ return toLocalDateDouble1900(dateTimeOffset);
+ }
+
+ private static double toLocalDateDouble1900(Duration time) {
+ long dateTimeSeconds = time.getSeconds();
+ long timeSeconds = dateTimeSeconds % SECONDS_PER_DAY;
+ if(timeSeconds < 0) {
+ timeSeconds += SECONDS_PER_DAY;
+ }
+ long dateSeconds = dateTimeSeconds - timeSeconds;
+ long timeNanos = time.getNano();
+
+ double timeDouble = ((((double)timeNanos / NANOS_PER_SECOND) + timeSeconds)
+ / SECONDS_PER_DAY);
+
+ double dateDouble = ((double)dateSeconds / SECONDS_PER_DAY);
+
+ if(dateSeconds < 0) {
+ timeDouble = -timeDouble;
+ }
+
+ return dateDouble + timeDouble;
+ }
+
/**
* @return an appropriate Date long value for the given object
*/
- private static long toDateLong(Object value)
- {
+ private static long toDateLong(Object value) {
return ((value instanceof Date) ?
((Date)value).getTime() :
((value instanceof Calendar) ?
@@ -1019,24 +1174,19 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
* Gets the timezone offset from UTC to local time for the given time
* (including DST).
*/
- private static long getToLocalTimeZoneOffset(long time, Calendar c)
- {
- c.setTimeInMillis(time);
- return ((long)c.get(Calendar.ZONE_OFFSET) + c.get(Calendar.DST_OFFSET));
+ private static long getToLocalTimeZoneOffset(long time, TimeZone tz) {
+ return tz.getOffset(time);
}
/**
* Gets the timezone offset from local time to UTC for the given time
* (including DST).
*/
- private static long getFromLocalTimeZoneOffset(long time, Calendar c)
- {
+ private static long getFromLocalTimeZoneOffset(long time, TimeZone tz) {
// getting from local time back to UTC is a little wonky (and not
- // guaranteed to get you back to where you started)
- c.setTimeInMillis(time);
- // apply the zone offset first to get us closer to the original time
- c.setTimeInMillis(time - c.get(Calendar.ZONE_OFFSET));
- return ((long)c.get(Calendar.ZONE_OFFSET) + c.get(Calendar.DST_OFFSET));
+ // guaranteed to get you back to where you started). apply the zone
+ // offset first to get us closer to the original time
+ return tz.getOffset(time - tz.getRawOffset());
}
/**
@@ -1990,8 +2140,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
return ((value instanceof Double) ? value :
toNumber(value, db).doubleValue());
case SHORT_DATE_TIME:
- return ((value instanceof DateExt) ? value :
- new Date(toDateLong(value)));
+ return db.getDateTimeFactory().toInternalValue(db, value);
case TEXT:
case MEMO:
case GUID:
@@ -2011,6 +2160,11 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
}
}
+ static DateTimeFactory getDateTimeFactory(DateTimeType type) {
+ return ((type == DateTimeType.LOCAL_DATE_TIME) ?
+ LDT_DATE_TIME_FACTORY : DEF_DATE_TIME_FACTORY);
+ }
+
String withErrorContext(String msg) {
return withErrorContext(msg, getDatabase(), getTable().getName(), getName());
}
@@ -2028,8 +2182,10 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
/**
* Date subclass which stashes the original date bits, in case we attempt to
- * re-write the value (will not lose precision).
+ * re-write the value (will not lose precision). Also, this implementation
+ * is immutable.
*/
+ @SuppressWarnings("deprecation")
private static final class DateExt extends Date
{
private static final long serialVersionUID = 0L;
@@ -2046,6 +2202,41 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
return _dateBits;
}
+ @Override
+ public void setDate(int time) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setHours(int time) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setMinutes(int time) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setMonth(int time) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setSeconds(int time) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setYear(int time) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setTime(long time) {
+ throw new UnsupportedOperationException();
+ }
+
private Object writeReplace() throws ObjectStreamException {
// if we are going to serialize this Date, convert it back to a normal
// Date (in case it is restored outside of the context of jackcess)
@@ -2450,4 +2641,65 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
sb.append("allowZeroLength=false");
}
}
+
+ /**
+ * Factory which handles date/time values appropriately for a DateTimeType.
+ */
+ static abstract class DateTimeFactory
+ {
+ public abstract DateTimeType getType();
+
+ public abstract Object fromDateBits(ColumnImpl col, long dateBits);
+
+ public abstract Object toInternalValue(DatabaseImpl db, Object value);
+ }
+
+ /**
+ * Factory impl for legacy Date handling.
+ */
+ static final class DefaultDateTimeFactory extends DateTimeFactory
+ {
+ @Override
+ public DateTimeType getType() {
+ return DateTimeType.DATE;
+ }
+
+ @Override
+ public Object fromDateBits(ColumnImpl col, long dateBits) {
+ long time = col.fromDateDouble(
+ Double.longBitsToDouble(dateBits));
+ return new DateExt(time, dateBits);
+ }
+
+ @Override
+ public Object toInternalValue(DatabaseImpl db, Object value) {
+ return ((value instanceof Date) ? value :
+ new Date(toDateLong(value)));
+ }
+ }
+
+ /**
+ * Factory impl for LocalDateTime handling.
+ */
+ static final class LDTDateTimeFactory extends DateTimeFactory
+ {
+ @Override
+ public DateTimeType getType() {
+ return DateTimeType.LOCAL_DATE_TIME;
+ }
+
+ @Override
+ public Object fromDateBits(ColumnImpl col, long dateBits) {
+ return ldtFromLocalDateDouble(Double.longBitsToDouble(dateBits));
+ }
+
+ @Override
+ public Object toInternalValue(DatabaseImpl db, Object value) {
+ if(value instanceof TemporalAccessor) {
+ return toLocalDateTime((TemporalAccessor)value, null, db.getZoneId());
+ }
+ Instant inst = Instant.ofEpochMilli(toDateLong(value));
+ return LocalDateTime.ofInstant(inst, db.getZoneId());
+ }
+ }
}