Coding Best Practices Using DateTime in the .NET Framework
Dan Rogers
Microsoft Corporation
February 2004
Applies to
Microsoft® .NET Framework
Microsoft® ASP.NET Web Services
XML serialization
Summary: Writing programs that store, perform calculations, and serialize time values using the DateTime type in the Microsoft .NET Framework requires an awareness of the different issues associated with time representations available in Windows and .NET. 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 Microsoft .NET-based applications and assemblies. (18 printed pages)
Contents
Background
What Is a DateTime, Anyway?
The Rules
Storage Strategies
Best Practice #1
Best Practice #2
Performing Calculations
Don't Get Fooled Again
Best Practice #3
Sorting Out DateTime Methods
The Special Case of XML
Best Practice #4
Best Practice #5
The Class Coders Quandary
Best Practice #6
Dealing with Daylight Savings Time
Best Practice #7
Formatting and Parsing User-Ready Values
Future Consideration
Issues with the DateTime.Now() Method
Best Practice #8
A Couple of Little Known Extras
Conclusion
Background
Many programmers encounter assignments that require them to accurately store and process data that contain date and time information. On first glance, the common language runtime (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 issues associated with logic involving DateTime, and in doing so, uncovers best practices for writing and testing programs that capture, store, retrieve, and transmit DateTime information.
What Is a DateTime, Anyway?
When we look at 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, 0001 AD to 11:59:59 PM, December 31 9999 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 in 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 lead you into a few problems, so let's start identifying them. 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 documentation currently still says 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
- 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.
- 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 alongside the DateTime value) is the most accurate and allows different developers at different points in a program's lifecycle 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 does not require additional storage to save a user's 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.
- 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 arithmetic calculations. For a specific list of operations and proper time-zone contexts, see the table in the Sorting out DateTime Methods section.
- 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 (in Visual Basic®) and DateTime (in the .NET CLR) classes return new instances that represent the result of a calculation or operation.
- When using the .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.
- 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.
- When dealing with high-level, user-facing concepts such as scheduling, and you can safely assume that each day has 24 hours from a user's perspective, it may be okay 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 from 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-based applications.
Storage Strategies
According to the rules above, calculations on date values are 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 choosing to save the values you have gathered into a database or file, you as the programmer are responsible for applying a strategy that allows the associated time-zone information to be understood at a later time.
Best Practice #1
When coding, 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 seen in an informal survey of different .NET-based 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 above 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 renders the result in local time unless some non-default culture is used as an optional argument to the Parse family of methods.
The previously shown code 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: 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 (all dates being compared and calculated 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).
Best Practice #2
When testing, check to see that stored values represent the point-in-time value 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). Next 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 MS-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 operating system is related to the backwards-compatibility requirements that Microsoft maintains. 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 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 the 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 we don't. Let's just call it a special case for using the Add/Subtract methods in time zones that celebrate daylight savings time.
Best Practice #3
When coding, be careful if you need to perform DateTime calculations (add/subtract) on values representing 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 straightforward:
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 is to convert local-time-based values to universal time, perform the calculations, and then convert the values back.
Sorting Out DateTime Methods
Throughout this article, different System.DateTime class methods are discussed. Some yield a correct result when the underlying instance represents local time, some when they represent Universal time, and others still require no underlying instance at all. Further, some are completely agnostic to time zone (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 instance 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 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 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 looked like in their XML. 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 and adhere to the rule (see Rule #5).
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.
<XmlType(TypeName:="timeTestDef", _
Namespace:= "http://tempuri.org/Timetester.xsd")>), _
XmlRoot(), Serializable()> _
Public Class timeTestDef
Private __timeVal As DateTime
<XmlIgnore()> _
Public timeValSpecified As Boolean
<XmlElement(ElementName:="timeVal", IsNullable:=False, _
Form:=XmlSchemaForm.Qualified, DataType:="dateTime", _
Namespace:="http://tempuri.org/Timetester.xsd")> _
Public Property timeVal() As DateTime
Get
timeVal = __timeVal
End Get
Set(ByVal Value As DateTime)
__timeVal = Value
timeValSpecified = True
End Set
End Property
End Class
Now, let's use this class to write some XML to a file.
' write out to the file
Dim t As Xml.XmlTextWriter
Dim ser As XmlSerializer
Dim tt As New timeTest ' a class that has a DateTime variable
' set the fields in your class
tt.timeVal = DateTime.Parse("12/12/2003 12:01:02 PM")
tt.timeVal = tt.TimeVal.ToUniversalTime()
' get a serializer for the root type, and serialize this UTC time
ser = New XmlSerializer(GetType(timeTest))
t = New Xml.XmlTextWriter("c:\timetest.xml", System.Text.Encoding.UTF8)
ser.Serialize(t, tt)
t.Close()
t = Nothing
tt = Nothing
When this code runs, the XML that is serialized to the output file contains an XML DateTime representation as follows:
<timeVal>2003-12-12T20:01:02.0000000-08:00</timeVal>
This is an error: the value encoded in the XML is off by eight hours! Since this happens to be the time zone offset of my current machine, we should be suspicious. Looking at the XML itself, the date is right, and the 20:01:02 date corresponds to the clock time in London for my own noontime, but the offset portion is not correct for a London-based clock. When the XML looks like the London time, the offset should also represent the London viewpoint, which this code doesn't achieve.
The XML serializer always assumes that DateTime values being serialized represent local machine time, so it applies the machine local time zone offset as the offset portion of the encoded XML time. When we deserialize this onto another machine, the original offset is subtracted from the value being parsed, and the current machine's time-zone offset is added.
When we start with a local time, the result of serialization (encode to XML DateTime followed by decode to local machine time) is always correct—but only if the starting DateTime value being serialized represents local time when serialization begins. In the case of this broken code example, we had already adjusted the DateTime value in the timeVal member variable to UCT time, so when we serialize and deserialize, the result is off by the number of hours equal to the time-zone offset of the originating machine. This is bad.
Best Practice #4
When testing, calculate the value you expect to see in the XML string that is serialized using a machine local time view of the point in time being tested. If the XML in the serialization stream differs, log a bug!
Fixing this code is simple. Comment out the line that calls ToUniversalTime().
Best Practice #5
When writing code to serialize classes that have DateTime member variables, the values must represent local time. If they do not contain local time, adjust them prior to any serialization step, including passing or returning types that contain DateTime values in Web services.
The Class Coders Quandary
Earlier we looked at a pretty unsophisticated class that exposed a DateTime property. In that class, we simply serialized what we stored in a DateTime, without regard to whether the value represented a local or universal time viewpoint. Let's look at a more sophisticated approach that offers programmers an overt choice as to what time-zone assumptions they desire, while always serializing properly.
When coding a class that will have a member variable of type DateTime, a programmer has a choice of making the member variable public or writing the property logic to wrap the member variable with get/set operations. Choosing to make the type public has several disadvantages that, in the case of DateTime types, can have consequences that are not under the class developer's control.
Using what we learned so far, consider instead providing two properties for each DateTime type.
The following example illustrates the recommended approach to managing DateTime member variables:
<XmlType(TypeName:="timeTestDef", _
Namespace:= "http://tempuri.org/Timetester.xsd")>), _
XmlRoot(), Serializable(), _
EditorBrowsable(EditorBrowsableState.Advanced)> _
Public Class timeTestDef
Private __timeVal As DateTime
<XmlIgnore()> _
Public timeValSpecified As Boolean
<XmlElement(ElementName:="timeVal", IsNullable:=False, _
Form:=XmlSchemaForm.Qualified, DataType:="dateTime", _
Namespace:="http://tempuri.org/Timetester.xsd")> _
Public Property timeVal() As DateTime
Get
timeVal = __timeVal.ToLocalTime()
End Get
Set(ByVal Value As DateTime)
__timeVal = Value.ToUniversalTime()
timeValSpecified = True
End Set
End Property
<XmlIgnore()> _
Public Property timeValUTC() As DateTime
Get
timeValUTC = __timeVal
End Get
Set(ByVal Value As DateTime)
__timeVal = Value
timeValSpecified = True
End Set
End Property
End Class
This example is the corrected equivalent to the prior class serialization example. In both class examples (this one and the earlier one), the classes are implementations that are described with the following schema:
<?xml version="1.0" encoding="utf-8" ?>
<xs:schema id="Timetester"
targetNamespace="http://tempuri.org/Timetester.xsd"
elementFormDefault="qualified"
xmlns="http://tempuri.org/Timetester.xsd"
xmlns:mstns="http://tempuri.org/Timetester.xsd"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="timeTest" type="timeTestDef"/>
<xs:complexType name="timeTestDef">
<xs:sequence>
<xs:element name="timeVal" type="xs:dateTime"/>
</xs:sequence>
</xs:complexType>
</xs:schema>
In this schema, and in any class implementations, we define a member variable that represents an optional time value. In our recommended example, we have provided two properties with both getters and setters—one for the universal time and one for local time. The angle-bracketed attributes that you see in the code tell the XML serializer to use the local time version for serialization, and generally make the class implementation result in schema-compliant output. To make the class properly deal with the optional lack of expression when no value is set in the instance, the timeValSpecified variable and associated logic in the property setter controls whether the XML element is expressed at serialization time or not. This optional behavior exploits a feature in the serialization subsystem that was designed to support optional XML content.
Using this approach to managing DateTime values in your .NET classes gives you the best of both worlds—you get storage access based on universal time so that calculations are accurate, and you get proper serialization of local time views.
Best Practice #6
When coding, make DateTime member variables private and provide two properties for manipulating your DateTime members in either local or universal time. Bias the storage in the private member as UCT time by controlling the logic in your getters and setters. Add the XML serialization attributes to the local time property declaration to make sure that the local time value is what is serialized (see example).
Caveats to this approach
The recommended approach of managing a DateTime in Universal time within your private member variables is sound, as is the recommendation to provide dual properties to allow coders to deal with the versions of time that they are most comfortable with. One issue that a developer using this or any other approach that exposes any local time to a program continues to be the 25-hour-day issue around daylight savings time. This will continue to be an issue for programs that use CLR version 1.0 and 1.1, so you have to be aware as to whether your program falls into this special case (the added or missing hour for the time being represented), and adjust manually. For those who cannot tolerate a one-hour per year issue window, the current recommendation is to store your dates as strings or some other self-managed approach. (Unix long integers are a good option.)
For CLR version 2.0 (available in the upcoming release of Visual Studio® code-named "Whidbey"), awareness of whether a DateTime contains a local time or a universal time value is being added to the .NET Framework. At that point, the recommended pattern will continue to work, but for programs that interact with member variables via the UTC properties, these errors in the missing/extra hour period will be eliminated. For this reason, the best practice for coding using dual properties is strongly suggested today, so that your programs will migrate cleanly to CLR version 2.0.
Dealing with Daylight Savings Time
As we prepare to close and leave the topic of coding and testing practices for DateTime values, there remains one special case that you need to understand. This case involves the ambiguities that surround daylight saving time and the repeated one-hour per year issue. This issue is primarily one that only affects applications that collect time values from user input.
For those of you in the country-count majority, this case is trivial because in most countries daylight savings time is not practiced. But for those of you who are in the affected programs majority (that is, all of you who have applications that need to deal with time that may be represented in or sourced in places that DO practice daylight savings), you have to know this problem exists and account for it.
In areas of the world that practice daylight savings time, there is one hour each fall and spring where time seemingly goes haywire. On the night that the clock time shifts from standard time to daylight time, the time jumps ahead an hour. This occurs in the spring. In the fall of the year, on one night, the local time clock jumps back an hour.
On these days, you can encounter conditions where the day is 23 or 25 hours in length. So if you are adding or subtracting spans of time from date values and the span crosses this strange point in time where the clocks switch, your code needs to make a manual adjustment.
For logic that is using the DateTime.Parse() method to calculate a DateTime value based on user input of a specific date and time, you need to detect that certain values are not valid (on the 23-hour day), and certain values have two meanings because a particular hour repeats (on the 25-hour day). To do this, you need to know the dates involved and look for these hours. It may be useful to parse and redisplay the interpreted date information as the user exits the fields used to enter dates. As a rule, avoid having users specify daylight savings time in their input.
We've already covered the best practice for time-span calculations. By converting your local time views to universal time prior to performing your calculations, you get past the issues of time accuracy. The harder-to-manage case is the ambiguity case associated with parsing user input that occurs during this magical hour in the spring and fall.
Presently there is no way to parse a string that represents a user's view of time and have it accurately assigned a universal time value. The reason is that people who experience daylight savings time don't live in places where the time zone is Greenwich Mean Time. Thus, it is entirely possible that someone living on the east coast of the United States types in a value like "Oct 26, 2003 01:10:00 AM".
On this particular morning, at 2:00 AM, the local clock is reset to 1:00 AM, creating a 25-hour day. Since all values of clock time between 1:00 AM and 2:00 AM occur twice on that particular morning—at least in most of the United states and Canada. The computer really has no way to know which 1:10 AM was meant—the one that occurs prior to the switch, or the one that occurs 10 minutes after the daylight savings time switch.
Similarly, your programs have to deal with the problem that happens in the springtime when, on a particular morning, there is no such time as 2:10 AM. The reason is that at 2:00 on that particular morning, the time on local clocks suddenly changes to 3:00 AM. The entire 2:00 hour never happens on this 23-hour day.
Your programs have to deal with these cases, possibly by prompting the user when you detect the ambiguity. If you aren't collecting date-time strings from users and parsing them, then you probably don't have these issues. Programs that need to determine whether a particular time falls in daylight savings time can make use of the following:
Timezone.CurrentTimeZone.IsDaylightSavingTime(DateTimeInstance)
or
DateTimeInstance.IsDaylightSavingTime
Best Practice #7
When testing, if your programs accept user input specifying date and time values, be sure to test for data loss on "spring-ahead", "fall-back" 23- and 25-hour days. Also make sure to test for dates gathered on a machine in one time zone and stored on a machine in another time zone.
Formatting and Parsing User-Ready Values
For programs that do take date and time information from users and need to convert this user input into DateTime values, the Framework provides support for parsing strings that are formatted in specific ways. In general, the DateTime.Parse and ParseExact methods are useful for converting strings that contain dates and times into DateTime values. Conversely, the methods ToString, ToLongDateString, ToLongTimeString, ToShortDateString, and ToShortTimeString are all useful for rendering DateTime values into human-readable strings.
Two main issues that affect parsing are culture and format string. The DateTime Frequently Asked Questions (FAQ) covers the basic issues around culture, so here we'll focus on the format string best practices that affect DateTime parsing.
The recommended format strings for converting DateTime to strings are:
'yyyy'-'MM'-'dd'T'HH': 'mm': 'ss.fffffff'Z' —For UCT values
'yyyy'-'MM'-'dd'T'HH': 'mm': 'ss.fffffff'zzz' —For local values
'yyyy'-'MM'-'dd'T'HH': 'mm': 'ss.fffffff' —For abstract time values
These are the format string values that would be passed to the DateTime.ToString method if you want to get output that is compatible with the XML DateTime type specification. The quotes insure that the local date-time settings on the machine don't override your formatting options. If you need to specify different layouts, you can pass other format strings for a fairly flexible date rendering capability, but you need to be careful to only use the Z notation to render strings from UCT values, and use the zzz notation for local time values.
Parsing strings and converting them to DateTime values can be accomplished with the DateTime.Parse and ParseExact methods. For most of us, Parse is sufficient since ParseExact requires you to provide your own Formatter object instance. Parse is pretty capable and flexible, and can accurately convert most strings that contain dates and times.
Finally, it is important to always call the Parse and ToString methods only after setting the thread's CultureInfo to CultureInfo.InvariantCulture.
Future Consideration
One thing you can't do easily at present with DateTime.ToString is format a DateTime value into an arbitrary time zone. This feature is being considered for future implementations of the .NET Framework. If you need to be able to determine that the string "12:00:00 EST" is equivalent to "11:00:00 EDT", you will have to handle the conversion and comparison yourself.
Issues with the DateTime.Now() Method
There are several issues when dealing with the method named Now. For the Visual Basic developers reading this, this applies to the Visual Basic Now function, as well. Developers who regularly use the Now method know that it is commonly used to get the current time. The value returned by the Now method is in the current machine time-zone context, and cannot be treated as an immutable value. A common practice is to convert times that are going to be stored or sent between machines into Universal (UCT) time.
When daylight savings time is a possibility, there is one coding practice that you should avoid. Consider the following code that can introduce a hard-to-detect bug:
Dim timeval As DateTime
timeval = DateTime.Now().ToUniversalTime()
The value that results from running this code will be off by an hour if called during the extra hour that occurs during the daylight savings time switch in the fall. (This only applies to machines that are in time-zones that practice daylight savings time.) Because the extra hour falls into that place where the same value, such as 1:10:00 AM, occurs twice on that morning, the value returned may not match the value you wanted.
To fix this, a best practice is to call DateTime.UtcNow() instead of calling DateTime.Now, and then converting to universal time.
Dim timeval As DateTime
timeval = DateTime.UtcNow()
This code will always have the proper 24-hour-day perspective, and may then be safely converted to local time.
Best Practice #8
When you are coding and desire to store current time represented as universal time, avoid calling DateTime.Now() followed by a conversion to universal time. Instead, call the DateTime.UtcNow function directly.
Caveat: If you are going to serialize a class that contains a DateTime value, be sure that the value being serialized does not represent Universal time. XML serialization will not support UCT serialization until the Whidbey release of Visual Studio.
A Couple of Little Known Extras
Sometimes when you start diving into a part of an API you find a hidden gem—something that helps you achieve a goal, but which, if you aren't told about it, you don't uncover in your day-to-day travels. The DateTime value type in .NET has several such gems that may help you achieve more consistent use of universal time.
The first is the DateTimeStyles enumeration that is found in the System.Globalization namespace. The enumeration controls behaviors of the DateTime.Parse() and ParseExact functions that are used to convert user-specified input and other forms of input string representations to DateTime values.
The following table highlights some of the features that the DateTimeStyles enumeration enables.
Enumeration Constant | Purpose | Caveats |
---|---|---|
AdjustToUniversal | When passed as a part of a Parse or ParseExact method, this flag causes the value returned to be universal time. | Documentation is ambiguous, but this works with both Parse and ParseExact. |
NoCurrentDateDefault | Suppresses the assumption that strings being parsed with no date components will have a DateTime value returned that is the time on the current date. | If this option is used, the DateTime value returned is the time specified on the Gregorian date January 1 in the year 1. |
AllowWhiteSpaces
AllowTrailingWhite AllowLeadingWhite AllowInnerWhite |
These options all enable tolerance for added white spaces in front of, behind, and in the middle of the date strings being parsed. | None |
Other interesting support functions are found in the System.Timezone class. Be sure to check those out if you want to detect whether daylight savings time will affect a DateTime value, or if you want to programmatically determine the current time zone offset for the local machine.
Conclusion
The .NET Framework DateTime class provides a full-featured interface for writing programs that deal with time. Understanding the nuances of dealing with the class goes beyond what you can glean from Intellisense®. Here we covered the best practices for coding and testing programs that deal with dates and time. Happy coding!