跟 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 呢?逻辑其实很简单。
-
2020-10-16T00:26:37 这个时间是时区+10 的时间;
-
当我们把这个时间转成 UTC 时间的时候,就变成了 2020-10-15T14:26:37;
-
CurrentDate 往前推 24 小时是 2020-10-15T23:09:50;
-
很明显,2020-10-15T14:26:37 不在 24 小时这个区间之内,所以是 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 信息,比如:
- 在本地时间+11 的情况下,下面的代码输出的结果是
"2020-10-16T01:26:37+11:00"
- 在本地时间 UTC 的情况下,下面的代码输出的结果是
"2020-10-15T14:26:37+00:00"
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 信息。
- 在本地时间+11 的情况下,下面的代码输出的结果是
"2020-10-16T01:26:37+11:00"
- 在本地时间 UTC 的情况下,下面的代码输出的结果是
"2020-10-15T14:26:37+00:00"
string jToken = @"
{
""DateTime"":""2020-10-16T00:26:37+10:00""
}
";
JToken deserializedObj = JsonConvert.DerializeObject(jToken);
所以服务器的时区一般都是 UTC 时间,当位于不同的服务器的应用进行数据交换的时候,假如这些服务器使用的时区信息不一样,就很容易出现各种奇怪的问题。
3. 结语
-
本机时区影响了
DateTime.Parse()
这个方法的输出结果,输出的 DateTime = 本机时区的 DateTime。 -
这个 DateTime object 在与 UTC 时间进行比较的时候,会影响结果。因为比较的时候没有时区的信息,两个时间都被当做了 UTC 时间。
-
Json 的序列化(Serialization)和反序列化(Deserialization)也会导致原始时区信息消失,默认使用运行机器的时区进行日期转换。
-
服务器时间一般都是用 UTC 时间,带有时区信息的时间很可能会由于各种转换而最终成为 UTC 时间,丢失原始时区。 我们读取数据的时候要保证源数据的时区信息无误,不然就很容易造成日期之间的比较结果出错(比如我一开始就给源数据错误地加了 10 小时……)。
-
在本地测试时,如果切换了时区,最好关闭现在的程序/文件,重新创建一个文件,然后再做测试。
今天关于日期的问题就先聊这些,我们的系统有很多跟 json、数据读写相关的操作,而且不同的客户有不同的时区,所以我还要再研究一下 DateTimeOffset 和 DateTime 的相关知识,有心得再和大家分享~