Date And Time Tracking in .Net

Sep 28, 2009
by:   Tim Stanley

Getting the right date and time sounds simple enough, but when you look at a few interesting scenarios with multiple teams coordinating actions across multiple time zones with multiple clients and servers, designing for the date and time for a given event becomes complicated very quickly.  I’d like to share an some approaches that I think work well in .Net. 

The approaches I describe are based on web servers and clients in multiple countries.  Having a single standalone application(EXE) on a desktop is somewhat easier to deal with because the application has full access to the time zone, culture settings, and correct time.

Requirements

Multiple servers, clients, and teams in multiple time zones.  For example:

  1. A Server hosted in the United States in Texas (GMT-06:00) Central Time (US & Canada) (supports DST)
  2. A team in Delhi India: (GMT+05:30) Chennai, Kolkata, Mumbai, New Delhi (doesn’t support DST)
  3. A team in Raleigh, North Carolina: (GMT-05:00) Easter Time (US & Canada)  (supports DST)
  4. A team in Phoenix, Arizona: (GMT-07:00) Arizona (doesn’t support DST)
  5. A team in Colorado: (GMT-07:00) Maintain Time (US & Canada) (Supports DST)
  6. A public syndication reader (client time zone unknown)

There are several different types of dates and times and how they may need to be handled.

  1. Creation, publication and update dates and times (fixed time around the globe)
  2. Start and stop dates and times for displaying a task (relative to the time zone)
  3. Event start and end dates and times for a task (fixed time around the globe)
  4. Due start and end dates and times for a task (relative to the time zone)

Design Considerations

There are two basic types of scheduled events for dates and times.  A fixed point in time around the globe and a relative point in time from the local time zone.  There are several key pieces of information required to convert from one time zone to another.

  1. The base time offset of the client time zone from GMT / UTC.
  2. If daylight savings time is supported by the client time zone or not.
  3. The rules for when daylight savings time starts and ends for the client.

Microsoft .net 3.5 provides for a TimeZoneInfo class which provides DST and DST rules, but, without writing probing code in the browser client, a browser client only knows about the offset from GMT.

Changes In Calendars

There are times in the current era when dates and times change based on different rules than the standard rollover from 11:59:59 PM to 12:00:00 AM on the next calendar day.

Changes .Net takes into account:

  • The Gregorian calendar
  • Leap day, leap year (added every four years, not every 100 years, but added every 400 years)
  • Daylight savings time (including the 2009 changes on effective dates)

.Net date and time libraries do not take into account:

  • Leap second (I can find no reference that .Net supports leap seconds.  The Microsoft documentation states the value can be between 0 and 59 which implies that leap seconds which require values of 60, 61, are not supported)
  • Historical dates prior to the year 0001 AD on the Gregorian calendar.
  • Changes of political conversions to the Gregorian calendar (10-13 days dropped depending on which country and when they converted to the Gregorian calendar)
  • Conversion to non Gregorian based calendars (Chinese, Hebrew, Islamic, Hindu, Iranian)
  • Julian dates
  • Terrestrial time
  • Unix time

Julian dates are often used in astronomy to reference a common point in time that doesn’t conflict with other calendars. The Julian day and calendar is are based off a point in time of origin around 4713 BC Greenwich noon.

Daylight Savings Time

Keep in mind that .Net DateTime arithmetic and comparison operations do not take daylight saving time into account.  However, conversion between time zones does properly take into account daylight savings time. Using TimeZoneInfo, also takes into account multiple daylight savings time rules for different years (See the Year 2007 DST Problem for more about the details of why and when this change took place).

A Fixed Point In Time

fixed points in time

The chart above shows two times local to the Easter Standard Time (EST);  one at 10:00 AM and one at 10:00 PM.  We can see that the 10:00 PM EST event will span into another date at 8:30 AM for IST users.  Fixed points in time are best dealt with by converting them to UTC (GMT) or universal time and then when displayed, converted back to the local users time using the time zone information.

Fixed points in time are usually used for creation dates and times, update dates and times, specific events or meetings.  Storing the values as universal time values (UTC) allows the information to be stored independent of clients, web servers, and database servers.  If a web server is moved from one time zone to another, the UTC value for the data isn’t going to change.

Relative Points In Time

relative points in time

The chart above shows an event occurring at 10:00 AM relative to the start of each time zone.  Relative points in time are usually used for start / stop dates for promotions, local displays, and for business day processing.  If times are converted to UTC on set operations and to the client time on get operations, no other conversion needs to take place when displaying the time.  However, in calculating if the task is over due, then some difference needs to be calculated.

In order to calculate a relative point in time, two pieces of additional information are needed.

  • The time zone of the administrator that set the time needs to be stored so that relative comparisons can take place.
  • A flag that indicates a relative time comparison needs to take place (i.e. IsPastDueRelative).
        /// 
        /// True if the client time is > DueStartDate.
        /// 
        /// Current time at the client used for comparison
        /// 
        public Boolean IsPastDueStart()
        {
            DateTime clientTime = Utils.DateClientNow;
            int diff;
            if (IsPastDueRelative)
            {
                TimeZoneInfo tz = null;
                tz = TimeZoneInfo.FindSystemTimeZoneById(this.DueDateTimeZoneId);
                DateTime dueTime = TimeZoneInfo.ConvertTimeFromUtc(this.DueStartDateUtc, tz);
                diff = clientTime.CompareTo(dueTime);
            }
            else
            {
                diff = clientTime.CompareTo(this.DueStartDate);
            }
            return (diff > 0);
        }

Logical Business Day

Business day chart

A business day becomes important for tracking when a particular event might be recorded for historical or business purposes.  The chart above shows a situation where a business day for recording events starts at 8:00 AM and continues until 8:00 AM the next day.  A similar situation can occur for recording transactions at the point of sale.  Sales might continue for business day 1 until someone closes a register, counts the money and balances the till.  If a sale is rung up after that (say 5:15 pm), the sale is recorded on the next logical business day, not the current calendar day.

In order to implement a logical business day, there needs to be either an indicator for the current logical business day that changes at some point in the day, or a table that indicates that start / end times for a logical business day for a particular location.

MinValue, MaxValue And Null

Dates can have specific values that applications can utilize. DateTime.MinValue, DateTime.MaxValue, null.  Dealing with dates or times or dates and times can be tricky with databases, and SQL.  Using a null value can complicate things even further.  Using a MinValue and MaxValue helps keep nulls out of the picture and they can be sorted as well.  The .net values used for MinValue and MaxValue are:

  • MinValue = 1/1/0001 12:00:00 AM (00:00:00.0000000, January 1, 0001)
  • MaxValue = 12/31/9999 11:59:59 PM (23:59:59.9999999, December 31, 9999)

The interesting anomaly of these min and max values is they are always in LocalTime timezones.  When converted to UTC, these values will change based off the UtcOffset.  If you want a MinValue or MaxValue in a UTC DateTime, the solution isn’t so obvious.  There may be better solutions, but using ConvertTimeToUtc and TimeZoneInfo.Utc, I found a combination that worked.

        DateTime min = TimeZoneInfo.ConvertTimeToUtc(DateTime.MinValue, TimeZoneInfo.Utc);
        DateTime max = TimeZoneInfo.ConvertTimeToUtc(DateTime.MaxValue, TimeZoneInfo.Utc);

For the curious, the UTC Conversion still equals the MinValue or MaxValue.  The values below both return true.

        min.Equals(DateTime.MinValue)
        max.Equals(DateTime.MaxValue)

Convert Database and Internal Times To UTC

When using dates and times for Create and Update as a single point in time (not local to the time zone), they need to be converted to GMT / UTC.  Converting these to UTC and storing them in the database as UTC dates and times is easy to accomplish using .ToUniversalTime().  The nice thing about .Net is that converting a date / time to UTC that is already UTC isn’t going to change the time (refer to the .Kind property for how .Net does this).

Converting the value back to local time isn’t always appropriate.  First, because when running on an IIS Server, the class doesn’t know the browser clients TimeZoneInfo.  Second, because it’s not clear at this context what specific format (local or UTC) is desired.  I like the example shown of the Microsoft best practices of exposing a client date/time property and a UTC date/time property. This removes any ambiguity as to what time zone a particular time is based. In the example below, client time is always local to the client (based off the clients TimeZoneId, which is assumed to be set earlier in the session).

    private DateTime _CreateTime;
    /// 
    /// Date / time when the class was created in the UTC time zone.
    /// 
    public DateTime CreateTimeUtc
    {
        get { return _CreateTime; }
        set { _CreateTime = value.ToUniversalTime(); }
    }

    /// 
    /// Date / time when the class was created in the client time zone.
    /// 
    public DateTime CreateTimeUtc
    {
        get { return DateClientTimeFromUtc(_CreateTime); }
        set { _CreateTime = value.ToUniversalTime(); }
    }

Convert UTC Times To Client Times

Internal values stored as UTC can be converted to client times, provided the clients time zone information is known.  The code below doesn’t convert DateTime.MinValue.

    private DateTime _StartTime;
    /// 
    /// Date / time to start displaying the item. Based on the client time zone.
    /// Also the published date.
    /// 
    /// 
    /// Stored as UTC in memory and DB.

    public DateTime StartTime
    {
        get
        {
            DateTime val = _StartTime;
            if (val != DateTime.MinValue)
            {
                val = DateClientTimeFromUtc(val);
            }
            return val;
        }
        set
        {
            DateTime val = value;
            if (val != DateTime.MinValue)
            {
                val = val.ToUniversalTime();
            }
            _StartTime = val;
        }
    }

    public DateTime StartTimeUtc
    {
        get
        {
            return _StartTime;
        }
        set
        {
            DateTime val = value;
            if (val != DateTime.MinValue)
            {
                val = val.ToUniversalTime();
            }
            _StartTime = val;
        }
    }

    public static DateTime DateClientTimeFromUtc(DateTime clientTime)
    {
        DateTime convertedTime;
        string tzId = (string) HttpContext.Current.Session["TimeZoneInfoId"];
        TimeZoneInfo TimeZoneInfoClient = TimeZoneInfo.FindSystemTimeZoneById(tzId);
        convertedTime = TimeZoneInfo.ConvertTimeFromUtc(clientTime, TimeZoneInfoClient);
        return convertedTime;
    }

Convert Parsed Times to UTC

At the outset, the strategy sounds simple, convert date and time values to UTC internally, and when stored in the database, and convert them to the clients time when retrieved or displayed. The tricky part comes when a user enters data from the client.  Let’s say a user enters a date and time into a textbox as “2009 10 31 11:00 PM”.  By default, the Parse function treats this as System.Globalization.DateTimeStyles.None.  In order to use the clients time zone for the conversion, a slight modification is needed.  Use TimeZoneInfo.ConvertTimeBySystemTimeZoneId() and the clients TimeZone to convert the parsed date appropriately.

        DateTime clientTime;
        string tzId = (string) HttpContext.Current.Session["TimeZoneInfoId"]; 
        clientTime = DateTime.Parse("2009 10 31 11:00 PM");
TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById(tzId)
clientTime = TimeZoneInfo.ConvertTimeToUtc(clientTime, tzId);

Sample times:

  • 10/31/2009 11:00:00 PM PST => 11/1/2009 6:00:00 AM GMT / UTC
  • 11/1/2009 3:00:00 AM PST => 11/1/2009 11:00:00 AM GMT / UTC

Convert Output Formats Based On Culture

How does one display date formats based on the users date preferences date format preferences.  These preferences are normally based on the culture of the browser (or client application).  For example, if the goal is to output dates based off the users culture settings, the date will be different based on the culture settings.

Note: Browser culture settings are not the same as the System Control panel format settings in Windows.

  • Scenario server time: 9/23/2009 10:41:45 AM   (EST)
  • The server is in EST GMT-5:00
  • Client 1 is in PST GMT-8:00
  • Client 1 culture is “en-US”
  • Client 2 is in London, England GMT+1:00 (GMT Standard Time, which is different than GMT coordinated time)
  • Client 2 culture is “en-GB”
  • Client 1: Display date format: 9/23/2009 7:41:45 AM
  • Client 2: Display date format: 23/09/2009 15:41:45

There are two ways to accomplish this; a hard way and an easy way.  In the example below, TimezoneInfoClient is the TimeZoneInfo value stored in a cookie or session set based on the clients preferences.

Easy Date Formatting

The easy way to format the output is to let .Net do all the work.  DateTime.ToString() will use the users current culture and format the date based on those settings.  This works in code on a desktop or code running on a server.  The users culture that’s taken into account is the browsers culture setting not the control panel settings(HttpContext.Current.Request.ServerVariables["HTTP_ACCEPT_LANGUAGE"]).  It’s easy to test this without flying to London or without changing the workstation culture settings.  Changing the Firefox value intl.accept_languages to “en-gb, en” instead of “en-us” for example will change all ACCEPT_LANGUAGE values for future requests.

DateTime t;
t = TimeZoneInfo.ConvertTime(DateTime.Now, TimeZoneInfoClient);
t.ToString()

Result:

  • Client 1: Display date format: 9/23/2009 7:41:45 AM (en-US culture)
  • Client 2: Display date format: 23/09/2009 15:41:45 (en-GB culture)

Complex Date Formatting

It’s easy to get the same result with more code.  There are some circumstances where this might really be necessary.  For example, code running in a service that doesn’t know the clients specific culture, and the culture needs to be passed around to other layers for date formatting.  While it can be argued the formatting should be pushed as closest to the presentation layer as possible, the world isn’t a perfect place and re-using existing code sometimes creates a less than optimal situation.

DateTime t;
t = TimeZoneInfo.ConvertTime(DateTime.Now, TimeZoneInfoClient);
t.ToString()
CultureInfo culture = CultureInfo.CreateSpecificCulture("en-GB");
t.ToString(culture.DateTimeFormat));

Result:

  • Display date format: 23/09/2009 15:41:45

W3C Dates And Times

XML, Web Services, SOAP, WCF, WSDL, and other standards use dates and times based on the W3C date standard formats.  Some times it’s desirable to get the exact W3C date format for use.  In .Net, this is easy to accomplish.

DateTime.Now.ToString("yyyy-MM-ddTHH:mm:sszzz");
DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ssK");
DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ssZ");

FF is used in the format string if tenths of seconds is desired.

DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.FFzzz");
DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.FFK");
DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.FFZ");

Note: Interestingly, the Z form should be used on UTC based dates.  If the “zzz “or “k” form is used on a UTC date / time, an error exception will be thrown.

Deferred Date Formatting

Another approach is to defer formatting of dates until they are shown to the client.  It’s  not practical for all scenarios, but it does provide two benefits.  First, for some content output pages can cache a single page for all cultures and then the client script can decide based off the browsers time zone offset and the culture the formatting.  It makes things a bit more complicated, but it’s interesting and useful for some scenarios.

This strategy works by sending W3C output or GMT output times similar to what I’ve shown below.

  • W3C date/time format: 2009-09-23T10:41:45-04:00
  • W3C date/time format: 2009-09-23T14:41:45Z

These dates and times are then converted to the browser date / time formats.  The technique can also be used to convert times to text (for example, updated 15 minutes ago).

A few examples that use this approach:

Summary of DateTime Best Practices

  1. Convert Create and Update time to UTC (.ToUniversalTime()) when storing these in a database.
  2. Use DateTime.MinValue, DateTime.MaxValue and null appropriately.
  3. Do not blindly convert DateTime.MinValue and DateTime.MaxValue to UTC.
  4. Convert fixed points in time to universal time (UTC).
  5. Expose UTC and Client time zone based date / time properties for dates.
  6. Convert DateTime.Parse() to UTC using the client’s time zone, not the servers.
  7. Remember date/time arithmetic doesn’t preserve DST rules or changes.
  8. To avoid DST issues, convert to UTC, perform the arithmetic, then convert back to local time if needed.

References

Add to favorites Send to a friend Digg It! DZone It! StumbleUpon Technorati Reddit Del.icio.us NewsVine Furl BlinkList