Thursday, June 26, 2008

Handling time zones in .Net is really easy

I have a project right now where we are going to implement a feature that allows an admin to specify the time zone that each user is in. When they view reports on things like system activity or similar that have a time component to them, they will see the times in their own time zone. When they request a report and specify a time, it will parse that time in whatever their own time zone is.

Time is a fairly difficult problem to handle since many different areas have different time zones with different rules. Does it have Daylight Savings time? When does that start/stop? What if the dates it starts or stops on change? What about the 25 hour day you get when you change and the 23 hour day you get when you change back? There are a lot of edge cases and a lot of time zones (for example, Arizona is on Pacific Time but it does not have Daylight Savings Time) that you have to account for. Fortunately, .Net has this nifty thing called the TimeZoneInfo class that makes this a trivial problem.

Some useful methods

There are a number of static methods on this class that are useful to the feature that I'm trying to implement. The first story that I want to implement is "As an Admin, I want to see all available time zones so that I can select the appropriate one for a user." Not a problem. I want to load them all and store them in a listbox. Here's the code:


ListBox list = new ListBox();
IList systemtimes = TimeZoneInfo.GetSystemTimeZones();
foreach(TimeZoneInfo t in systemtimes)
{
list.Items.Add(t);
list.DisplayMember = "DisplayName";
}
This will give me a list box that tells me all the time zones that are currently installed in the registries. I have a property on my TimeZoneInfo class called "Id" that gives me the id of the particular time zone, so if I want to save the time zone that a user is on, I can persist the id of that class somewhere and then use this to retrieve it:

int tzID = MyTimeZoneRepository.GetTimeZoneIDForUser(someUser);
TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById(tzID);
Alternately, I can serialize and deserialize my TimeZoneInfo class in order to be able to persist it and reconstitute it in situations where I may not want or have available the system time objects. The code for that would be this:

TimeZoneInfo tz = TimeZOneInfo.FindSystemTimeZoneById(1);
//tz now has some time zone value

string serialized = tz.ToSerializedString();
//serialized is some really long string that has all the time zone info in it

TimeZoneInfo tzNew = TimeZoneInfo.FromSerializedString(serialized);

Assert.AreEqual(tz, tzNew); //should return true


This also means that I can create my own custom timezone if I want to by defining a set of rules and then instantiating a TimeZoneInfo class from my rules (I'm not going to give an example here but there's plenty of them out there).

Now, to actually use these things, I use the TimeZoneInfo.ConvertTime() method. So, if I want to fulfil my second user story, which is "As a user, I want to have all my reports reflect my local time zone so that I can see when things happened in relation to where I am" then I have a means to do so.

In order to do this, I need a UserTimeZoneRepository that gets the correct time zone for a user by their ID. First, some helpful interfaces. Let's assume that these all have implementations that do what they look like they should do:

public interface IUser{
public int ID {get;set}
}

public interface ITimeZoneRepository {
TimeZoneInfo GetTimeZoneForUser(IUser u);
}



Now, here's some code to handle the time zone problem. Let's assume that ALL times in my database are converted to UTC when they are stored there:

public class TimeZoneConverter{

private IUser u;
private ITimeZoneRepository r;

public TimeZoneConverter(ITimeZoneRepository r, IUser u){
_repository = r;
_user = u;
}

DateTime ConvertTimeToUTC(DateTime t){
return TimeZoneInfo.ConvertTimeToUTC(t, r.GetTimeZoneForUser(u));
}

DateTime ConvertTimeFromUTC(DateTime t){
return TimeZoneInfo.ConvertTimeFromUTC(t, r.GetTimeZoneForUser(u));
}
}


So why all of this? Well, first, this gives me a centralized place to handle all time zone conversion issues (Don't Repeat Yourself). Next, it decouples the user and the user's time zone from the actual functionality of converting the times (Single Responsibility Principle) so that I can test it in isolation but I also don't have to concern myself with where that time zone is coming from. Finally, this gives me a convenient place to hook date formatting into my application when I need to. Remember, I can't just user DateTime.Parse because I don't know if 04/05/2008 means April 5th or May 4th without having some sort of cultural info associated with this. If I store the user's culture somewhere and have a similar repository, I can then combine this object with the DateTime parsing and formatting commands and create one unified DateTimeService for my application where a Date string goes in and a DateTime comes out (works the other way also). It just so happens that I have another story that reads "As a user, I want to read dates and enter dates in a way that I am familiar with so that I can interperate the meaning correctly" so this is definitely going to come in handy in the future. Some other useful things that the TimeZoneInfo class has:

TimeZoneInfo.SupportsDaylightSavingTime - this property returns a bool that is true if there is a daylight savings time on that instance of TimeZoneInfo. For Pacific Time, this would return true, for Arizona time it would return false.

TimeZoneInfo.DaylightName - this property returns the name of the Daylight Savings Time Zone, so for Pacific time it would return "Pacific Daylight Time"

TimeZoneInfo.Local - this static property returns a TimeZoneInfo class that reflects whatever the computer you're running it on is set to.

TimeZoneInfo.Utc - static property that returns a TimeZoneInfo class that is set to UTC time

TimeZoneInfo.IsDaylightSavingsTime(DateTime) - instance method that tells you if a particular datetime occurs within daylight savings time for that time zone

Anyway, this is my first exposure to having to deal with this stuff and it's convenient that .Net provides some good functionality for dealing with it easily. If you want more info, there's a number of MSDN articles on the TimeZoneInfo class and how to deal with Dates and Times in general that you can read but this post has enough info to get you started.

No comments: