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

Noda – .NET 的 DateTime 扩展

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.25/5 (4投票s)

2010 年 5 月 1 日

CPOL

5分钟阅读

viewsIcon

38310

downloadIcon

304

Noda – .NET 的 DateTime 扩展

引言

在您开始猜测 Noda 可能代表什么之前:Noda 本身不代表任何东西,它只是试图表达它在概念上接近于流行的 Java 框架 Joda。

背景

.NET 的 DateTime 实现的一个主要缺点是它只对时区有半吊子的感知:如果您创建一个简单标准的 DateTime 对象,该对象将是 DateTimeKind.Local 类型,这意味着它是在机器上定义的本地时区中创建的。

DateTime today = DateTime.Today; // Kind is DateTimeKind.Local

如果您使用默认构造函数创建一个 DateTime 对象,则 DateTimeKind 是未指定的,这意味着它不感知任何时区。

DateTime dateTime = new DateTime(2006, 1, 1); // DateTimeKind.Unspecified

在这两种情况下,您都不能简单地传递 DateTime 对象(甚至不能将其存储在数据库中)而不丢失创建它的时区的重要信息。

考虑以下情况:位于纽约的用户 A 创建一个 DateTime 并将其存储在数据库中。位于哥本哈根的用户 B 以某种方式查询该记录并在本地机器上实例化一个新的 DateTime 对象。用户 B 当然希望看到相对于他所在时区的对象,而不是用户 A 创建该对象时的原始时区时间。

显而易见的解决方案是让时区成为 DateTime 对象的一部分,然后在本地时区不同时进行转换。这将意味着扩展 DateTime 对象以包含时区信息,但也意味着如果您将对象存储在数据库或 XML 中,您需要同时存储 DateTime 和时区才能重新创建对象。

另一个在 .NET 3.5 中引入的解决方案是使用 DateTimeOffset 来表达相对于 UTC 的时间差异。这也对常规 DateTime 对象来说是一个破坏性更改,因为您还需要额外存储偏移量。为此目的,有一个相应的 SQL Server 类型称为 datetimeoffset,更改您的实现将迫使您也更改数据库类型。因此,它似乎并不是 DateTime 的理想替代品,而且它在夏令时方面也有一些其他问题。

如果您查询 BCL 博客关于 DateTime 的信息,您会找到很多关于 DateTime 对象历史的信息。BCL 承认他们在框架的 1.0 版本中可能选择了错误的实现,但现在他们必须保留签名和行为以保持兼容性。

Using the Code

我们的实现试图以一种特定的方式解决这个问题,但我们认为它可能适用于大多数常见场景。

如果您的业务应用程序必须处理不同的时区,您可能(否则我们认为您应该)始终将这些 DateTime 值存储在参考时区中。业务逻辑操作要么在一个特定时区的上下文中进行,要么相对于参考时区进行。显示和解析 DateTime 值也是在一个特定时区的上下文中进行的。

我们引入了一个 DateTime 的替代品,称为 NodaDateTime,它与 DateTime 对象具有相同的签名,但在内部,它将值存储为 UTC 以及一个时区。时区默认为 NodaDateTime.CurrentTimeZone 中定义的时区,该时区内部默认为 TimeZoneInfo.Local,即本地计算机上定义的时区。

NodaDateTime.CurrentTimeZone = 
	TimeZoneInfo.FindSystemTimeZoneById("Romance Standard Time");
            
var dateTime = new NodaDateTime(2006, 3, 21, 18, 0, 0);
Console.Out.WriteLine(dateTime); // 21-03-2006 18:00:00

NodaDateTime.CurrentTimeZone = 
	TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");

Console.Out.WriteLine(dateTime); // 21-03-2006 09:00:00

现在的好处是,所有操作(例如添加小时、分钟等)始终相对于内部 UTC 值进行,并且存储值可以通过 UTC 值完成。

DateTime utc = dateTime.UtcDateTime;

CurrentTimeZone 是线程 static 的,可以在 Web 应用程序中使用,在请求开始时设置时区上下文,以便所有后续操作都相对于该上下文进行。在 WPF 或 Windows Forms 应用程序中使用时,您可以让用户选择他们偏好的 CurrentTimeZone,然后所有输入/输出都将使用提供的时间区进行。

这个方法存在一些问题应该被提及:一个问题是:什么时候两个 NodaDateTime 实例是相等的?通常,人们可能期望同一个对象的两个实例在它们的成员相等时是相等的。在 NodaDateTime 的情况下,这意味着只有当两个日期在同一个时区创建时,它们才是相等的。然而,这并不是您所期望的。当两个实例指向同一时间点时,它们应该是相等的。

var utc = NodaDateTime.FromUtc(new DateTime(2006, 3, 21, 17, 0, 0));

// UTC -8:00
//
var pacificTime = new NodaDateTime(2006, 3, 21, 9, 0, 0, "Pacific Standard Time");

// UTC +1:00
//
var copenhagenTime = new NodaDateTime(2006, 3, 21, 18, 0, 0, "Romance Standard Time");

Assert.AreEqual(utc, pacificTime.ToUtc());
Assert.AreEqual(utc, pacificTime.To(TimeZoneInfo.Utc));
Assert.AreEqual(utc, pacificTime);
Assert.AreEqual(utc, copenhagenTime);
Assert.AreEqual(utc, pacificTime.To("Romance Standard Time"));

这更进一步:只要 DateTime 对象明确了它的创建时区,NodaDateTime 对象也可以与常规 DateTime 对象相等。

Assert.AreEqual(utc, new DateTime(2006, 3, 21, 17, 0, 0, DateTimeKind.Utc));

NodaDateTimeDateTime 都定义了相同的绝对时刻和时间,因此它们是相等的。

另一个问题是,像 Year、Month 等属性应该返回什么?UTC 还是本地值?NodaDateTime 始终在对象创建的时区内返回属性。

var utc = NodaDateTime.FromUtc(new DateTime(2006, 3, 21, 0, 0, 0));

// UTC -8:00
//
var pacificTime = new NodaDateTime(utc, "Pacific Standard Time");

Assert.AreEqual(pacificTime.Year, 2006);
Assert.AreEqual(pacificTime.Month, 3);
Assert.AreEqual(pacificTime.Day, 20);
Assert.AreEqual(pacificTime.Hour, 16);
Assert.AreEqual(pacificTime.Minute, 0);
Assert.AreEqual(pacificTime.Second, 0);

关注点

有多种尝试(例如在此处找到:TimeZoneAwareDateTime.aspx)试图解决 DateTime 局限性带来的问题。对于引用的示例,我们看到了一些缺点。

  • CET/UTC 和本地日期时间的分离对象。

    我们认为没有必要为 CET 或 UTC 等事物设置特定的对象,而是使用一个包含时区信息的原始对象。DateTime 对象是 UTC 的信息仅仅是 DateTime 的一个属性,而不是一个独立的类型。这些对象在定义 DateTime 对象作为特定时间点的引用方面没有区别。

  • 硬编码的时区信息。

    时区信息会随着时间而变化。将它们硬编码在框架中(Joda 框架也是如此)将导致每次更新时区信息时都需要发布新版本。

该框架的其他方面是为 Days、Months 等原始类型提供支持,以及 Interval 等功能。该框架绝对缺乏文档且测试覆盖率不足,但我们决定仍然展示它以收集反馈。

因此,任何反馈都将受到赞赏。

历史

  • V1.0 初始版本
© . All rights reserved.