如何在 Delphi 中扩展 JSON $date,或者它是关于 System.JSON 中时区和 UTC 的错误?
How to do extended JSON $date right in Delphi, or is it a bug about timezones and UTC in System.JSON?
我正在尝试使用 TJSONObjectBuilder...AddPairs()
解析扩展 JSON。我的 JSON 包含一个 $date
(我需要它在 Utc 中用于 MongoDB)。但是不知何故,时区被打破了,不管我的输入是否已经是 Utc。
Input : {"Zulu":{"$date":"2019-01-01T00:00:00.000Z"},"Utc+1":{"$date":"2019-01-01T01:00:00.000+01:00"}}
Output: {"Zulu":{"$date":"2019-01-01T01:00:00.000Z"},"Utc+1":{"$date":"2019-01-01T01:00:00.000Z"}}
^ ^
没有 TJsonDateTimeZoneHandling.Utc
它是正确的,但这对我没有帮助,因为我需要 Utc 格式的结果:
Output: {"Zulu":{"$date":"2019-01-01T01:00:00.000+01:00"},"Utc+1":{"$date":"2019-01-01T01:00:00.000+01:00"}}
这是我展示它的最少代码:
program SystemJsonDateTest;
{$APPTYPE CONSOLE}
uses
System.Classes, System.JSON.Types, System.JSON.Writers, System.JSON.Builders;
var
StringWriter: TStringWriter;
JsonWriter: TJsonTextWriter;
Builder: TJSONObjectBuilder;
begin
StringWriter:= TStringWriter.Create;
JsonWriter:= TJsonTextWriter.Create(StringWriter);
JsonWriter.ExtendedJsonMode:= TJsonExtendedJsonMode.StrictMode;
JsonWriter.DateTimeZoneHandling:= TJsonDateTimeZoneHandling.Utc;
TJSONObjectBuilder.Create(JsonWriter)
.BeginObject
.AddPairs('{"Zulu":{"$date":"2019-01-01T00:00:00.000Z"},'
+ '"Utc+1":{"$date":"2019-01-01T01:00:00.000+01:00"},'
+ '"Unix":{"$date":1546300800000}}')
.EndObject
.Free;
JsonWriter.Free;
WriteLn(StringWriter.ToString);
StringWriter.Free;
ReadLn;
end.
背景: 我正在使用 TMongoDocument.AsJSON
,发现了这种行为并尝试用最少的代码重现它并且没有任何参考 MongoDB 组件。如果我做了一些奇怪的事情或者演示可以更简化,请评论...
在那个 MongoDocument 中,使用 TBsonWriter
代替,但它显示了同样的问题:
Stream:= TFileStream.Create('file.bson', fmCreate);
BsonWriter:= TBsonWriter.Create(Stream);
TJSONObjectBuilder.Create(BsonWriter).BeginObject.AddPairs(//see above
我知道,这是很多文字 - 如果您忘记了问题,它在标题中;)
MongoDB 客户端可能支持 JSON 输入中 Date
字段的“$date”扩展语法中的区域(即使 Delphi 客户端似乎忽略了它),但 MongoDB 服务器不会处理其 BSON 存储中的区域。
事实上,Date
值的 reference documentation states 存储为 UTC - 它们甚至在 BSON 格式中被称为 UTC Date
,并存储为 Unix 毫秒数的 Int64:
BSON Date is a 64-bit integer that represents the number of milliseconds since the Unix epoch (Jan 1, 1970). This results in a representable date range of about 290 million years into the past and future.
因此,您的 "Utc+1"
和 "Zulu"
字段将包含完全相同的 UTC 时间戳,即使在客户端库正确转换时区之后也是如此。
所以您最好只将 UTC 日期发送到 MongoDB,并在客户端进行转换。即使转换正确,在所有情况下您都会丢失区域信息,因为它将存储为 UTC。并且不要使用 ISO-8601 文本进行传输,而只是 UnixTime
值,作为整数:
function DateTimeToUnixMSTime(const AValue: TDateTime): Int64;
begin
result := Round((AValue - UnixDateDelta) * MSecsPerDay);
end;
顺便说一句,最好只在任何类型的数据库中使用 UTC 日期,然后使用即时转换到 display/reporting 上的当前本地用户,并将本地区域存储在一个单独的区域中字段,如果确实需要,可以作为文本标识符,也可以作为以天为单位的浮点偏差(可能更方便 - 请注意区域偏差不是必需的整数,例如阿富汗)。
是的,是bug,10.3已经修复
在单元System.JSON.Builders中,TJSONCollectionBuilder.TBaseCollection.WriteJSON()
创建了一个默认DateTimeZoneHandling=Local的TJsonTextReader,这意味着任何DateTimes都被转换为本地。
但在 System.JSON.Writers、TJsonTextWriter.WriteValue(Value: TDateTime)
中,当 DateTimeZoneHandling=Utc 时,不能包含时区的 DateTime 值被预期并解释为 Utc。
因此,TJSONCollectionBuilder 需要一个 DateTimeZoneHandling=Local 的 Writer,这使得无法以正确的 Utc 获取输出。
...调试之后,我知道 google 的用途:
http://docwiki.embarcadero.com/RADStudio/Rio/en/New_features_and_customer_reported_issues_fixed_in_RAD_Studio_10.3
- TMongoDocument 和 TJSONCollectionBuilder 错误地解析 ISO 日期
数据,Data\FireDAC,RTL,RTL\Delphi RSP-17046
- [FireDAC,MongoDB]
[TJSONCollectionBuilder.TBaseCollection.WriteJSON] 日期总是
如果值在文本模式数据中更新,则转换为本地时区,
Data\FireDAC、RTL\Delphi RSP-20571
我正在尝试使用 TJSONObjectBuilder...AddPairs()
解析扩展 JSON。我的 JSON 包含一个 $date
(我需要它在 Utc 中用于 MongoDB)。但是不知何故,时区被打破了,不管我的输入是否已经是 Utc。
Input : {"Zulu":{"$date":"2019-01-01T00:00:00.000Z"},"Utc+1":{"$date":"2019-01-01T01:00:00.000+01:00"}}
Output: {"Zulu":{"$date":"2019-01-01T01:00:00.000Z"},"Utc+1":{"$date":"2019-01-01T01:00:00.000Z"}}
^ ^
没有 TJsonDateTimeZoneHandling.Utc
它是正确的,但这对我没有帮助,因为我需要 Utc 格式的结果:
Output: {"Zulu":{"$date":"2019-01-01T01:00:00.000+01:00"},"Utc+1":{"$date":"2019-01-01T01:00:00.000+01:00"}}
这是我展示它的最少代码:
program SystemJsonDateTest;
{$APPTYPE CONSOLE}
uses
System.Classes, System.JSON.Types, System.JSON.Writers, System.JSON.Builders;
var
StringWriter: TStringWriter;
JsonWriter: TJsonTextWriter;
Builder: TJSONObjectBuilder;
begin
StringWriter:= TStringWriter.Create;
JsonWriter:= TJsonTextWriter.Create(StringWriter);
JsonWriter.ExtendedJsonMode:= TJsonExtendedJsonMode.StrictMode;
JsonWriter.DateTimeZoneHandling:= TJsonDateTimeZoneHandling.Utc;
TJSONObjectBuilder.Create(JsonWriter)
.BeginObject
.AddPairs('{"Zulu":{"$date":"2019-01-01T00:00:00.000Z"},'
+ '"Utc+1":{"$date":"2019-01-01T01:00:00.000+01:00"},'
+ '"Unix":{"$date":1546300800000}}')
.EndObject
.Free;
JsonWriter.Free;
WriteLn(StringWriter.ToString);
StringWriter.Free;
ReadLn;
end.
背景: 我正在使用 TMongoDocument.AsJSON
,发现了这种行为并尝试用最少的代码重现它并且没有任何参考 MongoDB 组件。如果我做了一些奇怪的事情或者演示可以更简化,请评论...
在那个 MongoDocument 中,使用 TBsonWriter
代替,但它显示了同样的问题:
Stream:= TFileStream.Create('file.bson', fmCreate);
BsonWriter:= TBsonWriter.Create(Stream);
TJSONObjectBuilder.Create(BsonWriter).BeginObject.AddPairs(//see above
我知道,这是很多文字 - 如果您忘记了问题,它在标题中;)
MongoDB 客户端可能支持 JSON 输入中 Date
字段的“$date”扩展语法中的区域(即使 Delphi 客户端似乎忽略了它),但 MongoDB 服务器不会处理其 BSON 存储中的区域。
事实上,Date
值的 reference documentation states 存储为 UTC - 它们甚至在 BSON 格式中被称为 UTC Date
,并存储为 Unix 毫秒数的 Int64:
BSON Date is a 64-bit integer that represents the number of milliseconds since the Unix epoch (Jan 1, 1970). This results in a representable date range of about 290 million years into the past and future.
因此,您的 "Utc+1"
和 "Zulu"
字段将包含完全相同的 UTC 时间戳,即使在客户端库正确转换时区之后也是如此。
所以您最好只将 UTC 日期发送到 MongoDB,并在客户端进行转换。即使转换正确,在所有情况下您都会丢失区域信息,因为它将存储为 UTC。并且不要使用 ISO-8601 文本进行传输,而只是 UnixTime
值,作为整数:
function DateTimeToUnixMSTime(const AValue: TDateTime): Int64;
begin
result := Round((AValue - UnixDateDelta) * MSecsPerDay);
end;
顺便说一句,最好只在任何类型的数据库中使用 UTC 日期,然后使用即时转换到 display/reporting 上的当前本地用户,并将本地区域存储在一个单独的区域中字段,如果确实需要,可以作为文本标识符,也可以作为以天为单位的浮点偏差(可能更方便 - 请注意区域偏差不是必需的整数,例如阿富汗)。
是的,是bug,10.3已经修复
在单元System.JSON.Builders中,TJSONCollectionBuilder.TBaseCollection.WriteJSON()
创建了一个默认DateTimeZoneHandling=Local的TJsonTextReader,这意味着任何DateTimes都被转换为本地。
但在 System.JSON.Writers、TJsonTextWriter.WriteValue(Value: TDateTime)
中,当 DateTimeZoneHandling=Utc 时,不能包含时区的 DateTime 值被预期并解释为 Utc。
因此,TJSONCollectionBuilder 需要一个 DateTimeZoneHandling=Local 的 Writer,这使得无法以正确的 Utc 获取输出。
...调试之后,我知道 google 的用途: http://docwiki.embarcadero.com/RADStudio/Rio/en/New_features_and_customer_reported_issues_fixed_in_RAD_Studio_10.3
- TMongoDocument 和 TJSONCollectionBuilder 错误地解析 ISO 日期 数据,Data\FireDAC,RTL,RTL\Delphi RSP-17046
- [FireDAC,MongoDB] [TJSONCollectionBuilder.TBaseCollection.WriteJSON] 日期总是 如果值在文本模式数据中更新,则转换为本地时区, Data\FireDAC、RTL\Delphi RSP-20571