Introduction
Before you start wondering what Noda might stand for: Noda stands for nothing, except that it tries to express its conceptional closeness to the popular Java Framework called Joda.
Background
One of the major disadvantages of the .NET DateTime
implementation is that it is only half-aware of the time zone: if you create a simple and standard DateTime
object, the object is of DateTimeKind.Local
which means, it has been created in the local time zone defined on the machine.
DateTime today = DateTime.Today;
If you create a DateTime
object using its default constructor, then the DateTimeKind
is unspecified meaning that it’s unaware of any time zone:
DateTime dateTime = new DateTime(2006, 1, 1);
In both cases, you cannot just simply pass the DateTime
objects around (even not storing it in a database) without losing the important information of the time zone it has been created in.
Consider the following case: User A, situated in New York, creates a DateTime
and stores it in the database. User B, in Copenhagen, somehow queries the record and instantiates a new DateTime
object locally on his machine. User B would certainly expect to see the object relative to his time zone and not the original time of the zone User A has created the object.
The obvious solution would be to let the time zone be part of the DateTime
object and then convert it whenever the local time zone is different. This would mean to extend the DateTime
object with the time zone information but would also mean that if you store the object in a database or an XML, you would need to store both, DateTime
and time zone to be able to recreate the object.
Another solution introduced in .NET 3.5 with the DateTimeOffset
is to express the differences relative in time as an offset to UTC. This also is a breaking change to the regular DateTime
object since you additionally need to store the offset. There is a corresponding SQL-Server type for such purpose called datetimeoffset
and changing your implementation would force you to change database types too. Therefore it didn’t seem to be an ideal candidate for a DateTime
replacement, also because it has some other problems with daylight savings.
If you query the BCL blog regarding DateTime
, you’ll find a lot of information about the history of the DateTime
object. The BCL acknowledge that they might have chosen the wrong implementation in version 1.0 of the framework but they are now caught to keep the signature and behavior to retain compatibility.
Using the Code
Our implementation tries to address the issue in a specific way but we think, it might be suitable for the most common scenarios:
If your business application has to deal with different time zones, you might (otherwise we think you should) have stored those DateTime
values always in a reference time zone. Business logic operations are either done in a context of a specific time zone or relative to the reference time zone. Displaying and parsing DateTime
values is also done in a context of a specific time zone.
We have introduced a DateTime
replacement called NodaDateTime
which has the same signature as the DateTime
object but internally, it stores its value as UTC together with a time zone. The time zone defaults to the time zone defined as NodaDateTime.CurrentTimeZone
which internally defaults to TimeZoneInfo.Local
as the time zone defined on the local machine:
NodaDateTime.CurrentTimeZone =
TimeZoneInfo.FindSystemTimeZoneById("Romance Standard Time");
var dateTime = new NodaDateTime(2006, 3, 21, 18, 0, 0);
Console.Out.WriteLine(dateTime);
NodaDateTime.CurrentTimeZone =
TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
Console.Out.WriteLine(dateTime);
The benefit now is that all operations like adding hours, minutes, etc. are always done relative to the internal UTC value and storing the value can be done via the UTC value:
DateTime utc = dateTime.UtcDateTime;
The CurrentTimeZone
is made thread static
to be used in web applications to set the time zone context at the beginning of the request so that all further operations are done relative to the context. When using it in a WPF or Windows Forms application, you can let the user select the CurrentTimeZone
he or she prefers to work in and all input/output will then be done with the time zone provided.
There are some issues around this approach which should be mentioned: one question is: when are two instance of NodaDateTime
equal to each other? Normally, one might expect that two instances of the same object are equal when their members are equal. In case of NodaDateTime
, it would mean that two dates are only equal if they are created within the same time zone. However, this is not what you would expect. Two instances should be equal when they relate to the same moment in time.
var utc = NodaDateTime.FromUtc(new DateTime(2006, 3, 21, 17, 0, 0));
var pacificTime = new NodaDateTime(2006, 3, 21, 9, 0, 0, "Pacific Standard Time");
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"));
This goes even further: A NodaDateTime
object can also be equal to a regular DateTime
object as long as the DateTime
object makes clear in which time zone it was created:
Assert.AreEqual(utc, new DateTime(2006, 3, 21, 17, 0, 0, DateTimeKind.Utc));
Both, NodaDateTime
and DateTime
are defining the same absolute moment and time and hence, they are equal.
Another issue is, what should the properties such as Year, Month, etc. return? The Uct or the local value? The NodaDateTime
always returns the properties within the time zone the object has been created in:
var utc = NodaDateTime.FromUtc(new DateTime(2006, 3, 21, 0, 0, 0));
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);
Points of Interest
There are multiple attempts (found f.i. here: TimeZoneAwareDateTime.aspx) which are trying to address the issues around the limitations of DateTime
. As for the referenced example, we see some disadvantages:
- Separate objects for CET/UTC and local date time
We don’t see any need for having specific objects for UTC or CET, etc. while having a primitive object that holds time zone information. The information that a DateTime
object is UCT is merrily a property of the DateTime
rather than a separate type. Those objects don’t differ in terms of definition of a DateTime
object as being a reference to a specific moment in time
- Coded time zone information
Time zone information is subject to changes over time. Hardcoding them in a framework (as also done in the Joda framework) will result in the need for releasing new versions whenever the time zone information is updated.
Other aspects of the framework are to provide support for primitive types such as Days, Months, etc. as well as features as the Interval. The framework is definitely missing documentation and lacks test coverage, but we decided to show it anyway to gather feedback.
So, any feedback is appreciated.
History