我有一个独特的问题,我的石英作业调度程序实现使用quartz.net代码库版本2.0.1构建,最近发现在运行和执行作业时忽略了时区和utc偏移。这是本版本的quartz.net中的继承错误,更新到2.1.1版现在已超出范围,因此我使用此算法编写了一种快速而肮脏的计算偏移的方法:
(ServerTime - ClientTime) - TargetTime = New_TargetTime_With_Offset
这里的想法是客户,在纽约市说,在下午5点找工作,并希望它在下午2点运行。服务器(此应用程序和作业服务器运行的位置)当前时间是下午2:00,因此我们将客户端时间和服务器时间用于获取偏移量并将该偏移量应用于目标时间,即作业应运行的时间。
我的问题是,这感觉就像计算日期的方式一样,但似乎它可以完成这项工作。有没有更好/更可靠的方法来做这个日期数学?此外,这似乎是边缘情况下的错误,我错过了什么?
以下是实施:
/// <summary>
/// Takes three dates and returns the adjusted hour value.
/// All date data is ignored except for the hour.
/// </summary>
/// <param name="serverTime"></param>
/// <param name="clientTime"></param>
/// <param name="targetTime"></param>
/// <returns></returns>
private static DateTime OutputDate(DateTime serverTime, DateTime clientTime, DateTime targetTime)
{
DateTime? output = null;
TimeSpan? dateDiff;
if (serverTime < clientTime)
{
dateDiff = (clientTime - serverTime);
}
else
{
dateDiff = (serverTime - clientTime);
}
output = (targetTime - dateDiff);
return output.Value;
}
以下是利用它的两个例子:
/// <summary>
/// -5 Offset (NYC)
/// </summary>
/// <returns></returns>
private static Int32 ZoneTest001()
{
var targetTime = DateTime.Parse("6/12/2013 5:00PM"); // NYC (est) [The time the report should be received in NYC]
var clientTime = DateTime.Parse("6/12/2013 5:00PM"); // NYC (est) [The time of the client when the report is created (now) ]
var serverTime = DateTime.Parse("6/12/2013 2:00PM"); // SEA (pst) [The time of the app server when the report is created (now) ]
//
// NYC Wants to send a report at 5:00pm EST
// The server time will be 2:00pm PST
// The client time will be 5:00pm EST
double outputHour = 0; // should end up as 2:00pm PST
//
// 1) Get offset (diff between client & server time)
// 2) Subtract offset from "targetTime"
// 3) Set the report to be sent at the new hour value.
outputHour = OutputDate(serverTime, clientTime, targetTime).Hour;
return (int)outputHour;
}
/// <summary>
/// +5 Offset (India)
/// </summary>
/// <returns></returns>
private static Int32 ZoneTest002()
{
var targetTime = DateTime.Parse("6/12/2013 5:00PM"); // IND (ist)
var clientTime = DateTime.Parse("6/12/2013 9:00AM"); // IND (ist)
var serverTime = DateTime.Parse("6/12/2013 2:00PM"); // SEA (pst)
//
// INDIA Wants to send a report at 5:00pm IST
// The server time will be 2:00pm PST
// The client time will be 9:00am PST
double outputHour = 0; // should end up as 2:00pm PST
outputHour = OutputDate(serverTime, clientTime, targetTime).Hour;
return (int)outputHour;
}
谢谢。
答案 0 :(得分:11)
你实际上错过了很多。
时区偏移不是常数。许多时区为夏令时(a.k.a。“夏令时”)切换偏移。因此,当您根据每个位置(服务器,客户端,目标)的“现在”计算偏移量时,这仅反映当前偏移量。
在具有DST的任何时区中,时钟前滚时缺少小时,而时钟向后滚动时重复小时。如果您正在处理当地时间,并且预定的事件处于模糊的时间段,您无法确定实际运行时间。为了消除歧义,您需要告知相应的偏差是什么,或者你需要用UTC来处理。
如果您要从一个时区转换为另一个时区,则需要处理时区,而不仅仅是他们的偏移。在.Net中,您可以使用内置的Windows时区数据库和相应的TimeZoneInfo
类。或者,您可以使用更标准的IANA时区数据库,以及Noda Time等库。
使用DateTime
类型时,请特别注意.Kind
属性的设置。在处理不同类型时,许多功能都有不同的行为。使用DateTimeOffset
类型会更安全,更有用。
您真的不应该依赖运行代码的服务器的时区。服务器代码应该是时区中立的。您应该在{em>桌面或移动应用程序中使用DateTime.Now
或TimeZoneInfo.Local
或任何类似功能的唯一位置。服务器代码应仅依赖于UTC。
我真的不明白为什么你的OutputDate
方法中有可以为空的值。没有理由这样做。而且,你实际上是在取差异的绝对值 - 即降低方向性。时区偏移确实是方向性的,因此您可能会在当前实现中获得无效结果。
我查看了Quartz.net API,看起来他们更喜欢用UTC安排活动时间。这是一件非常好的事情,因为UTC没有歧义。从Quartz.Net Tutorial开始,trigger.StartTimeUtc
显然是UTC DateTime
。既然你说你不能使用最新的版本,我也检查了他们的旧的1.0 API文档,它仍然是UTC。
更新: Quartz.Net 2.5以及更好地处理时区。有关详细信息,请参阅#317。
让我们将这一切放在一起作为您的示例用例。纽约市的一位客户希望在当地时区下午2:00开始工作。服务器的时区无关紧要,创建作业的时间也是如此。
// June 6, 2013 2:00 PM Kind = Unspecified
DateTime dt = new DateTime(2013, 6, 13, 14, 0, 0);
// This is the correct Windows time zone for New York
TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
// Get the time in UTC - The kind matters here.
DateTime utc = TimeZoneInfo.ConvertTimeToUtc(dt, tz);
// Feed it to a quartz event trigger
trigger.StartTimeUtc = utc;
当我在第三步中将时间转换为UTC时,如果时间不明确,.Net将假定您需要标准时间而不是日光时间。如果您想要更具体,您必须检查歧义,然后询问您的用户他们想要的两个当地时间。然后你必须使用DateTimeOffset
来区分它们。如果你认为你可能需要这个,请告诉我,我可以制作一个样本,但它有点复杂。
而且,如果您想将IANA时区与Noda Time一起使用,那么这就好了:
LocalDateTime ldt = new LocalDateTime(2013, 6, 13, 14, 0);
DateTimeZone tz = DateTimeZoneProviders.Tzdb["America/New_York"];
ZonedDateTime zdt = ldt.InZoneLeniently(tz);
trigger.StartTimeUtc = zdt.ToDateTimeUtc();
InZoneLeniently
方法将提供与上述代码相同的行为。但是如果需要,还可以指定其他选项。
哦,这不重要,但印度是+5:30
,而不是+5