diff options
Diffstat (limited to 'src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java')
-rw-r--r-- | src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java | 356 |
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()); + } + } } |