C# 在不损失精度的情况下通过double往返日期时间

C# 在不损失精度的情况下通过double往返日期时间,c#,.net,datetime,serialization,f#,C#,.net,Datetime,Serialization,F#,这个问题不是关于将DateTime序列化为double和back是否明智,而是关于当您必须这样做时应该做什么 表面上的解决方案是使用DateTime.ToOADate,如,但这会严重降低精度,例如 让now=DateTime.UtcNow在DateTime.FromOADatenow.ToOADate.Ticks-now.Ticks中 结果是 val it:int64=-7307L,这非常糟糕,因为这几乎是一毫秒 在这方面,一种更为粗糙的方法(在F中称为float)只是在long和double之

这个问题不是关于将DateTime序列化为double和back是否明智,而是关于当您必须这样做时应该做什么

表面上的解决方案是使用DateTime.ToOADate,如,但这会严重降低精度,例如

让now=DateTime.UtcNow在DateTime.FromOADatenow.ToOADate.Ticks-now.Ticks中 结果是 val it:int64=-7307L,这非常糟糕,因为这几乎是一毫秒

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

DateTimeint64floatnow.Ticks.Ticks-now.Ticks中的now=DateTime.UtcNow 结果类似于val it:int64=-42L-更好,但仍然不精确。例如,本文讨论了精度损失的原因

因此,问题是:有没有一种方法可以在不损失精度的情况下,将日期时间转换为双精度和双精度


更新:在解释其实际工作原理时,公认的答案是明确的,但事实证明System.BitConverter.Int64BitsToDouble和System.BitConverter.DoubleToInt64位或多或少都能做到这一点,尽管显然只限于longdouble转换,并且仅限于小端机器。有关实际代码,请参阅。

延迟选项是特定于域的,但可能适用于大多数应用程序,它具有一个固定的已知基号和实际序列化/反序列化的偏移量。基数需要足够大,以便将您关心的所有日期时间所需的偏移量都保持在双精度范围内

在实验上,我已经确定,在28.5年的时间里,这个补偿只需要一点时间。例如,如果您希望进行类似let dt=DateTime.Parse2020-11-09的往返,则需要从dt.Ticks中减去此基数,将结果转换并存储为double,然后当您再次读入时,再将基数相加。在我们的示例中,它可以类似于let base=floatDateTime.Parse2000-01-01.Ticks


更少的延迟-更正确的-选项将涉及实际重用dt.Ticks的长值位,并将其存储在double中,因为位的数量是相同的,但我将把它留给其他响应程序。

延迟选项,它是特定于域的,但可能适用于大多数应用程序,就是要有一个固定的已知基数和实际序列化/反序列化的偏移量。基数需要足够大,以便将您关心的所有日期时间所需的偏移量都保持在双精度范围内

在实验上,我已经确定,在28.5年的时间里,这个补偿只需要一点时间。例如,如果您希望进行类似let dt=DateTime.Parse2020-11-09的往返,则需要从dt.Ticks中减去此基数,将结果转换并存储为double,然后当您再次读入时,再将基数相加。在我们的示例中,它可以类似于let base=floatDateTime.Parse2000-01-01.Ticks


更少的延迟-更正确的-选项将涉及实际重用dt.Ticks的long值的位,并将其存储在double中,因为位的数量是相同的,但我将把它留给其他响应者。

因为您似乎不关心生成的double或hacky方法的实际内容,但是,只有能够将它们转换回来,并且这两种类型都是非托管的,才可以使用非常通用的方法

如果启用不安全代码,则可以使用stackalloc执行直接超快速实现:

静态环[]args { 检查DateTime.MinValue、DateTime.MinValue的名称; 检查DateTime.MaxValue、DateTime.MaxValue的名称; 检查DateTime.Now的名称,DateTime.Now; 检查DateTime.UtcNow的名称,DateTime.UtcNow; Console.ReadLine; } 静态无效检查字符串名称,DateTime@DateTime { Console.WriteLine$@{name}应为:{@DateTime}; var@double=ConvertUnmanaged@DateTime; @日期时间=ConvertUnmanaged@double; WriteLine$@{name}非托管返回:{@DateTime}; @双重=ConvertFixed@DateTime; @日期时间=ConvertFixed@double; Console.WriteLine$@{name}返回的地址:{@DateTime}; } //类型可以有不同的大小 静态不安全TOut转换器非托管pIn where-TIn:非托管 兜售地点:非托管 { var mem=stackalloc字节[Math.MaxsizeofTIn,sizeofTOut]; var mIn=TIn*mem; *最小=引脚; 返回*TOut*mIn; } //类型应具有相同的大小 静态不安全TOut转换器固定销 where-TIn:非托管 兜售地点:非托管 { if sizeofTIn != sizeofTOut抛出新参数异常; 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将简单地转换任何非托管类型,但是案例中的临时持有类型double的大小应该与案例中的主类型DateTime的大小相同或更大


ConvertFixed有一点限制

,因为您似乎不关心生成的double或hacky方法的实际内容,而只关心将它们转换回来的能力,并且这两种类型都是非托管的,所以可以使用非常通用的方法

如果启用不安全代码,则可以使用stackalloc执行直接超快速实现:

静态环[]args { 检查DateTime.MinValue、DateTime.MinValue的名称; 检查DateTime.MaxValue、DateTime.MaxValue的名称; 检查DateTime.Now的名称,DateTime.Now; 检查DateTime.UtcNow的名称,DateTime.UtcNow; Console.ReadLine; } 静态无效检查字符串名称,DateTime@DateTime { Console.WriteLine$@{name}应为:{@DateTime}; var@double=ConvertUnmanaged@DateTime; @日期时间=ConvertUnmanaged@double; WriteLine$@{name}非托管返回:{@DateTime}; @双重=ConvertFixed@DateTime; @日期时间=ConvertFixed@double; Console.WriteLine$@{name}返回的地址:{@DateTime}; } //类型可以有不同的大小 静态不安全TOut转换器非托管pIn where-TIn:非托管 兜售地点:非托管 { var mem=stackalloc字节[Math.MaxsizeofTIn,sizeofTOut]; var mIn=TIn*mem; *最小=引脚; 返回*TOut*mIn; } //类型应具有相同的大小 静态不安全TOut转换器固定销 where-TIn:非托管 兜售地点:非托管 { 如果sizeofTIn!=sizeofTOut抛出新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将简单地转换任何非托管类型,但是案例中的临时持有类型double的大小应该与案例中的主类型DateTime的大小相同或更大


ConvertFixed有一点限制

,所以正如其他人已经说过的,最好使用ticks的原生日期时间值。但正如@PatrickBeynio所指出的,如果你必须这样做,你可以。Patrick的两种方法都是通用的,非常酷,但我会抛弃另外两种方法。首先使用位转换器,然后使用.Net不安全类

        DateTime now = DateTime.Now;

        var bytes = BitConverter.GetBytes(now.ToBinary());
        var timeAsDouble = BitConverter.ToDouble(bytes);

        var timeAsBinary = BitConverter.ToInt64(BitConverter.GetBytes(timeAsDouble));

        DateTime roundTripped = DateTime.FromBinary(timeAsBinary);

        Console.WriteLine(now.ToString("hh:mm:ss:fff"));
        Console.WriteLine(roundTripped.ToString("hh:mm:ss:fff"));

        var binaryTime = now.ToBinary();
        ref double doubleTime = ref Unsafe.As<long,double>(ref binaryTime);

        Console.WriteLine(doubleTime);

        ref long backToBinaryTime = ref Unsafe.As<double, long>(ref doubleTime);
        roundTripped = DateTime.FromBinary(backToBinaryTime);

        Console.WriteLine(roundTripped.ToString("hh:mm:ss:fff"));

因此,正如其他人已经说过的,最好使用ticks的原生日期时间值。但正如@PatrickBeynio所指出的,如果你必须这样做,你可以。Patrick的两种方法都是通用的,非常酷,但我会抛弃另外两种方法。首先使用位转换器,然后使用.Net不安全类

        DateTime now = DateTime.Now;

        var bytes = BitConverter.GetBytes(now.ToBinary());
        var timeAsDouble = BitConverter.ToDouble(bytes);

        var timeAsBinary = BitConverter.ToInt64(BitConverter.GetBytes(timeAsDouble));

        DateTime roundTripped = DateTime.FromBinary(timeAsBinary);

        Console.WriteLine(now.ToString("hh:mm:ss:fff"));
        Console.WriteLine(roundTripped.ToString("hh:mm:ss:fff"));

        var binaryTime = now.ToBinary();
        ref double doubleTime = ref Unsafe.As<long,double>(ref binaryTime);

        Console.WriteLine(doubleTime);

        ref long backToBinaryTime = ref Unsafe.As<double, long>(ref doubleTime);
        roundTripped = DateTime.FromBinary(backToBinaryTime);

        Console.WriteLine(roundTripped.ToString("hh:mm:ss:fff"));

你为什么坚持用双倍的?最直接的往返是通过Ticks进行的,这是一个很长的循环,它可以通过相应的构造函数简单地恢复。我意识到序列化long显然是正确的做法,但在我的例子中,存储DateTime的API恰好是通过存储/读取double来实现的,因此产生了这个问题。你为什么坚持使用double?最直接的往返是通过Ticks进行的,这是一个很长的循环,它可以通过相应的构造函数简单地恢复。我意识到序列化long显然是正确的做法,但在我的例子中,存储DateTime的API恰好是通过存储/读取double来实现的——因此这个问题就诞生了。因为它不仅适用于double和DateTime,而且适用于任何合适的非托管类型。我也认为@György-Kőszeg应该发表他的评论,我投了更高的票作为答案!因为这是解决这个问题最好的办法。我的答案,虽然是一个有效的解决方案,但对有类似问题的人来说,更像是糖衣,告诉他们如何使用指针绕过非托管类型的类型检查这正是我想要的答案。基本上,我不知道如何将long的位打包成double的位,而您提供了一个通用的解决方案作为奖励。谢谢。因为它不仅适用于double和datetime,而且适用于任何合适的非托管类型。我也认为@György-Kőszeg应该发表他的评论,我投了更高的票作为答案!因为这是解决这个问题最好的办法。我的答案,虽然是一个有效的解决方案,但对有类似问题的人来说,更像是糖衣,告诉他们如何使用指针绕过非托管类型的类型检查这正是我想要的答案。基本上,我不知道如何将long的位打包成double的位,而您提供了一个通用的解决方案作为奖励。Tha 谢谢你。