View Ticket
Not logged in
Ticket Hash: 69cf6e5dc86ba3bcc8b670baf48a9e87c49215f7
Title: Round trip of DateTime is sometimes 1 ms off with JulianDay Format
Status: Closed Type: Code_Defect
Severity: Minor Priority: Medium
Subsystem: Convert Resolution: Fixed
Last Modified: 2016-12-14 19:35:40
Version Found In:
User Comments:
anonymous added on 2016-11-23 22:37:45:
Assume a SQLiteConnection with a connection string of "data source=test.sqlite;datetimeformat=JulianDay;datetimekind=Utc" on Win 10 and .NET FW 4.

Inserting a row to a table with a DateTime-Parameter for a column writes this DateTime as JulianDay-Double into the column. 
Reading this column of that row back may result in a DateTime value which is sometimes 1ms less then the original value. An example DateTime is 2016-11-20 00:46:03.000 (or any other date with the same time of day).

The reason:
1) Not every (finite decimal) time of day fraction can be stored on a (binary finite) floating point value. In such cases some of the available (binary finite) neighbours are used to approximate the time of day fraction.
2) DateTime values are stored and *recreated* only with millisecond precision in SQLiteConvert

Here the code-example from a C# console project:

        public static void copyPaste()
            using (var lConn = new SQLiteConnection("data source = test.sqlite;datetimeformat = JulianDay;datetimekind = Utc")) {
                using (var lCreateCmd = lConn.CreateCommand())
                    lCreateCmd.CommandText = @"CREATE TABLE [Test] (dt)";
                using (var lInsertCmd = lConn.CreateCommand())
                    lInsertCmd.CommandText = @"INSERT INTO [Test](dt) VALUES(@dt)";
                    var dt = lInsertCmd.CreateParameter();
                    dt.ParameterName = "dt";
                    dt.Value = new DateTime(2016, 11, 20, 00, 46, 03, 000, DateTimeKind.Utc);
                using (var lSelectCmd = lConn.CreateCommand())
                    lSelectCmd.CommandText = @"SELECT dt FROM [Test]";
                    using (var lReader = lSelectCmd.ExecuteReader())
                        while (lReader.Read())
                            Console.WriteLine("Value: {0:yyyy-MM-dd HH:mm:ss.ffff}", lReader.GetDateTime(0));

As long as the Julian Day Number is not too large, the JulianDay-Double can store values to a precision of at least 1/10 msec (100usec). Therefore it is possible to restore the original DateTime value with millisecond precision using the sub-ms fraction of the JulianDay-Double.

The proposed solution System.Data.SQLite.SQLiteConvert.cs (row 259):
        private static long DoubleToJd(
            double julianDay
            return (long)Math.Round(julianDay * 86400000.0);

mistachkin added on 2016-11-27 01:00:46:
So, using Math.Round will result in the desired behavior?  I'll need to spend
some time understanding the corner cases impacted by this change.

anonymous added on 2016-11-27 19:52:32:
As long as SQLiteConvert only store a DateTime value with millisecond precision (with the julian day conversion, see SQLiteConvert.computeJD) the conversion back to a DateTime should yield the original value. 

The following methods in SQLiteConvert are of interest for this case:
long DoubleToJd(double julianDay)
double JdToDouble(long jd)
long computeJD(DateTime dateTime)

C# Interactive
> // Julian-Day-Number of 2016-11-27 * 86400000 + 00:46:03.000 in msec since 00:00
> long sjd = 212347010763000;
> long ljd = (long)((sjd / 86400000.0) * 86400000.0);
> Console.WriteLine(ljd)

mistachkin added on 2016-12-05 22:42:23:
I was able to reproduce the issue locally.

mistachkin added on 2016-12-06 01:51:00:
Fixed on branch via check-in [471c16c6171c9fee].

mistachkin added on 2016-12-14 19:35:40:
Fix merged to trunk via check-in [f154173793f81c6f].