.NET DateTime coding best practices

80酷酷网    80kuku.com

  

.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, 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 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 arithmetic calculations.  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.

 

<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 out 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 noon-ish time, 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.

 

Testing Best Practice 2

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().

 

Coding Best Practice 3

When serializing 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 – which 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 thus 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 setters 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.

 

Coding Best Practice 4

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 of whether your program falls into this special case (that 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, 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 that 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 effects 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 effected 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 that are 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 falls in this magical hour in the spring and fall.

Presently, there is no way to parse a string that represents a users view of time and have it 100% 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 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

Testing Best Practice 3

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 effect parsing are culture and format string.  The DateTime Frequently Asked Questions (FAQ) covers the basic issues around culture, so here we’ll on the format string best practices that effect 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 (VB) developers reading this, this applies to the VB 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 (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.

 

Coding Best Practice 5

When you desire to store current time represented as universal time, avoid callingthe  DateTime.Now() followed by a conversion to universal time.  Instead call the direct DateTime.UtcNow function.

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, your day to day travels don’t uncover the gem.  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 methods 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 that 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
 
 

 

Other interesting support functions are found in the System.Timezone class.  Be sure to check those out if you want to detect whether a DateTime value is effected by daylight savings or 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?.  In this article we covered the best practices for coding and testing programs that deal with dates and time.  Happy coding.

 

分享到
  • 微信分享
  • 新浪微博
  • QQ好友
  • QQ空间
点击: