]> source.dussan.org Git - jackcess.git/commitdiff
round LDT date/times to millis; add some initial tests for LDT times
authorJames Ahlborn <jtahlborn@yahoo.com>
Thu, 13 Dec 2018 01:15:30 +0000 (01:15 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Thu, 13 Dec 2018 01:15:30 +0000 (01:15 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1237 f203690c-595d-4dc9-a70b-905162fa7fd2

src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java
src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java [new file with mode: 0644]

index f5b7d5b1e590a59395cad17f8d6971db141a9eba..423b19eb87660da9c0dfe67055eda4f7ced65144 100644 (file)
@@ -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<ColumnImpl> {
   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<ColumnImpl> {
 
     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<ColumnImpl> {
   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<ColumnImpl> {
     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<ColumnImpl> {
     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 (file)
index 0000000..5dde831
--- /dev/null
@@ -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<String> 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<String> foundDates = new ArrayList<String>();
+      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<String> foundDates = new ArrayList<String>();
+      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);
+    }
+  }
+
+}