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;
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
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.
*/
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
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);
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
*/
--- /dev/null
+/*
+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);
+ }
+ }
+
+}