65.9K
CodeProject 正在变化。 阅读更多。
Home

一个时区感知的 DateTime 实现

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.56/5 (10投票s)

2008 年 12 月 7 日

CPOL

6分钟阅读

viewsIcon

72308

downloadIcon

486

一个包装 DateTime 的实现, 用于跟踪时区状态

引言

本文重点介绍了我如何创建一个支持时区的 DateTime 包装器。我不敢声称自己是该领域的专家,但我花了数周时间研究这个主题,并且我认为我理解了一些我们需要解决的复杂问题。今天在使用 .NET DateTime 实例时面临的最大问题是,默认情况下它们表示本地时间,并且不包含有关它们最初创建时所在时区的任何信息。处理在非本地时区创建的 DateTime 实例似乎非常困难和尴尬。一个常见的变通方法是将值转换为 UTC,同时附带一些时区信息、偏移量,以及在序列化信息(到磁盘、数据库或网络)之前,考虑是否使用了夏令时。这些是该库试图解决的一些问题。

当然,此库支持将 DateTime 实例转换为 UTC。这是目前将日期和时间数据持久化到数据库的标准方法。检索这些 DateTime 实例时,通常会连同所有元数据(如偏移量、时区定义以及指示是否使用夏令时的标志)一起将 UTC 日期和时间发送到某个工厂,从而能够生成与请求时区对应的正确 LocalDateTime 实例。然后,LocalDateTime 的实例将在应用程序的其余部分内部公开,以简化日期和时间的处理。这正是我目前工作的地方正在做的事情,我们面临的挑战是,一些用例和报告需要在同一个报告(视图)中同时处理代表不同时区的不同 DateTime 实例,并且对我们的客户来说,确保此逻辑正确工作至关重要。

背景

如果您开始在网上搜索 DateTime 实现,您会遇到以下 URL

基本上,所有这三个 BCL 博客条目都试图解释 DateTime 为什么会变成现在这样,并且他们理解许多 API 消费者并不满意。他们还深入探讨了如何在 .NET 3.5 中通过新的日期时间结构 DateTimeOffset 来弥补这一状况。阅读博客评论非常有意思,因为大多数评论都表明用户对当前的方法不满意。有趣的是,新的 DateTimeOffset 不包含时区信息,只包含一个 TimeSpan 来表示偏移量。因此,这并不能解决透明地表示本地时间并在世界其他地方将其作为本地时间检索的问题。此外,在从一个时区转换为另一个时区时,需要 DST 规则。如果不包含时区信息,处理序列化的 DateTimeOffset 的接收者就必须对此进行猜测。现在,大多数开发人员会像前面描述的那样解决这个问题。但再次问,他们使用 .NET 3.5 提供的新结构是否比使用 DateTime 时更好?

另外,您可以编写一些代码来查找时区服务或 tz-database,以使您的时区定义保持最新。这与 Java 在 JRE 中内部捆绑时区的方式非常相似。查看 TimeZone 类的实现,了解如何做到这一点。

Using the Code

现在,让我们看看 LocalDateTime 实现的工作原理。我提供了许多构造函数;一些构造函数从操作系统派生时区元数据并标识要使用的 TimeZone 实例。但是,如果您想创建根植于另一个时区的 DateTime 实例,则应使用下面的构造函数

/// <summary>
/// Convenient constructor for creating a local date time instance in any time zone.
/// </summary>
/// <remarks>
/// Note that this constructor will not modify provided
/// date time value if provided time is within DST.
/// This should be corrected by the invoker,
/// or if the DateUtil.TimeZoneAdjustDateTime method is used,
/// this will be corrected automatically.
/// </remarks>
/// <param name="time">The time.</param>
/// <param name="zone">The time zone.</param>
public LocalDateTime(DateTime time, ITimeZone zone, bool isDST) : this()
{
    IsLocalTimeBased = false;
    ticks = time.Ticks;
    timeZoneId = zone.CanonicalID;
    this.isDST = isDST;
    NullInitSummerWinterDST();
}

例如,使用此构造函数将如下所示

LocalDateTime cetDateTime = 
  new LocalDateTime(new DateTime(2008, 12, 1), TimeZones.CET, false);

我使用了 Reflector 分析 System.DateTime 来创建一个包含所有 public 实例方法的 IDateTime 接口。这里只是该接口的一小部分摘录

/// <summary>
/// DateTime interface wrapping all methods
/// on DateTime so that you can interact with
/// an date time instance just as if it was a normal date time.
/// </summary>
public interface IDateTime
{
    #region Properties
    /// <summary>
    /// Gets the date component of this instance.
    /// </summary>
    IDateTime Date { get; }

    /// <summary>
    /// Gets the day of the month represented by this instance.
    /// </summary>
    int Day { get; }

    /// <summary>
    /// Gets the day of the week represented by this instance.
    /// </summary>
    DayOfWeek DayOfWeek { get; }

    /// <summary>
    /// Gets the day of the year represented by this instance.
    /// </summary>
    int DayOfYear { get; }

    /// <summary>
    /// Gets the hour component of the date represented by this instance.
    /// </summary>
    int Hour { get; }

    /// <summary>
    /// Gets a value that indicates whether the time represented
    /// by this instance is based on local time,
    /// Coordinated Universal Time (UTC), or neither.
    /// </summary>
    DateTimeKind Kind { get; }

    /// <summary>
    /// Gets the milliseconds component
    /// of the date represented by this instance.
    /// </summary>
    int Millisecond { get; }

    /// <summary>
    /// Gets the minute component of the date represented by this instance.
    /// </summary>
    int Minute { get; }

    /// <summary>
    /// Gets the month component of the date represented by this instance.
    /// </summary>
    int Month { get; }

    /// <summary>
    /// Gets a System.DateTime object that is set to the current date
    /// and time on this computer, expressed as the local time.
    /// </summary>
    IDateTime Now { get; }

该接口当然在 LocalDateTime 上实现,以便 API 程序员习惯的函数对 LocalDateTime 可用。该类型还实现为不可变结构,以尽可能接近 DateTime 的行为。因此,上面创建的实例也可以是

IDateTime cetDateTime = 
  new LocalDateTime(new DateTime(2008, 12, 1), TimeZones.CET, false);

创建一亿个日期时间实例与创建一亿个 LocalDateTime 实例相比,如果使用需要派生时区信息的构造函数,创建 LocalDateTime 实例会产生额外的开销。但是,通过使用为 MSSQL 设计的构造函数或应用程序内部生成 LocalDateTime 的工厂,没有明显的时间差异(创建一亿个实例时,DateTime 结构快了 600 毫秒)。

/// <summary>
/// Internal constructor to be used
/// by SQL-server when creating an instance of the class
/// based on internally stored type value.
/// </summary>
/// <param name="ticks">The number of ticks that makes up a date</param>
/// <param name="timeZoneId">The CanonicalId as defined
///         by the Olson TZ-database for a given time zone.</param>
/// <param name="isDST">A stored value indicating
///       whether the ticks value is within a DST range.</param>
public LocalDateTime(long ticks, string timeZoneId, bool isDST) : this()
{
    this.ticks = ticks;
    this.timeZoneId = timeZoneId;
    this.isDST = isDST;
}

此构造函数还揭示了如果您不将 LocalDateTime 用作用户定义类型,则必须创建的列。通过使用用户定义类型,这些值将与结构实例一起存储在单个列中。为了实现这一点,我不得不向结构添加一些属性,并实现可空接口

[Serializable]
[SqlUserDefinedType(Format.Native)]
public struct LocalDateTime : IDateTime, INullable
{
....

下面是一个通过的单元测试,演示了 LocalDateTime 实现是支持时区的

/// <summary>
/// First create a LocalDateTime in NY-time at 01:59:59AM,
/// one second before the timezone's summer time ends.
/// Due to different DST changes, converting to CET
/// will change the summer time to winter time 
/// for that exact point in time since CET ends
/// summer time before America/New York timezone does. 
/// While in CET time, add one second and convert
/// that LocalDateTime instance back into New York time. 
/// The time should now NOT be 2 AM, but actually 1 AM as the end of DST rule for 
/// America/New York timezone is to set the clock 1 hour back.
/// </summary>
[TestMethod]
public void TestAmerica_NewYorkEndDSTByConvertingToCETAndAddingOneSecondAndConvertingBack()
{
    // Get DST change day
    TimeZone tzNewYork = TimeZones.America__New_York;
    DaylightSavingTime winter = tzNewYork.WinterChange;

    int dayInMonth = DateUtil.FindDayInMonth(2009, winter.Month, 
                     winter.GetDayOfWeek(), winter.GetDayOccurrenceInMonth());
    DateTime winterDateTime = new DateTime(2009, winter.Month, dayInMonth);

    // Set time to 01:59 AM
    TimeSpan oneSecondBefore2AM = new TimeSpan(1, 59, 59);
    DateTime dateTime = winterDateTime.Add(oneSecondBefore2AM);

    // Create a LocalDateTime to represent this time with
    // TimeZone information specified, one second before DST ends
    LocalDateTime ldt = new LocalDateTime(dateTime, tzNewYork, true);

    // Assert that the dateTime instance is equal to localDateTime instance
    Assert.AreEqual(ldt.GetDateTime(), dateTime);

    // Assert that the localDateTime instance's DST flag is true
    Assert.IsTrue(ldt.IsDaylightSavingTime());

    LocalDateTime cet = LocalDateTime.ConvertTo(ldt, TimeZones.CET);

    // Add one second to both the dateTime instance and the cet instance
    dateTime = dateTime.AddSeconds(1);
    IDateTime localDateTime = cet.AddSeconds(1);

    //Transform the manipulated CET back to New York time:
    ldt = LocalDateTime.ConvertTo((LocalDateTime)localDateTime, ldt.TimeZone);

    // Assert that these are not equal as the dateTime instance
    // should be 02:00 AM while the localDateTime instance should be 01:00 AM
    Assert.AreNotEqual(ldt.GetDateTime(), dateTime);

    // Subtract one hour to the dateTime instance so that
    // it will be 01:00 AM and assert that these instances now are equal.
    Assert.AreEqual(ldt.GetDateTime(), dateTime.AddHours(-1));

    // Assert that localDateTime instance's DST flag now is set to false
    Assert.IsFalse(ldt.IsDaylightSavingTime());
}

由于我喜欢使用 NHibernate 作为我的对象关系映射器,所以我创建了一个 CompositeUserType,以便可以使用常规的 NHibernate 映射来保存它。这两种方法(UDT 和多列方法)都适用。

这是这个小型库的第一个版本。它可能包含错误,并且很可能有很多需要改进的地方。如果您有任何反馈,请联系我,以便我改进这个小库。

政策变更

另一个值得研究的方法(我们目前已放弃)是将 LocalDateTime 结构注册为您喜欢的数据库中的“用户定义类型”(UDT)。这至少可以加快数据检索速度,因为您不必将 UTC 时间转换为偏移时间值,以表示它应该在哪种时区下显示。通过使用用户定义类型,您通常用于存储的三个或四个列现在可以表示为单个 LocalDateTime 类型的列。

经过一番讨论,我们决定不使用 MSSQL 中的 UDT。最终,如果我们发现需要重新注册 UDT,可能会给升级客户带来额外的负担。此外,只要 LocalDateTime 数据以已定义时区的本地时间表示,基于日期和时间的排序和排序将不起作用。至少,如果使用 UDT,LocalDateTime 应该将日期时间转换为 UTC 并将其存储为 UTC,以便在 DBMS 中进行一致的 SQL 操作。尽管我们认为这些问题是缺点,以至于我们最终采用了“三列方法”,但这并不意味着 UDT 可能不适合其他应用程序。

历史

添加了单元测试项目和更多测试,演示了 LocalDateTime 的工作原理。

© . All rights reserved.