首页 > 解决方案 > 通过 double 往返 DateTime 而不会损失精度

问题描述

这个问题不是关于将 DateTime 序列化为 double 和 back 是否明智,而是关于当这是你必须做的事情时该怎么做。

表面上的解决方案是使用DateTime.ToOADate(), 根据Convert DateTime to Double但这会严重损失精度,例如

let now = DateTime.UtcNow in DateTime.FromOADate(now.ToOADate()).Ticks - now.Ticks

结果类似于 val it : int64 = -7307L,这非常糟糕,因为这几乎是一毫秒。

在这方面,一种更粗略的方法(只是在和之间进行转换long(在 F#double中调用float)实际上要好一些:

let now = DateTime.UtcNow in DateTime(int64(float(now.Ticks))).Ticks - now.Ticks

结果类似于val it : int64 = -42L-- 更好,但仍然不准确。例如,在C#: Double to long conversion中讨论了精度损失的原因。

所以问题是:有没有办法在不损失精度的情况下将 a往返DateTime于 a之间?double

更新:接受的答案很清楚地解释了“它实际上是如何工作的”,但事实证明,System.BitConverter.Int64BitsToDouble()并且System.BitConverter.DoubleToInt64Bits()或多或少地做到了这一点,尽管显然受限于long<->double转换,并且仅限于小端机器。有关实际代码,请参阅https://referencesource.microsoft.com/#mscorlib/system/bitconverter.cs,db20ea77a561c0ac

标签: c#.netdatetimeserializationf#

解决方案


由于您似乎并不关心生成的 double 或“hacky”方法的实际内容,而只关心将它们转换回来的能力并且两种类型都是unmanaged,因此您可以使用一种非常通用的方法。

如果您启用不安全代码,您可以使用以下方法进行直接超快速实现stackalloc

        static void Main(string[] args)
        {
            Check(nameof(DateTime.MinValue), DateTime.MinValue);
            Check(nameof(DateTime.MaxValue), DateTime.MaxValue);
            Check(nameof(DateTime.Now), DateTime.Now);
            Check(nameof(DateTime.UtcNow), DateTime.UtcNow);
            Console.ReadLine();
        }

        static void Check(string name, DateTime @DateTime)
        {
            Console.WriteLine($@"{name} expected: {@DateTime}");
            var @double = ConvertUnmanaged<DateTime, double>(@DateTime);
            @DateTime = ConvertUnmanaged<double, DateTime>(@double);
            Console.WriteLine($@"{name} unmanaged returned: {@DateTime}");
            @double = ConvertFixed<DateTime, double>(@DateTime);
            @DateTime = ConvertFixed<double, DateTime>(@double);
            Console.WriteLine($@"{name} address returned: {@DateTime}");
        }

        // types can be of different size
        static unsafe TOut ConvertUnmanaged<TIn, TOut>(TIn pIn)
        where TIn : unmanaged
        where TOut : unmanaged
        {
            var mem = stackalloc byte[Math.Max(sizeof(TIn), sizeof(TOut))];
            var mIn = (TIn*)mem;
            *mIn = pIn;
            return *(TOut*)mIn;
        }

        // types should be of same size
        static unsafe TOut ConvertFixed<TIn, TOut>(TIn pIn)
        where TIn : unmanaged
        where TOut : unmanaged
        {
            if (sizeof(TIn) != sizeof(TOut)) throw new ArgumentException();
            return *(TOut*)(&pIn);
        }

这将输出:

MinValue expected: 01.01.0001 00:00:00
MinValue unmanaged returned: 01.01.0001 00:00:00
MinValue address returned: 01.01.0001 00:00:00
MaxValue expected: 31.12.9999 23:59:59
MaxValue unmanaged returned: 31.12.9999 23:59:59
MaxValue address returned: 31.12.9999 23:59:59
Now expected: 09.11.2020 16:43:24
Now unmanaged returned: 09.11.2020 16:43:24
Now address returned: 09.11.2020 16:43:24
UtcNow expected: 09.11.2020 15:43:24
UtcNow unmanaged returned: 09.11.2020 15:43:24
UtcNow address returned: 09.11.2020 15:43:24

如您所见,ConvertUnmanaged将简单地转换任何unmanaged类型,但临时持有类型(在您的情况下为双倍)大小应该与主要类型的大小(在您的情况下为 DateTime)具有相同或更大的大小

ConvertFixed 有点受限


推荐阅读