Luna Tech

Tutorials For Dummies.

跟 Date 奋战的第一回合

2020-11-11


0. 前言

最近遇到了一些关于 DateTime 的技术问题,这篇文章聊聊我遇到的一些坑。


1. 背景

客户给的文件里面有一个时间,我们需要把当前时间 24 小时之内的记录标记为 True,其他的标记为 False。

在生产环境中,我们发现有这样一条记录被标记为 False。

DateTime: "2020-10-16T00:26:37+10:00"
CurrentDate: "2020-10-16T23:09:50+00:00"

为什么被标记成 False 呢?逻辑其实很简单。

而客户那边说,他们给的时间是 UTC 时间,所以我们需要在读取数据的时候,把时区去掉,变成下面这样:

DateTime: "2020-10-16T00:26:37+00:00"
CurrentDate: "2020-10-16T23:09:50+00:00"

这样一来,这条记录就满足了当前时间 24 小时之内这个条件,可以被标记为 True。

我的任务是在本地环境下进行测试,验证这段用来做比较的代码:

string s1 = "2020-10-16T00:26:37+10:00";
object input = DateTime.Parse(s1);
object dt = DateTime.UtcNow.AddDays(-1); // 24 小时之前的时间
Console.WriteLine(((DateTime)input).CompareTo(dt));

尝试重现问题

Input Date:“2020-11-10T08:49:06+10:00”

UtcNow:“2020-11-11T08:15:06+00:00”

问:Input Date 是否属于当前时间 24 小时之内?

预期结果:False(-1)

string s1 = "2020-11-10T08:49:06+10:00";
object input = DateTime.Parse(s1);
object dt = DateTime.UtcNow.AddDays(-1);
Console.WriteLine(input.ToString());
Console.WriteLine(dt.ToString());
Console.WriteLine(((DateTime)input).CompareTo(dt));

本机时区 +11 的运行结果

输出:True(1)

问了同事之后,得知本机的时区会影响 DateTime.Parse() 这个方法,于是我把自己的本机时间改成了 UTC。

但是,改完之后,我重新运行这段代码,发现结果还是 1。

于是我就开始怀疑人生了,跟同事说,这条记录在我本地无法重现。

同事马上热心地提供了另外的几条服务器记录。

未解之谜

同事发来的记录长这样:

看着这个记录,我彻底懵了……

明明第一条记录和第二条记录在计算了+10 的时区之后,依旧满足 24 小时的规则,为什么一条是 False,一条是 True???

百思不得其解之后,我发了一条无奈的朋友圈……然后睡觉去了(做梦都在做 date comparison,┭┮﹏┭┮)。

第二天早上,我以为自己经过了一夜的思考,能够参悟这个问题,然而我并没有(😔 ),横测竖测,这两条都是 True。

最后,我只能选择跟上级申请更多的数据权限,来进行整个过程的完整测试。

这次测试时,我在读取数据的时候把时区去掉了,结果是:之前那些由于时区+10 而被错误标记为 False 的记录(14-24 小时区间),都成功地被标记成 True ,测试通过。


2. 总结一下遇到的坑

坑 1:DateTime.Parse 受本机时区影响

object input = DateTime.Parse("2020-11-03T03:49:06+10:00");
Console.WriteLine(input.ToString());
// 本机时区+11,结果:3/11/2020 4:49:06 AM
// 本机时区+0,结果:2/11/2020 5:49:06 PM
// 服务器结果:2/11/2020 5:49:06 PM

坑 2:正在运行的程序可能无法实时体现本机时区的切换

我用的测试工具是 LinqPad 和 Visual Studio。

比如下面两个 query 是我在本机不同时区的情况下分别创建的,这两个相同的 query 会按照创建时的时区来输出结果,而不是根据本机当前的时区来实时改变结果。

我在更换时区之后没有关闭并且重启 Visual Studio 的 project,可能时区也没有真的换过来。

在我尝试重现问题的时候,LinqPad 的 Query1,是我在时区 +11 的时候创建的,改了时区之后,运行结果不变。

当我在时区 0 的时候,新建 query2,把 query1 的内容复制进去运行,就得到了 -1 的结果。

可惜我一开始没有发现这个坑,而是跑去找同事要另外的日志,然后就被带偏了……

坑 4:夏令时

现在是悉尼用的是夏令时,所以本机时区是 +11,而不是 +10。

我的测试数据是按照 +10 来的,实际比较的时候会被 DateTime.Parse() 转换成夏令时时间(上面的 Query1 结果),一开始没反应过来,总觉得时间不对 😓。

坑 5:Json 序列化和反序列化(C#为例)

Json Serialization 的时候会根据本地时区来进行日期转换,我们会丢失原始数据里面的 timezone 信息,比如:

string jToken = @"
 {
  ""DateTime"":""2020-10-16T00:26:37+10:00""
 }
 ";


 JToken j = JToken.Parse(jToken);
 string serializedString = JsonConvert.SerializeObject(j);

同样的,Json Deserialization 的时候也会把含有 timezone 的 string 转换成跟本地时间一致的 datetime,此时我们又一次丢失了原始数据里面的 timezone 信息。

string jToken = @"
 {
  ""DateTime"":""2020-10-16T00:26:37+10:00""
 }
 ";

 JToken deserializedObj = JsonConvert.DerializeObject(jToken);

所以服务器的时区一般都是 UTC 时间,当位于不同的服务器的应用进行数据交换的时候,假如这些服务器使用的时区信息不一样,就很容易出现各种奇怪的问题。


3. 结语

  1. 本机时区影响了 DateTime.Parse() 这个方法的输出结果,输出的 DateTime = 本机时区的 DateTime。

  2. 这个 DateTime object 在与 UTC 时间进行比较的时候,会影响结果。因为比较的时候没有时区的信息,两个时间都被当做了 UTC 时间。

  3. Json 的序列化(Serialization)和反序列化(Deserialization)也会导致原始时区信息消失,默认使用运行机器的时区进行日期转换。

  4. 服务器时间一般都是用 UTC 时间,带有时区信息的时间很可能会由于各种转换而最终成为 UTC 时间,丢失原始时区。 我们读取数据的时候要保证源数据的时区信息无误,不然就很容易造成日期之间的比较结果出错(比如我一开始就给源数据错误地加了 10 小时……)。

  5. 在本地测试时,如果切换了时区,最好关闭现在的程序/文件,重新创建一个文件,然后再做测试。

今天关于日期的问题就先聊这些,我们的系统有很多跟 json、数据读写相关的操作,而且不同的客户有不同的时区,所以我还要再研究一下 DateTimeOffset 和 DateTime 的相关知识,有心得再和大家分享~