Friday, March 4, 2011

ColdFusion, Dates and Timezones

Recently I have been working on a project where I would like to display date/time values to the user in their preferred timezone. This seemed like a pretty straight forward task; take a date and convert it to the user's timezone based on the offset.

The first thing I did was to see what ColdFusion offered for built in functionality. Sadly, I found that ColdFusion didn't have much for working with date/times in multiple timezones. I was optimistic when I found the dateConvert() method, but found that it only converted from the server's local timezone to UTC and back.

After some research I found that the date object itself does not have a timezone value. All dates are represented with a long integer value as the number milliseconds from the epoch (which is January 1, 1970) in UTC. This fact is important when thinking about what a conversion from one timezone to another really is.

Let's say I have a date/time of '1/1/2011 08:00:00.000' that I want to display to users in many timezones. Assume my ColdFusion server is running on US/Central time and I want to display the date/time to users in the UTC timezone. Ignoring daylight time, the US/Central timezone is 6 hours behind UTC. Using the dateAdd() method I might do something like this:
localDate = parseDateTime("1/1/2011 08:00:00.000");
UtcDate = dateAdd("h",6,localDate);
When I output the result of both of these values I get the following:

localDate = {ts '2011-01-01 08:00:00'}
UtcDate = {ts '2011-01-01 14:00:00'}

And this seems to make sense. The first date/time is exactly what should be expected, and the dateAdd() method added six hours to the date to get the UTC date/time. Now, I'll output the values again, except this time I will run the date object's getTime() method:

localDate.getTime() = 1293890400000
UtcDate.getTime() = 1293912000000

Notice here that the values are different. As I explained, dates are stored as an offset, in milliseconds, from the epoch in UTC. Therefore, these two date objects are now representing different date/time values. This may not be a big issue in most circumstances, but it didn't sit right with me that the object returned was actually a different value.

When I want to show the same date/time in multiple timezones, I really just want a different format of the date/time. Unfortunately, in ColdFusion there is no option for a timezone in any of the date or time formatting functions. The parseDateTime() method recognizes a timezone, and there are Locale Specific functions for the date and time formatting functions, but there is nothing for displaying date/time values with a timezone.

The easiest way that I found to format dates for multiple timezones was by dropping down into Java. Two classes are all that are needed: java.text.SimpleDateFormat and java.util.TimeZone. These two classes are part of the Standard Java classes, so there is no need for JavaLoader or adding the classes to your classpath. Here is how the classes can be used to make a really simple formatting function to display date/times in different timezones.
<cfcomponent name="TZDateFormat">

 <cfset variables.TimeZone = createObject("java","java.util.TimeZone")>

  <cffunction name="format" access="public" returntype="string" output="false">
  
    <cfargument name="datetime" required="true">
    <cfargument name="mask" required="true">
    <cfargument name="timezone" required="false">
  
    <cfset var tzObject = ''>
    <cfset var dateFormatter = ''>
  
    <cfif not structKeyExists(arguments,"timezone")>
      <cfset tzObject = variables.TimeZone.getDefault()>
    <cfelse>
      <cfset tzObject = variables.TimeZone.getTimeZone(arguments.timezone)>
    </cfif>
  
    <cfset dateFormatter = createObject("java","java.text.SimpleDateFormat").init(arguments.mask)>
  
    <cfset dateFormatter.setTimeZone(tzObject)>
  
    <cfreturn dateFormatter.format(parseDateTime(arguments.datetime)) >  
  
 </cffunction>
 
</cfcomponent>
The function takes three arguments. First, is the date/time value. Second, is the mask or format that I want the date/time to be output in. Last, is the optional timezone that I want the date/time to be displayed in. If the last argument is not supplied it will default to the default timezone on the server. To see all the available timezone IDs that are supported call the getAvailableIDs() method on the java.util.TimeZone object. This will return an array of the IDs.

So, I'll go back to my original example. This time, though, I will use my format method to display the date/time in UTC.
localDate = TZDateFormat.format("1/1/2011 08:00:00","MM/dd/yyyy HH:mm:ss")
UtcDate = TZDateFormat.format("1/1/2011 08:00:00","MM/dd/yyyy HH:mm:ss","UTC")
When I output these values I get the following:

localDate = 01/01/2011 08:00:00
UtcDate = 01/01/2011 14:00:00

As you can see, these values look very similar to the earlier outputs that I did. The major thing to notice here is that the return from the format function is a string. They look like dates when I output them, but they are just strings. A word of warning: ColdFusion will implicitly treat these strings as date objects if you use any of the built in date functions, and will interpret these as dates in the server's default timezone. This probably wouldn't lead to desirable behavior.

I think there are three main benefits to using SimpleDateFormat object to display dates in other timezones:
  1. The dateAdd() or dateConvert() methods actually change the date. The SimpleDateFormat format method returns a string formatted as desired. It also has the ability to output the timezone with the 'z' mask (which ColdFusion cannot do).
  2. The SimpleDateFormat object has access to the JREs timezone database. This means all the timezone conversion, including daylight time, is taken care of.
  3. The format in SimpleDateFormat can do date and time formatting together. In ColdFusion I have to use dateFormat() and timeFormat() methods separately.
That's all there really is to showing the dates in different timezones. I ended up setting my ColdFusion instance to run in UTC timezone, and I save all dates in my database in UTC. I did this to erase any chance of ambiguity of dates coming from the database (think daylight time switches in the fall). I also created a replacement function for the datePart() method using java.util.GregorianCalendar. I did this so that I could get the datepart for different timezones. Be aware that that GregorianCalendar starts the months at zero, and ColdFusion starts the months at one. The final caveat to the whole thing are the masks that SimpleDateFormat uses. The masks between Java and ColdFusion are not the same, so check the Java APIs for the correct mask characters.

I was disappointed that ColdFusion didn't support this out-of-the-box. However, it is great that ColdFusion can drop down into Java libraries so easily to leverage this kind of functionality.

2 comments:

  1. Nice work! I've been curious how to handle this exact situation. Another part of the problem is asking a user to select a timezone. Have you done that? If so, are you using TimeZone.getAvailableIDs() and then looping through them?

    PS: It would be great if you allowed anonymous comments!

    ReplyDelete
  2. You could really easily loop through the array returned from TimeZone.getAvailableIDs(), but the array returned is really large. Many of the IDs are redundant or deprecated. My solution was to create a reference table of a standard list of timezones and then map each of those timezones to the appropriate java timezone ID. I actually used the list of timezones that Windows uses in the clock settings for my default list.

    ReplyDelete