From 58a79025779db0cd9f7d8776e995d962dbf5cd74 Mon Sep 17 00:00:00 2001 From: Jean-Jacques Lafay Date: Fri, 29 Oct 2021 16:38:04 +0200 Subject: [PATCH] Fix loss of precision with BigDateTime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We should ensure as much as possible a correct round-trip with DateTime. Since DateTime has a higher precision (1/10 µs instead of 1 µs), this is the limit, but we can avoid truncating microseconds. --- .../Internal/StreamReadExtensions.cs | 10 +++++----- .../Internal/StreamWriteExtensions.cs | 4 ++-- ...StreamWriteTests.cs => StreamWriteReadTests.cs} | 14 +++++++++----- 3 files changed, 16 insertions(+), 12 deletions(-) rename test/AdoNetCore.AseClient.Tests/Unit/{StreamWriteTests.cs => StreamWriteReadTests.cs} (78%) diff --git a/src/AdoNetCore.AseClient/Internal/StreamReadExtensions.cs b/src/AdoNetCore.AseClient/Internal/StreamReadExtensions.cs index 4ac963dc..59c25888 100644 --- a/src/AdoNetCore.AseClient/Internal/StreamReadExtensions.cs +++ b/src/AdoNetCore.AseClient/Internal/StreamReadExtensions.cs @@ -184,10 +184,10 @@ public static DateTime ReadBigDateTime(this Stream stream) { var usSinceYearZero = stream.ReadLong(); var usSinceEpoch = usSinceYearZero - Constants.Sql.BigDateTime.EpochMicroSeconds; - var msSinceEpoch = usSinceEpoch / 1000; - var timeSinceEpoch = TimeSpan.FromMilliseconds(msSinceEpoch); + const long ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; + var ticksSinceEpoch = usSinceEpoch * ticksPerMicrosecond; - return Constants.Sql.BigDateTime.Epoch + timeSinceEpoch; + return Constants.Sql.BigDateTime.Epoch.AddTicks(ticksSinceEpoch); } public static DateTime ReadShortPartDateTime(this Stream stream) @@ -222,9 +222,9 @@ public static DateTime ReadTime(this Stream stream) var buf = new byte[length]; stream.Read(buf, 1, remainingLength); - + Array.Reverse(buf); - + return new AseDecimal(precision, scale, isPositive, buf); } diff --git a/src/AdoNetCore.AseClient/Internal/StreamWriteExtensions.cs b/src/AdoNetCore.AseClient/Internal/StreamWriteExtensions.cs index 6373b44b..19e37069 100644 --- a/src/AdoNetCore.AseClient/Internal/StreamWriteExtensions.cs +++ b/src/AdoNetCore.AseClient/Internal/StreamWriteExtensions.cs @@ -184,8 +184,8 @@ public static void WriteIntPartDateTime(this Stream stream, DateTime value) public static void WriteBigDateTime(this Stream stream, DateTime value) { var timeSinceEpoch = value - Constants.Sql.BigDateTime.Epoch; - var msSinceEpoch = timeSinceEpoch.Ticks / TimeSpan.TicksPerMillisecond; - var usSinceEpoch = msSinceEpoch * 1000; + const long ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; + var usSinceEpoch = timeSinceEpoch.Ticks / ticksPerMicrosecond; var usSinceYearZero = usSinceEpoch + Constants.Sql.BigDateTime.EpochMicroSeconds; stream.WriteByte(8); // length diff --git a/test/AdoNetCore.AseClient.Tests/Unit/StreamWriteTests.cs b/test/AdoNetCore.AseClient.Tests/Unit/StreamWriteReadTests.cs similarity index 78% rename from test/AdoNetCore.AseClient.Tests/Unit/StreamWriteTests.cs rename to test/AdoNetCore.AseClient.Tests/Unit/StreamWriteReadTests.cs index b9869bb8..93743c86 100644 --- a/test/AdoNetCore.AseClient.Tests/Unit/StreamWriteTests.cs +++ b/test/AdoNetCore.AseClient.Tests/Unit/StreamWriteReadTests.cs @@ -6,20 +6,23 @@ namespace AdoNetCore.AseClient.Tests.Unit { - public class StreamWriteTests + public class StreamWriteReadTests { - [TestCaseSource(nameof(WriteBigDateTime_Succeeds_Cases))] - public void WriteBigDateTime_Succeeds(string _, DateTime value, byte[] expected) + [TestCaseSource(nameof(WriteReadBigDateTime_Succeeds_Cases))] + public void WriteReadBigDateTime_Succeeds(string _, DateTime value, byte[] expected) { using (var ms = new MemoryStream()) { ms.WriteBigDateTime(value); ms.Seek(0, SeekOrigin.Begin); Assert.AreEqual(BitConverter.ToInt64(expected, 0), BitConverter.ToInt64(ms.ToArray(), 1)); + ms.Seek(1, SeekOrigin.Begin); + var readBack = ms.ReadBigDateTime(); + Assert.AreEqual(value, readBack); } } - public static IEnumerable WriteBigDateTime_Succeeds_Cases() + public static IEnumerable WriteReadBigDateTime_Succeeds_Cases() { yield return new TestCaseData("0001_1", new DateTime(0001, 01, 01, 0, 0, 0, 0), new byte[] { 0x00, 0x40, 0xEB, 0xA9, 0xC2, 0x1C, 0x00, 0x00 }); yield return new TestCaseData("0001_2", new DateTime(0001, 01, 01, 0, 0, 0, 1), new byte[] { 0xE8, 0x43, 0xEB, 0xA9, 0xC2, 0x1C, 0x00, 0x00 }); @@ -30,7 +33,8 @@ public static IEnumerable WriteBigDateTime_Succeeds_Cases() yield return new TestCaseData("1753_1", new DateTime(1753, 1, 1, 0, 0, 0, 0, 0), new byte[] { 0x00, 0xA0, 0x7E, 0xDC, 0xB6, 0x88, 0xC4, 0x00 }); yield return new TestCaseData("1900_1", new DateTime(1900, 1, 1, 0, 0, 0, 0, 0), new byte[] { 0x00, 0x60, 0x5A, 0x60, 0xB1, 0x03, 0xD5, 0x00 }); yield return new TestCaseData("1900_2", new DateTime(1900, 1, 1, 23, 59, 59, 999), new byte[] { 0x18, 0xBC, 0x31, 0x7E, 0xC5, 0x03, 0xD5, 0x00 }); - yield return new TestCaseData("1900_3", new DateTime(1900, 01, 01).Add(TimeSpan.FromHours(24).Add(TimeSpan.FromTicks(-1))), new byte[] { 0x18, 0xBC, 0x31, 0x7E, 0xC5, 0x03, 0xD5, 0x00 }); + // .Net ticks are 1/10 us, so we must lose the last digit of number of ticks + yield return new TestCaseData("1900_3", new DateTime(1900, 01, 01).Add(TimeSpan.FromHours(24).Add(TimeSpan.FromTicks(-10))), new byte[] { 0xFF, 0xBF, 0x31, 0x7E, 0xC5, 0x03, 0xD5, 0x00 }); yield return new TestCaseData("9999_1", new DateTime(9999, 1, 1, 0, 0, 0, 0, 0), new byte[] { 0x00, 0x80, 0x76, 0xE9, 0x1F, 0x04, 0x61, 0x04 }); } }