以UTC格式计算开始日期和月初

时间:2013-01-21 17:37:39

标签: c# timezone

我的用户可以位于不同的时区,我希望确定他们的日期和月份的开头的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;
}

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

我的时区实现是否正确或是否存在会产生某种错误的边缘情况?

感谢您的建议。

1 个答案:

答案 0 :(得分:4)

说实话,你的方法似乎不必要复杂。

为什么你会有一个名为TheUTCTime的参数,然后创建它的UTC版本?它不应该已经拥有UTC的Kind吗?即使它没有,你最好还是使用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调用。