在UTC计算日的开始和月的开始

本文关键字:开始 UTC 计算 | 更新日期: 2023-09-27 17:55:03

我有可以在不同时区的用户,我正在寻找确定他们的日子和月份开始的UTC值。在一个对象中,我有一个方法来尝试这样做;它看起来像这样:

private void SetUserStartTimesUTC()
{
    DateTime TheNow = DateTime.UtcNow.ConvertUTCTimeToUserTime(this.UserTimezoneID);
    DateTime TheUserStartDateUserTime = TheNow.Date;
    DateTime TheUserStartMonthUserTime = new DateTime(TheNow.Year, TheNow.Month, 1);
    DateTime TheUserEndMonthUserTime = TheUserStartMonthUserTime.AddMonths(1);
    this.UserStartOfDayUTC = TheUserStartDateUserTime.ConvertUserTimeToUTCTime(this.UserTimezoneID);
    this.UserStartOfMonthUTC = TheUserStartMonthUserTime.ConvertUserTimeToUTCTime(this.UserTimezoneID);
    this.UserEndOfMonthUTC = TheUserEndMonthUserTime.ConvertUserTimeToUTCTime(this.UserTimezoneID);
}

这个方法依赖于另外两个在用户时间和UTC时间之间进行转换的扩展方法

public static DateTime ConvertUserTimeToUTCTime(this DateTime TheUserTime, string TheTimezoneID)
{
    TimeZoneInfo TheTZ = TimeZoneInfo.FindSystemTimeZoneById(TheTimezoneID);
    DateTime TheUTCTime = new DateTime();
    if (TheTZ != null)
    {
        DateTime UserTime = new DateTime(TheUserTime.Year, TheUserTime.Month, TheUserTime.Day, TheUserTime.Hour, TheUserTime.Minute, TheUserTime.Second);
        TheUTCTime = TimeZoneInfo.ConvertTimeToUtc(UserTime, TheTZ);
    }
    return TheUTCTime;
}
public static DateTime ConvertUTCTimeToUserTime(this DateTime TheUTCTime, string TheTimezoneID)
{
    TimeZoneInfo TheTZ = TimeZoneInfo.FindSystemTimeZoneById(TheTimezoneID);
    DateTime TheUserTime = new DateTime();
    if (TheTZ != null)
    {
        DateTime UTCTime = new DateTime(TheUTCTime.Year, TheUTCTime.Month, TheUTCTime.Day, TheUTCTime.Hour, TheUTCTime.Minute, 0, DateTimeKind.Utc);
        TheUserTime = TimeZoneInfo.ConvertTime(UTCTime, TheTZ);
    }
    return TheUserTime;
}

现在我已经处理了一段时间的时区问题,我知道时区问题可能会引入一个很难检测到的错误。

我的实现时区似乎是正确的,还是有一个边缘情况,将创建某种off-by- 1的bug?

谢谢你的建议。

在UTC计算日的开始和月的开始

老实说,你的方法似乎太复杂了。

为什么你有一个名为TheUTCTime的参数,然后创建它的UTC版本?不应该已经有UTC的Kind吗?即使它没有,你最好使用DateTime.SpecifyKind -目前当转换一种方式时,你会抹去秒,而转换另一种方式,你不…在这两种情况下,您都可以清除任何次秒值。

:

  • TimeZoneInfo.FindSystemTimeZoneById从不返回null
  • 如果找不到时区,返回new DateTime()(即公元0001年1月1日)似乎是指示错误的一种非常糟糕的方式
  • 在你的转换方法中没有必要有一个局部变量;直接返回调用ConvertTime的结果你的"月底"其实是"下个月的开始";这可能是你想要的,但还不清楚。

我个人强烈建议您完全避免使用BCL DateTime。我完全偏向于成为主要作者,但我至少希望你能发现野田时间更愉快地工作……它分离出"没有时间组件的日期"、"没有日期组件的时间"、"没有特定时区的本地日期和时间"answers"特定时区的日期和时间"的概念……因此,类型系统可以帮助您只做合理的事情。

编辑:如果你真的在BCL类型中这样做,我会这样写:

private void SetUserStartTimesUTC()
{
    DateTime nowUtc = DateTime.UtcNow;
    TimeZoneInfo zone = TimeZoneInfo.FindSystemTimeZoneById(UserTimeZoneID);
    // User-local values, all with a Kind of Unspecified.
    DateTime now = TimeZoneInfo.ConvertTime(nowUtc, zone);
    DateTime today = now.Date;
    DateTime startOfThisMonth = todayUser.AddDays(1 - today.Day);
    DateTime startOfNextMonth = startOfThisMonth.AddMonths(1);
    // Now convert back to UTC... see note below
    UserStartOfDayUTC = TimeZoneInfo.ConvertTimeToUtc(today, zone);
    UserStartOfMonthUTC = TimeZoneInfo.ConvertTimeToUtc(startOfThisMonth, zone);
    UserEndOfMonthUTC = TimeZoneInfo.ConvertTimeToUtc(startOfNextMonth, zone);
}

您所添加的扩展方法实际上并没有提供多少好处,正如您所看到的。

现在,代码提到了一个"注释"——你目前总是假设午夜总是存在并且是明确的。并不是所有时区都是这样。例如,在巴西,当夏令时向前更改时,时间从午夜跳到凌晨1点-所以午夜本身基本上是无效的。

在Noda Time中,我们通过DateTimeZone.AtStartOfDay(LocalDate)来解决这个问题,但这对于BCL来说并不那么容易。

作为比较,等效的Noda时间代码看起来像这样:

private void SetUserStartTimesUTC()
{
    // clock would be a dependency; you *could* use SystemClock.Instance.Now,
    // but the code would be much more testable if you injected it.
    Instant now = clock.Now;
    // You can choose to use TZDB or the BCL time zones
    DateTimeZone zone = zoneProvider.FindSystemTimeZoneById(UserTimeZoneID);
    LocalDateTime userLocalNow = now.InZone(zone);
    LocalDate today = userLocalNow.Date;
    LocalDate startOfThisMonth = today.PlusDays(1 - today.Day);
    LocalDate startOfNextMonth = startOfThisMonth.PlusMonths(1);
    UserStartOfDayUTC = zone.AtStartOfDay(today);
    UserStartOfMonthUTC = zone.AtStartOfDay(startOfThisMonth);
    UserEndOfMonthUTC = zone.AtStartOfDay(startOfNextMonth);
}

…其中属性类型为ZonedDateTime(记住时区)。如果您愿意,可以将它们更改为Instant类型(这只是一个时间点),只需为每个属性setter链接ToInstant调用。