.NET DateTime coding best practices
Author: Dan Rogers (danro), Microsoft Corporation
January 9, 2004
Synopsis
Writing programs that store, perform calculations and serialize time values using the .NET DateTime type requires an awareness of the different issues associated with time representations available in the Windows and .NET platform. This article focuses on key testing and development scenarios involving time, and defines the best practice recommendations for writing programs that use the DateTime type in .NET applications and assemblies.
Background
Many programmers encounter assignments that require them to accurately store and process data that containing date and time information. On first glance, the CLR DateTime data type appears to be perfect for these tasks. It isn’t uncommon, however, for programmers, but more likely testers to encounter cases where a program simply loses track of correct time values. This article focuses on the testing issues associated with logic involving DateTime, and in doing so, uncovers best practices for writing programs that capture, store, retrieve and transmit DateTime information.
What is a DateTime, anyway?
When we look at the documentation found in the .NET Framework class library documentation, we see that “the CLR System.DateTime value type represents dates and times ranging from 12:00:00 midnight, January 1, 00 01 AD to 11:59:59 PM, December 31 99 99 AD.” Reading further we learn, unsurprisingly, that a DateTime value represents an instant at a point in time, and that a common practice is to record point in time values in Coordinated Universal Time (UCT) – more commonly known as Greenwich Mean Time (GMT).
At first glance, then, a programmer discovers that a DateTime type is pretty good at storing time values that are likely to be encountered in current-day programming problems such as business applications. With this confidence, many an unsuspecting programmer begins coding, confident that they can learn as much as they need to about time as they go forward. This “learn as you go” approach can get you into a few snags, so let’s start nailing them down. They range from issues in the documentation to behaviors that need to be factored into your program designs.
The V1.0 and 1.1 documentation for System.DateTime makes a few generalizations that can throw the unsuspecting programmer off track. For instance, the docs currently still say that the methods and properties found on the DateTime class always use the assumption that the value represents the local machine local time zone when making calculations or comparisons. This generalization turns out to be untrue because there are certain types of Date and Time calculations that assume GMT, and others that assume a local time zone view. These areas are pointed out later in this article.
So, let’s get started by exploring the DateTime type by outlining a series of rules and best practices that can help you get your code functioning correctly the first time around.
The Rules:
1. Calculations and comparisons of DateTime instances are only meaningful when the instances being compared or used are representations of points in time from the same time-zone perspective.
2. A developer is responsible for keeping track of time-zone information associated with a DateTime value via some external mechanism. Typically this is accomplished by defining another field or variable that you use to record time-zone information when you store a DateTime value type. This approach (storing the time-zone sense along side the DateTime value) is the most accurate and allows different developers at different points in a programs life-cycle to always have a clear understanding of the meaning of a DateTime value. Another common approach is to make it a “rule” in your design that all time values are stored in a specific time-zone context. This approach saves on the added storage required to save a users view of the time-zone context, but introduces the risk that a time value will be misinterpreted or stored incorrectly down the road by a developer that isn’t aware of the rule.
3. Performing date and time calculations on values that represent machine local time may not always yield the correct result. When performing calculations on time values in time-zone contexts that practice daylight savings time, you should convert values to universal time representations before performing date arithmeti c c alculations. For a specific list of operations and proper time-zone contexts, see the table in the section titled “Sorting out DateTime methods below.
4. A calculation on an instance of a DateTime value does not modify the value of the instance – thus a call to MyDateTime.ToLocalTime() does not modify the value of the instance of the DateTime. The methods associated with the Date (VB) and DateTime (CLR) classes return new instances that represent the result of a calculation or operation.
5. When using .NET framework version 1.0 and 1.1, DO NOT send a DateTime value that represents UCT time thru System.XML.Serialization. This goes for Date, Time and DateTime values. For Web services, and other forms of serialization to XML involving System.DateTime, always make sure that the value in the DateTime value represents current machine local time. The serializer will properly decode an XML Schema defined DateTime value that is encoded in GMT (offset value = 0), but it will decode it to the local machine time viewpoint.
6. In general, if you are dealing with absolute elapsed time, such as measuring a timeout, performing arithmetic, or doing comparisons of different DateTime values, you should try and use a Universal time value if possible so that you get the best possible accuracy without effects of time-zone and/or daylight savings having an impact.
7. When dealing with high level user facing concepts, such as scheduling, and can safely assume that each day has 24 hours from a user’s perspective, it may be ok to counter rule 6 by performing arithmetic, et cetera, on local times.
Throughout this article, this simple list of rules serves as the basis for a set of best practices for writing and testing applications that process dates.
By now, several of you are already looking through your code and saying “oh darn, it’s not doing what I expected it to do” – which is the purpose of this article. For those of us that haven’t had an epiphany at reading this far, let’s take a look at the issues associated with processing DateTime values (from now on, I’ll just shorten this to dates) in .NET applications.
Storage strategies
According to the rules (above), calculations on date values is only meaningful when you understand the time-zone information associated with the date value you are processing. This means that whether you are storing your value temporarily in a class member variable, or choose to save the values you have gathered into a database or file, the programmer is responsible for applying a strategy that allows the associated time-zone information to be understood at a later time.
Coding Best Practice 1
_ Store the time-zone information associated with a DateTime type in an adjunct variable.
_
_
_
An alternative, but less reliable, strategy is to make a steadfast rule that your stored dates will always be converted to a particular time-zone, such as GMT, prior to storage. This may seem sensible, and many teams can make it work. However, the lack of an overt signal that says that a particular DateTime column in a table in a database is in a specific time zone invariably leads to mistakes in interpretation in later iterations of a project.
A common strategy see in an informal survey of different .NET applications is the desire to always have dates represented in universal (GMT) time. I say “desire” because this is not always practical. A case in point arises when serializing a class that has a DateTime member variable via a web service. The reason is that a DateTime value type maps to a XSD:DateTime type (as one would expect) – and the XSD type accommodates representing points in time in any time zone. We’ll discuss the XML case later. More interestingly, a good percentage of these projects weren’t actually achieving their goal, and were storing the date information in the server time-zone without realizing it.
In these cases, an interesting fact is that the testers weren’t seeing time conversion issues, so nobody had noticed that the code that was supposed to convert the local date information to UCT time was failing. In these specific cases, the data was later serialized via XML and was converted properly because the date information was in machine local time to start with.
Let’s look at some code that doesn’t work :
Dim d As DateTime
d = DateTime.Parse("Dec 03, 2003 12:00:00 PM") 'date assignment
d.ToUniversalTime()
The program then takes the value in variable d and saves it to a database – expecting the stored value to represent a UCT view of time. This example recognizes that the Parse method render the result in local time unless some non-default culture is used as an optional argument to the Parse family of methods.
The code above actually fails to convert the value in the DateTime variable d to universal time in the third line because as written, the sample violates rule # 4 (the methods on the DateTime class do not convert the underlying value). Note, that this code was seen in an actual application that had been tested.
How did it pass? The applications involved were able to successfully compare the stored dates because during testing, all of the data was coming from machines set to the same time-zone – so rule #1 was satisfied (dates being compared and calculated all are localized to the same time-zone point of view). The bug in this code is the kind that is hard to spot – a statement that executes but that doesn’t do anything (hint: the last statement in the example is a no-op as written.)
Testing Best Practice 1
_ Check to see that stored values represent the point in time value that you intend in the time-zone you intend _ .
Fixing the code sample is easy:
Dim d As DateTime
d = DateTime.Parse("Dec 03, 2003 12:00:00 PM").ToUniversalTime()
Since the calculation methods associated with the DateTime value type never impact the underlying value, but instead return the result of the calculation, a program must remember to store the converted value (if this is desired, of course). In the next section, we’ll examine how even this seemingly proper calculation can fail to achieve the expected results in certain circumstances involving daylight savings time.
Performing calculations
On first glance, the calculation functions that come with the System.DateTime class are really useful. Support is provided for adding intervals to time values, performing arithmetic on time values, and even converting .NET time values to the corresponding value-type appropriate for Win32 API calls as well as OLE Automation calls. A look at the support methods that surround the DateTime type evokes a nostalgic look back at the different ways that DOS and Windows have evolved for dealing with time and timestamps over the years.
The fact that all of these components are still present in various parts of the OS is related to the backwards compatibility requirements that Microsoft has taken on. To a programmer, this means that if you are moving data representing timestamps on files, directories, or doing COM/OLE interop involving date and DateTime values, you’ll have to become proficient at dealing with conversions between the different generations of time that are present in Windows.
Don’t get fooled again
Let’s suppose you have adopted the “we store everything in UCT time” strategy – presumably to avoid the overhead of having to store a time-zone offset (and perhaps as well a user-eyed view of time-zone such as Pacific Standard Time, or PST). There are several advantages to performing calculations using UCT time. Chief among them is the fact that when represented in universal time, every day has a fixed length, and there are no time-zone offsets to deal with.
If you were surprised reading that a day can have different lengths, be aware that in any time-zone that allows for daylight savings time, on two days of the year (typically) days have a different length. So even if you are using a local time value, such as Pacific Standard Time (PST), if you try and add a span of time to a specific DateTime instance value, you may not get the result you thought you should if interval being added takes you past the change-over time on a date that daylight savings time either starts or ends.
Let’s look at an example of code that doesn’t work in the Pacific Time zone in the United States :
Dim d As DateTime
d = DateTime.Parse("Oct 26, 2003 12:00:00 AM") 'date assignment
d = d.AddHours(3.0)
' - displays 10/26/2003 03:00:00 AM – an ERROR!
MsgBox(d.ToString)
The result that is displayed from this calculation may seem correct on first glance; however, on October 26, 2003 , one minute after 1:59 AM PST, the daylight savings time change took effect. The correct answer should have been 10/26/2003 , 02:00:00 AM, so this calculation based on a local time value failed to yield the correct result. But if we look back at Rule # 3, we seem to have a contradiction – but it isn’t. Let’s just call it a special case for using the Add/Subtract methods in time-zones that celebrate daylight savings time.
Coding Best Practice 2
_ Be careful if you need to perform DateTime calculations (add/subtract) on values that represent time-zones that practice daylight savings time. Unexpected calculation errors can result. Instead, convert the local time value to universal time, perform the calculation, and convert back to achieve maximum accuracy _ .
Fixing this broken code is straight forward:
Dim d As DateTime
d = DateTime.Parse("Oct 26, 2003 12:00:00 AM") 'date assignment
d = d.ToUniversalTime().AddHours(3.0).ToLocalTime()
' - displays 10/26/2003 02:00:00 AM – Correct!
MsgBox(d.ToString)
The easiest way to reliably add spans of time reliably is to convert local time based values to universal time, perform the calculation, and then convert the value back.
Sorting out DateTime methods
Throughout this article, different System.DateTime class methods are discussed. Some yield a correct result when the underlying instance represent local time, some when they represent Universal time, and others still require no underlying instance at all. Further, some are agnostic to timezone completely (e.g. AddYear, AddMonth) To simplify the overall understanding of the assumptions behind the most commonly encountered DateTime support methods, the following table is provided.
To read the table, consider the starting (input) and ending (returned value) viewpoint. In all cases, the end state of calling a method is returned by the method. No conversion is made to the underlying instance of data. Caveats that describe exceptions or useful guidance are also provided.
Method Name
|
Starting Viewpoint
|
Ending Viewpoint
|
Caveats
---|---|---|---
ToUniversalTime
|
Local Time
|
UTC
|
Do not call on a DateTime instance that already represents Universal Time
ToLocalTime
|
UTC
|
Local Time
|
Do not call on an DateTime instance that already represents local time
ToFileTime
|
Local Time
|
|
Method returns an INT64 that represents Win32 file time (UCT time)
FromFileTime
|
|
Local Time
|
Static Method – no instance required. Takes a INT64 UCT time as input.
ToFileTimeUtc
_ (V1.1 only)
_
|
UTC
|
|
Method returns a INT64 that represents a Win32 file time (UCT time)
FromFileTimeUtc
_ (V1.1 only)
_
|
|
UTC
|
Method converts INT64 Win32 file time to a DateTime UCT instance.
Now
|
|
Local Time
|
Static Method – no instance required. Returns a DateTime that represents the current time in Local machine time.
UtcNow
|
|
UTC
|
Static Method – no instance required
IsLeapYear
|
Local Time
|
|
Returns Boolean that indicates true if year portion of the local time instant is a leap year.
Today
|
|
Local Time
|
Static Method – no instance required. Returns a DateTime set to Midnight of the current day in local machine time.
The special case of XML
Several of the people I’ve talked to recently had the design goal of serializing time values over web services such that the XML that represents the DateTime would be formatted in GMT – e.g. with a zero offset. While I’ve heard various reasons, ranging from the desire to simply parse the field as a text string for display in a client, to wanting to preserve the “stored in UCT” assumptions that exist on the server all the way across to the callers of web services, I’ve not been convinced that there is ever a good reason to control the marshalling format on the wire to this degree. Why? Simply because the XML encoding for a DateTime type is perfectly adequate for representing an instant in time, and the XML serializer that is built into the .NET framework does a fine job of managing the serialization and deserialization issues associated with time values.
Further, it turns out that forcing the System.XML.Serialization serializer to encode a date value in GMT on the wire is not possible in .NET – at least not today. As a programmer, designer, or project manager, your job then becomes making sure that the data that is being passed in your application is performed accurately with a minimum of cost.
Several of the groups I talked with in the research that went into this paper had adopted the strategy of defining special classes and writing their own XML serializers so that they have full control over what the DateTime values on the wire in their XML looked like. While I admire the pluck that developers have when making the leap into this brave undertaking, rest assured that the nuances of dealing with daylight savings time and time zone conversion issues alone should make a good manager say “No Way”. Especially when the mechanisms provided in the .NET framework do a perfectly accurate job of serializing time values already.
There is only one trick you have to be aware of – and as a designer you MUST understand this (see rule 5) and adhere to the rule.
Code that doesn’t work :
Let’s first define a simple XML class with a DateTime member variable. For completeness, this class is the simplified equivalent of the recommended approach illustrated later in the article.
1<xmltype(typename:="timetestdef", )="" _="" namespace:="http://tempuri.org/Timetester.xsd">), _
2
3XmlRoot(), Serializable()> _
4
5Public Class timeTestDef
6
7Private __timeVal As DateTime
8
9<xmlignore()> _
10
11Public timeValSpecified As Boolean
12
13<xmlelement(elementname:="timeval", )="" ,="" _="" datatype:="dateTime" form:="XmlSchemaForm.Qualified," isnullable:="False" namespace:="http://tempuri.org/Timetester.xsd"> _
14
15Public Property timeVal() As DateTime
16
17Get
18
19timeVal = __timeVal
20
21End Get
22
23Set ( ByVal Value As DateTime)
24
25__timeVal = Value
26
27timeValSpecified = True
28
29End Set
30
31End Property
32
33End Class
34
35Now, let’s use this class to write out some XML to a file...
36
37' write out to the file
38
39Dim t As Xml.XmlTextWriter
40
41Dim ser As XmlSerializer
42
43Dim tt As New timeTest ' a class that has a DateTime variable
44
45' set the fields in your class
46
47tt.timeVal = DateTime.Parse("12/12/2003 12:01:02 PM")
48
49tt.timeVal = tt.TimeVal.ToUniversalTime()
50
51' get a serializer for the root type, and serialize this UTC time
52
53ser = New XmlSerializer( GetType (timeTest))
54
55t = New Xml.XmlTextWriter("c:\timetest.xml", System.Text.Encoding.UTF8)
56
57ser.Serialize(t, tt)
58
59t.Close()
60
61t = Nothing
62
63tt = Nothing
64
65When this code runs, the XML that is serialized to the output file contains an XML DateTime representation as follows:</xmlelement(elementname:="timeval",></xmlignore()></xmltype(typename:="timetestdef",>