0

我有可能处于不同时区的用户,我正在寻找确定他们的日子和月份开始的 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;
}

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

我的时区实现似乎是正确的,还是存在会产生某种错误的边缘情况?

感谢您的建议。

4

1 回答 1

5

老实说,您的方法似乎不必要地复杂。

为什么要调用一个参数TheUTCTime,然后创建它的 UTC 版本?它不应该已经有一个KindUTC吗?即使没有,您最好使用DateTime.SpecifyKind- 目前在转换一种方式时会消除秒数,而转换另一种方式则不会...在这两种情况下,您都会消除任何亚秒值。

还:

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

我个人强烈建议您完全避免使用 BCL DateTime。作为主要作者,我完全有偏见,但我至少希望你会发现Noda Time工作起来更愉快......它区分了“没有时间组件的日期”、“没有时间的时间”的概念日期组件”,“没有特定时区的本地日期和时间”和“特定时区的日期和时间”......所以类型系统可以帮助您只做明智的事情。

编辑:如果你真的必须在 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 Time 代码如下所示:

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(这只是一个时间点),只需链接ToInstant每个属性设置器的调用。

于 2013-01-21T17:43:12.323 回答