From: James Ahlborn Date: Thu, 13 Dec 2018 01:15:30 +0000 (+0000) Subject: round LDT date/times to millis; add some initial tests for LDT times X-Git-Tag: jackcess-3.0.0~7^2~24 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=99af2bc3a62fe0e710bb386365770439bc2984c3;p=jackcess.git round LDT date/times to millis; add some initial tests for LDT times git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1237 f203690c-595d-4dc9-a70b-905162fa7fd2 --- diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java index f5b7d5b..423b19e 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java @@ -65,6 +65,7 @@ import com.healthmarketscience.jackcess.complex.ComplexValue; import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey; import com.healthmarketscience.jackcess.expr.Identifier; import com.healthmarketscience.jackcess.impl.complex.ComplexValueForeignKeyImpl; +import com.healthmarketscience.jackcess.impl.expr.NumberFormatter; import com.healthmarketscience.jackcess.util.ColumnValidator; import com.healthmarketscience.jackcess.util.SimpleColumnValidator; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -95,6 +96,8 @@ public class ColumnImpl implements Column, Comparable { 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; + private static final long NANOS_PER_MILLI = 1_000_000L; + private static final long MILLIS_PER_SECOND = 1000L; /** * Access starts counting dates at Dec 30, 1899 (note, this strange date @@ -980,13 +983,13 @@ public class ColumnImpl implements Column, Comparable { double secondsDouble = (Math.abs(value) % 1.0d) * SECONDS_PER_DAY; long timeSeconds = (long)secondsDouble; - long timeNanos = Math.round((secondsDouble % 1.0d) * NANOS_PER_SECOND); + long timeMillis = (long)(roundToMillis(secondsDouble % 1.0d) * + MILLIS_PER_SECOND); - return Duration.ofSeconds(dateSeconds + timeSeconds, timeNanos); + return Duration.ofSeconds(dateSeconds + timeSeconds, + timeMillis * NANOS_PER_MILLI); } - - /** * Writes a date value. */ @@ -1053,7 +1056,8 @@ public class ColumnImpl implements Column, Comparable { public static double toDateDouble(Object value, TimeZone tz, ZoneId zoneId) { if(value instanceof TemporalAccessor) { - return toDateDouble(toLocalDateTime((Temporal)value, tz, zoneId)); + return toDateDouble( + toLocalDateTime((TemporalAccessor)value, tz, zoneId)); } // seems access stores dates in the local timezone. guess you just @@ -1147,8 +1151,25 @@ public class ColumnImpl implements Column, Comparable { long dateSeconds = dateTimeSeconds - timeSeconds; long timeNanos = time.getNano(); - double timeDouble = ((((double)timeNanos / NANOS_PER_SECOND) + timeSeconds) - / SECONDS_PER_DAY); + // we have a difficult choice to make here between keeping a value which + // most accurately represents the bits saved and rounding to a value that + // would match what the user would expect too see. since we do a double + // to long conversion, we end up in a situation where the value might be + // 19.9999 seconds. access will display this as 20 seconds (access seems + // to only record times to second precision). if we return 19.9999, then + // when the value is written back out it will be exactly the same double + // (good), but will display as 19 seconds (bad because it looks wrong to + // the user). on the flip side, if we round, the value will display + // "correctly" to the user, but if the value is written back out, it will + // be a slightly different double value. this may not be a problem for + // most situations, but may result in incorrect index based lookups. in + // the old date time handling we use DateExt to store the original bits. + // in jdk8, we cannot extend LocalDateTime. for now, we will try + // returning the value rounded to milliseconds (technically still more + // precision than access uses but more likely to round trip to the same + // value). + double timeDouble = ((roundToMillis((double)timeNanos / NANOS_PER_SECOND) + + timeSeconds) / SECONDS_PER_DAY); double dateDouble = ((double)dateSeconds / SECONDS_PER_DAY); @@ -1159,6 +1180,16 @@ public class ColumnImpl implements Column, Comparable { return dateDouble + timeDouble; } + /** + * Rounds the given decimal to milliseconds (3 decimal places) using the + * standard access rounding mode. + */ + private static double roundToMillis(double dbl) { + return ((dbl == 0d) ? dbl : + new BigDecimal(dbl).setScale(3, NumberFormatter.ROUND_MODE) + .doubleValue()); + } + /** * @return an appropriate Date long value for the given object */ diff --git a/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java b/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java new file mode 100644 index 0000000..5dde831 --- /dev/null +++ b/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java @@ -0,0 +1,160 @@ +/* +Copyright (c) 2018 James Ahlborn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.healthmarketscience.jackcess; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.math.BigDecimal; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; +import java.util.TreeSet; +import java.util.UUID; + +import com.healthmarketscience.jackcess.impl.ColumnImpl; +import com.healthmarketscience.jackcess.impl.DatabaseImpl; +import com.healthmarketscience.jackcess.impl.RowIdImpl; +import com.healthmarketscience.jackcess.impl.RowImpl; +import com.healthmarketscience.jackcess.impl.TableImpl; +import com.healthmarketscience.jackcess.util.LinkResolver; +import junit.framework.TestCase; +import static com.healthmarketscience.jackcess.TestUtil.*; +import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; +import static com.healthmarketscience.jackcess.Database.*; + +/** + * + * @author James Ahlborn + */ +public class LocalDateTimeTest extends TestCase +{ + public LocalDateTimeTest(String name) throws Exception { + super(name); + } + + public void testAncientDates() throws Exception + { + ZoneId zoneId = ZoneId.of("America/New_York"); + DateTimeFormatter sdf = DateTimeFormatter.ofPattern("uuuu-MM-dd"); + + List dates = Arrays.asList("1582-10-15", "1582-10-14", + "1492-01-10", "1392-01-10"); + + for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { + Database db = createMem(fileFormat); + db.setZoneId(zoneId); + db.setDateTimeType(DateTimeType.LOCAL_DATE_TIME); + + Table table = new TableBuilder("test") + .addColumn(new ColumnBuilder("name", DataType.TEXT)) + .addColumn(new ColumnBuilder("date", DataType.SHORT_DATE_TIME)) + .toTable(db); + + for(String dateStr : dates) { + LocalDate ld = LocalDate.parse(dateStr, sdf); + table.addRow("row " + dateStr, ld); + } + + List foundDates = new ArrayList(); + for(Row row : table) { + foundDates.add(sdf.format(row.getLocalDateTime("date"))); + } + + assertEquals(dates, foundDates); + + db.close(); + } + + for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.OLD_DATES)) { + Database db = openCopy(testDB); + db.setDateTimeType(DateTimeType.LOCAL_DATE_TIME); + + Table t = db.getTable("Table1"); + + List foundDates = new ArrayList(); + for(Row row : t) { + foundDates.add(sdf.format(row.getLocalDateTime("DateField"))); + } + + assertEquals(dates, foundDates); + + db.close(); + } + + } + + public void testZoneId() throws Exception + { + ZoneId zoneId = ZoneId.of("America/New_York"); + doTestZoneId(zoneId); + + zoneId = ZoneId.of("Australia/Sydney"); + doTestZoneId(zoneId); + } + + private static void doTestZoneId(final ZoneId zoneId) throws Exception + { + final TimeZone tz = TimeZone.getTimeZone(zoneId); + ColumnImpl col = new ColumnImpl(null, null, DataType.SHORT_DATE_TIME, 0, 0, 0) { + @Override + protected TimeZone getTimeZone() { return tz; } + @Override + protected ZoneId getZoneId() { return zoneId; } + }; + + SimpleDateFormat df = new SimpleDateFormat("yyyy.MM.dd"); + df.setTimeZone(tz); + + long startDate = df.parse("2012.01.01").getTime(); + long endDate = df.parse("2013.01.01").getTime(); + + Calendar curCal = Calendar.getInstance(tz); + curCal.setTimeInMillis(startDate); + + DateTimeFormatter sdf = DateTimeFormatter.ofPattern("uuuu.MM.dd HH:mm:ss"); + + while(curCal.getTimeInMillis() < endDate) { + Date curDate = curCal.getTime(); + LocalDateTime curLdt = LocalDateTime.ofInstant( + Instant.ofEpochMilli(curDate.getTime()), zoneId); + LocalDateTime newLdt = ColumnImpl.ldtFromLocalDateDouble( + col.toDateDouble(curDate)); + if(!curLdt.equals(newLdt)) { + System.out.println("FOO " + curLdt + " " + newLdt); + assertEquals(sdf.format(curLdt), sdf.format(newLdt)); + } + curCal.add(Calendar.MINUTE, 30); + } + } + +}