23/12/2011: Managing DateTime Offsets with the TimeZoneInfo ClassFriday, December 23, 2011 by Matt Woodward | Taxonomy: .Net, Development, Software, Tip | No Comments
This week I have been looking at trying to introduce some short term fixes to an .Net Web Application. The application has had some globalization work undertaken on it (for the use of language files) but had no concept of time zones at all. To be used in multiple countries, the application needed as a minimum to be able to display date & times in the local time zone (and also accommodate for daylight savings). I was keen to do this with the following rules in mind:
- I should have minimal impact on the existing code base (so no huge refactoring exercises!)
- I cannot make any database changes
- The changes would be for display purposes only (considered as the first step in many to true localisation)
I read a great article on the Coding Best Practices Using DateTime in the .Net Framework (2004) which sadly was many, many versions out of date but gave a good breakdown of programming with DateTime objects. I coupled this with a review of the DateTimeOffset Class introduced in .Net 3.5 to make sure I had a good picture of what native objects & types could provide for my conundrum, then went about devising my solution.
I decided to introduce a Localization “Utility” class which would have the responsibility for all datetime localisation routines. Being that I only had cultures to work with (not user / business level timezone’s – or even countries and states ) I had to “assume” a time zone for each culture. This wasn’t my proudest moment, but enabled the introduction of a sufficient level of localisation to enrich the application.
The first function I wrote was a routine to encapsulate determining the current culture, this was being set through the settings in the web.config, for example:
Inherently, for a web application, this setting will determine the culture of the current thread. So in order to detect the current culture, my code looked as follows:
''' ''' Obtain the current culture that the application is running under '''
”’ A CultureInfo instance ”’ Encapsulates the routine for determining the current culture Private Shared Function GetCurrentCulture() As CultureInfo Return Thread.CurrentThread.CurrentCulture End Function
Hardly genius, but encapsulating code in this way protects you better against change. If further down the track, the way in which culture is determined alters, we only have one place to amend this! Next up was how to determine the timezone based on the respective culture. To do this I wrote a simple routine to infer a timezone based on the current culture, this looked like:
''' ''' Obtain the current cultures assumed timezone '''
”’ A timezoneinfo object to cater for time conversion operations ”’ (DISPLAY ONLY) Routine for altering user-facing datimetime values Public Shared Function GetLocalTimezone() As TimeZoneInfo ‘Declare & initialise required variables Dim currentCulture As CultureInfo = GetCurrentCulture() Dim timeZoneId As String = “GMT Standard Time” ‘Assume UK by default ‘Verify the country we are localising for Select Case currentCulture.Name Case “de-DE” ‘Set Time Zone ID Based on Culture timeZoneId = “Central Europe Standard Time” Case “en-AU” ‘Set Time Zone ID Based on Culture timeZoneId = “AUS Eastern Standard Time” Case … End Select ‘Return the timezone for the current culture Return TimeZoneInfo.FindSystemTimeZoneById(timeZoneId) End Function
This would not cater for different timezones within the same culture, but as I didn’t have this level of detail this is the best that could be achieved. Next was to undertake the conversion of a GMT+0 time to the local culture’s time and cater for daylight savings. Having read the articles above, I was foreseeing that this may be a bit of a slog but was nicely surprised to see that the routine could be achieved with a fairly simple routine:
''' ''' Convert a datetime variable using a time offset based on the culture thread culture '''
”’The datetime variable as retrieved from the database ”’ A datetime variable representing the new date time i.e. accomodating the time offset ”’ (DISPLAY ONLY)) Routine to show datetime values accomodating a UTC offset. Public Shared Function GetLocalDateTime(dt As DateTime) As DateTime ‘Declare & initialise required variables Dim currentTimezone As TimeZoneInfo = GetLocalTimezone() Dim offsetDate As DateTimeOffset = New DateTimeOffset(dt, TimeSpan.Zero) ‘Return the DateTime accomodating the Timezone’s UTC Offset Return offsetDate.ToOffset(currentTimezone.GetUtcOffset(offsetDate)).DateTime End Function
By using the UTC offset of the current TimeZoneInfo object daylight savings are automatically catered for and the datetime altered accordingly, bingo! To finish off, and to cater for different formatting requirements, I added in a simple routine to provision for obtaining a localised instance of a DateTimeFormatInfo Object. This would ensure that as the localised DateTime Objects were written out they could be formatted according to local culture, here’s the code:
''' ''' Obtain the current cultures DateTimeFormatInfo object '''
”’ A DateTimeFormatInfo instance ”’ For use when displaying date & times in localised formatting Public Shared Function GetLocalDateTimeFormat() As DateTimeFormatInfo ‘Declare & initialise required variables Dim currentCulture As CultureInfo = GetCurrentCulture() ‘Return DateTime Format Instance Return currentCulture.DateTimeFormat End Function
Obviously this solution is fairly specific in regard to the constraints I had to work within, but it goes to show how you can start to introduce localisation even under the least ideal circumstances. Hope you find this handy, and feel free to comment or ask questions below!