将UTC日期时间转换为考虑时区夏令时的未来时间

4

我们有一个系统,用UTC时间表示从美国/芝加哥时区开始和结束日期时间的周。周从中央时间星期六凌晨零点开始,到中央时间星期五晚上23:59:59结束,因此它们在数据库中的UTC条目为:

Week 1 - begin: 2015-10-24 05:00:00, end 2015-10-31 04:59:59
Week 2 - begin: 2015-10-31 05:00:00, end 2015-11-07 05:59:59
Week 3 - begin: 2015-11-07 06:00:00, end 2015-11-14 05:59:59
Week 4 - begin: 2015-11-14 06:00:00, end 2015-11-21 05:59:59
Week 5 - begin: 2015-11-21 06:00:00, end 2015-11-28 05:59:59

从上面的例子可以看到,夏令时和标准时间之间的时间变化在10月31日至11月7日期间反映出来。

我需要返回一个给定周之前的N周。我们的系统是C# Azure工作和Web角色,并在Azure云中运行(所有计算节点都是UTC)。我的逻辑是,取开始周,添加N周的工作日到该周的开始日期/时间,并请求开始日期大于原始开始日期且小于或等于计算得出的未来日期的周。

var weeks = repository.Fetch(x => x.BeginDate <= nWeeksAheadUtc && x.BeginDate > week.BeginDate)l=;

这个方法是行得通的,除了当夏令时改变但答案没有反应出来的时候。由于时间变化,只基于向第1周的开始日期添加21天并要求接下来的3周结果只返回第2和第3周,因为计算未来值是2015-11-14 05:00:00,它不包括第4周。
我已经使用Nodatime以以下方式解决了这个问题:
LocalDateTime localDateTime = LocalDateTime.FromDateTime(week.BeginDate);
ZonedDateTime zonedDateTime = localDateTime.InZoneStrictly(DateTimeZoneProviders.Tzdb["UTC"]);
zonedDateTime = zonedDateTime.WithZone(DateTimeZoneProviders.Tzdb["America/Chicago"]);
DateTime centralDateTime = zonedDateTime.ToDateTimeUnspecified();
DateTime futureDateTime = centralDateTime.Add(TimeSpan.FromDays(weekCount*7));
localDateTime = LocalDateTime.FromDateTime(futureDateTime);
zonedDateTime = localDateTime.InZoneStrictly(DateTimeZoneProviders.Tzdb["America/Chicago"]);
DateTime nWeeksAheadUtc = zonedDateTime.ToDateTimeUtc();

var weeks = repository.Fetch(x => x.BeginDate <= nWeeksAheadUtc && x.BeginDate > week.BeginDate).OrderBy(x => x.RetailerWeekNumber).ToList();

虽然它可以正常运行,但对于跟随我维护此代码的开发人员来说,似乎笨重且不太直观。是否有更清晰的方法通过Nodatime API或(基本的C#日期/时间)来完成这个任务,我是否遗漏了什么?

添加所请求的示例 - 我刚为此创建了一个Unit测试项目,并编写了以下三个类:

Week.cs

using System;

namespace NodaTimeTest
{
    public class Week
    {
        public int Id { get; set; }
        public DateTime BeginDate { get; set; }
        public DateTime EndDate { get; set; }
    }
}

WeekService.cs

using NodaTime;
using System;
using System.Collections.Generic;
using System.Linq;

namespace NodaTimeTest
{
    public class WeekService
    {
        private readonly List<Week> repository;

        public WeekService()
        {
            this.repository = this.InitWeeks();
        }

        public List<Week> GetNextWeeks(int weekId, int weekCount)
        {
            Week week = this.repository.First(x => x.Id == weekId);

            // the meat - how to do this the right way?
            LocalDateTime localDateTime = LocalDateTime.FromDateTime(week.BeginDate);
            ZonedDateTime zonedDateTime = localDateTime.InZoneStrictly(DateTimeZoneProviders.Tzdb["UTC"]);
            zonedDateTime = zonedDateTime.WithZone(DateTimeZoneProviders.Tzdb["America/Chicago"]);
            DateTime centralDateTime = zonedDateTime.ToDateTimeUnspecified();
            DateTime futureDateTime = centralDateTime.Add(TimeSpan.FromDays(weekCount * 7));
            localDateTime = LocalDateTime.FromDateTime(futureDateTime);
            zonedDateTime = localDateTime.InZoneStrictly(DateTimeZoneProviders.Tzdb["America/Chicago"]);
            DateTime nWeeksAheadUtc = zonedDateTime.ToDateTimeUtc();

            var weeks = repository.Where(x => x.BeginDate <= nWeeksAheadUtc && x.BeginDate > week.BeginDate).OrderBy(x => x.Id).ToList();

            return weeks;
        }

        private List<Week> InitWeeks()
        {
            // sets up our list of 10 example dates in UTC encompassing America/Chicago daylight savings time change on 11/1
            // this means that all weeks are 168 hours long, except week "4", which is 169 hours long.
            var weeks = new List<Week>();
            DateTime beginDate = new DateTime(2015, 10, 10, 5, 0, 0, DateTimeKind.Utc);

            for (int i = 1; i <= 10; i++)
            {
                DateTime endDate = beginDate.AddDays(7).AddSeconds(-1);

                if (endDate.Date == new DateTime(2015, 11, 7, 0, 0, 0, DateTimeKind.Utc))
                {
                    endDate = endDate.AddHours(1);
                }

                weeks.Add(new Week { Id = i, BeginDate = beginDate, EndDate = endDate });

                beginDate = endDate.AddSeconds(1);
            }

            return weeks;
        }
    }
}

WeekServiceTest:
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;

namespace NodaTimeTest
{
    [TestClass]
    public class WeekServiceTest
    {
        private readonly WeekService weekService = new WeekService();

        [TestMethod]
        public void TestGetNextThreeWeeksOverDaylightTimeChange()
        {
            var result = this.weekService.GetNextWeeks(2, 3);

            Assert.AreEqual(3, result.ElementAt(0).Id);
            Assert.AreEqual(4, result.ElementAt(1).Id);
            Assert.AreEqual(5, result.ElementAt(2).Id);
        }

        [TestMethod]
        public void TestGetNextThreeWeeksWithNoDaylightTimeChange()
        {
            var result = this.weekService.GetNextWeeks(5, 3);

            Assert.AreEqual(6, result.ElementAt(0).Id);
            Assert.AreEqual(7, result.ElementAt(1).Id);
            Assert.AreEqual(8, result.ElementAt(2).Id);
        }
    }
}

2
@MattJohnson:我真的不确定是否应该将这个放在代码审查上。鉴于我们要求人们展示他们迄今为止所做的工作,如果有代码“几乎可以工作,但实际上并不好”,那么这不应该需要去CodeReview。这个问题的基础是“我应该如何实现X?”而不是“请审核我的执行X的代码。”但正如我所说,我有两种想法... - Jon Skeet
2
@MattJohnson "应该在代码审查(Code Review)中讨论" 不是一个有效的关闭理由。 - Quill
2
我认为这个问题适合在代码审查中讨论,但是没错,它也适合在这里讨论。 - Jon Skeet
1
@SuperBiasedMan 在多个网站上重复发布内容通常是不被赞同的。 - Quill
1
@IceBox13,如果你正在寻找对你的代码进行审查的地方,[codereview.se]可能是一个不错的选择,但如果你更想通过Nodatime API或基本方法来实现这个功能,那么Stack Overflow就是你需要去的地方。 - Quill
显示剩余12条评论
1个回答

4
好的,如果我理解正确,我认为您可能需要类似以下内容:

好的,如果我理解正确,我认为您可能需要这样的东西:

var zone = DateTimeZoneProviders.Tzdb["America/Chicago"];
var instantStart = Instant.FromDateTimeUtc(week.BeginDate);
var chicagoStart = instantStart.InZone(zone);
var localEnd = chicagoStart.LocalDateTime.PlusWeeks(weekCount);
var chicagoEnd = localEnd.InZoneLeniently(zone);
var bclEnd = chicagoEnd.ToDateTimeUtc();

var result = repository
    .Fetch(x => x.BeginDate >= week.BeginDate && x.BeginDate < bclEnd)
    .OrderBy(x => x.RetailerWeekNumber)
    .ToList();

请注意,我已将下限设置为包含,将上限设置为不包含 - 这通常是最简单的方法。 如果您真的想这样做,当然可以将所有这些链接在一起:
var zone = DateTimeZoneProviders.Tzdb["America/Chicago"];
var bclEnd = Instant.FromDateTimeUtc(week.BeginDate)
    .InZone(zone)
    .LocalDateTime
    .PlusWeeks(weekCount)
    .InZoneLeniently(zone)
    .ToDateTimeUtc();

编辑:如果您的BeginDate确实是您想要开始获取数据的时间点,那么上述内容就适用。但实际上,您可能想要在此基础上再增加一周。此时应该这样写:

var zone = DateTimeZoneProviders.Tzdb["America/Chicago"];
var instantNow = Instant.FromDateTimeUtc(week.BeginDate);
var chicagoNow = instantStart.InZone(zone);
var localStart = chicagoNow.LocalDateTime.PlusWeeks(1);
var localEnd = localEnd(weekCount);
var bclStart = localStart.InZoneLeniently(zone).ToDateTimeUtc();
var bclEnd = localEnd.InZoneLeniently(zone).ToDateTimeUtc();

var result = repository
    .Fetch(x => x.BeginDate >= bclStart && x.BeginDate < bclEnd)
    .OrderBy(x => x.RetailerWeekNumber)
    .ToList();

这正是我一直在寻找的。昨天当我尝试进行计算时,我添加了几天,但时间变化少了一个小时。看起来添加周数可以更好地处理这种情况。我已经用流畅链的第二个片段替换了我的代码。我必须保持我的lambda表达式不变,以获取除起始周之外的下n周。所有的单元测试都通过了 - 谢谢你的帮助! - IceBox13
1
@IceBox13:如果您将天数添加到“LocalDateTime”中,则应该可以正常工作。如果您确实需要使用独占下限和包含上限,那么这非常令人担忧...我不确定您所说的“排除起始周”的意思,但听起来可能只需首先添加一周(以同样的方式)。 - Jon Skeet
请返回翻译的文本:意思是原来的第一周不应该是结果集的一部分。从第四周开始,给我接下来的三周,这将提供第五、六和七周。你一直在谈论更改表达式的边界,但这不是我所拥有的核心问题。我总是乐于学习更多 - 你有什么文章或原则/模式可以让我了解为什么你对如何编写边界感到如此强烈吗? - IceBox13
2
@IceBox13: 在这种情况下,现有代码已经失效了,因为任何略微超过第4周的东西仍然会被返回。这确实是核心问题的一部分 :) 我会稍微编辑一下答案。我没有关于这个的文章,但也许我应该写一篇... - Jon Skeet
我仍然不确定你所说的“只是一点点”是什么意思,进入第四周。我正在处理的服务方法不允许输入任意日期 - 它以客户引用的周ID作为输入,并要求下一个n个连续周的发生情况。该方法首先获取引用的周,然后使用获取的周的BeginDate作为下一个查询的起点。任何一周的BeginDate都不会“只是一点点”进入这一周。 - IceBox13
1
@IceBox13:你的意思是说,你的数据(以及查询)的每个点都保证对齐在一周的边界上吗?这很奇怪,但这可以解释问题。不过,我会让查询表达出你真正想要表达的意思。例如,如果你正在查询数字,并且想要“至少五个,不超过7个”,你不会说“> 4,<= 6.99999”。你会说“>= 5,< 7”。除此之外,这意味着如果你正在查询的数据中的BeginDate(开始日期)比“周”更精确,你仍然会得到正确的结果。 - Jon Skeet

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接