Converting Dates Across Time Zones Using ColdFusion And Java
This week has been a very immersive week in world clocks, time zones, and the absurdly inconsistent rules of Daylight Saving Time (DST). After several days of complete befuddlement, I was finally able to gain some understanding of how all this timezone logic fits together. Yesterday, I talked about using Java's java.util.TimeZone and java.util.GregorianCalendar classes to help navigate through dates and time without having to worry about local timezone and daylight saving time (DST) rules. Today, I wondered if I could use the java.util.GregorianCalendar class in order to easily convert date and times between time zones.
As I was looking at the calendar class, I noticed two functions that might be really helpful:
- getTimeInMillis() :: long
- setTimeInMillis( long )
In both of these functions, the "long" data type is the number of UTC milliseconds from Epoch. While I haven't yet fully wrapped my head around the concepts of UTC and Epoch, what I have gathered is that this value is always the same, all around the world, regardless of any particular local time. In ColdFusion, this "long" value can be easily obtained from the method:
- getTickCount()
Given the fact that I can both Get and Set the date of a Java calendar using this normalized Epoch offset, I wondered if I could set the date of one calendar using the date of another calendar. To test this, I created a calendar in the Eastern Standard Time (EST) timezone (where my server is located). Then, I created two additional calendars for Pacific Standard Time (PST) and Arizona Time. While Pacific Standard Time is three hours less than Eastern Standard Time, it should be the same as Arizona time since Arizona does not adhere to Daylight Saving Time (DST) rules.
In other words, if it's 12PM on the east coast, it should be 9AM in both California and Arizona.
Once I have my EST calendar, I then use its "time in milliseconds" to set the date for the PST and Arizona calendars:
<cffunction
name="calendarToString"
access="public"
returntype="string"
output="false"
hint="I take a given instance of java.util.GregorianCalendar and return a readable date/time string.">
<!--- Define arguments. --->
<cfargument
name="calendar"
type="any"
required="true"
hint="I am the Java calendar for which we are creating a user-friendly date string."
/>
<!--- Define the local scope. --->
<cfset var local = {} />
<!---
Create a simple date formatter that will take our calendar
date and output a nice date string.
--->
<cfset local.formatter = createObject( "java", "java.text.SimpleDateFormat" ).init(
javaCast( "string", "MM/dd/yyyy 'at' h:mm aa" )
) />
<!---
By default, the date formatter uses the date in the default
timezone. However, since we are working with given calendar,
we want to set the timezone of the formatter to be that of
the calendar.
--->
<cfset local.formatter.setTimeZone(
arguments.calendar.getTimeZone()
) />
<!--- Return the formatted date in the given timezone. --->
<cfreturn local.formatter.format(
arguments.calendar.getTime()
) />
</cffunction>
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!---
Let's get the EST timezone. This will help us convert to and
from other timezone offsets.
--->
<cfset estTimezone = createObject( "java", "java.util.TimeZone" )
.getTimeZone(
javaCast( "string", "US/Eastern" )
)
/>
<!--- Create an EST calendar. --->
<cfset estCalendar = createObject( "java", "java.util.GregorianCalendar" ).init(
estTimezone
) />
<!---
Set the date for the EST calendar to be now() since my server
is located within the Easter Standard Timezone.
--->
<cfset today = now() />
<!---
Set the year, month, day, hour, and seconds.
NOTE: In the Java calendar, months start at 1 (not zero as in
ColdFusion). This is why we are subtracting 1 in the second
parameter in the following method call.
--->
<cfset estCalendar.set(
javaCast( "int", year( today ) ),
javaCast( "int", (month( today ) - 1) ),
javaCast( "int", day( today ) ),
javaCast( "int", hour( today ) ),
javaCast( "int", minute( today ) ),
javaCast( "int", second( today ) )
) />
<!---
Set the milliseconds in order to get a static time (otherwise
the calendar will be adding milliseconds based on the system
clock).
--->
<cfset estCalendar.set(
javaCast( "int", estCalendar.MILLISECOND ),
javaCast( "int", 0 )
) />
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!---
Now, let's create a PST timezone to see if we can convert time
back and forth between the two calendars.
--->
<cfset pstTimezone = createObject( "java", "java.util.TimeZone" )
.getTimeZone(
javaCast( "string", "US/Pacific" )
)
/>
<!--- Create an PST calendar. --->
<cfset pstCalendar = createObject( "java", "java.util.GregorianCalendar" ).init(
pstTimezone
) />
<!---
Set the time of the PST calendar using the time of the EST
calendar. This should take into account all of the offsets and
the daylight saving time calculations.
--->
<cfset pstCalendar.setTimeInMillis(
estCalendar.getTimeInMillis()
) />
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!---
Now, let's create an arizona timezone to make sure that this
will work with areas that do not adhere to the standard daylight
saving time (DST) rules.
--->
<cfset arizonaTimezone = createObject( "java", "java.util.TimeZone" )
.getTimeZone(
javaCast( "string", "US/Arizona" )
)
/>
<!--- Create an Arizona calendar. --->
<cfset arizonaCalendar = createObject( "java", "java.util.GregorianCalendar" ).init(
arizonaTimezone
) />
<!---
Set the time of the Arizone calendar using the time of the EST
calendar. This should take into account all of the offsets and
the daylight saving time calculations.
--->
<cfset arizonaCalendar.setTimeInMillis(
estCalendar.getTimeInMillis()
) />
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!---
Now, let's format all of the dates. Remember, since Arizona
does NOT use daylight saving time (DST), both the Pacific
Standard Time and Arizona shoudl be the same time (3 hours less
than Eastern Standard Time).
--->
<cfoutput>
EST: #calendarToString( estCalendar )#
<br />
PST: #calendarToString( pstCalendar )#
<br />
ARZ: #calendarToString( arizonaCalendar )#
</cfoutput>
At the top of this code, you'll notice that I have a function for formatting the date/time of a particular calendar. I need to do this because any attempt to use ColdFusion's dateFormat() or timeFormat() functions will end up formatting the date in the server's timezone (EST), not the given calendar's timezone.
Once I have the Eastern Standard Time (EST) calendar created, you can see that I am feeding the getTimeInMillis() return value of the EST calendar directly into the setTimeInMillis() method of the other two calendars. Doing this results in the following three date/time values:
EST: 09/23/2011 at 10:41 AM
PST: 09/23/2011 at 7:41 AM
ARZ: 09/23/2011 at 7:41 AM
As you can see, using the UTC-normalized Epoch offset, I was able to translate date/time values across time zones without having to worry about any of the GMT offsets and Daylight Saving Time (DST) rules! That's pretty awesome!
As I was reading up on all this timezone stuff, I came across a good post from Rob Brooks-Bilson on converting to and from this UTC-normalized Epoch offset. Since we know that Java Epoch is the number of milliseconds that have elapsed since January 1, 1970, we can get the local server time by adding the Epoch offset to the localized Epoch time:
<!---
Get the local server time (EST) using the UTC milliseconds from
Epoch as defined by the ARIZONA calendar.
--->
<cfset localDate = dateAdd(
"s",
(arizonaCalendar.getTimeInMillis() / 1000),
dateConvert( "utc2Local", "1970/01/01" )
) />
<!--- Test the conversion to local time. --->
<cfoutput>
Server Date:
#dateFormat( localDate, "mm/dd/yyyy" )# at
#timeFormat( localDate, "h:mm TT" )#
</cfoutput>
As you can see, we first convert the true Epoch to our server's Epoch using dateConvert(). Then, we simply add the number of milliseconds as defined by the Arizona calendar's getTimeInMillis() to the localized Epoch. Running the above code gives us the following date/time output:
Server Date: 09/23/2011 at 10:41 AM
As you can see, we got the same date/time as the Eastern Standard Time (EST) calendar from above.
Slowly, I'm starting to make heads and tails of all this timezone stuff. And, being able to dip into the Java layer definitely makes this a world easier than it would be otherwise. Now that I have a better understanding of how all the Epoch offset stuff works, I should be able to start fooling around with some database integration.
Want to use code from this post? Check out the license.
Reader Comments
Ben,
Why not just use the LSParseDateTime format to get the date/time in the format of the current locale? I then take that date/time and set it to GMT and then make tweaks based on the current person's locale using LSDateFormat and LSTimeFormat.
This gives you the output that you need based on the locale of the person inputting the data and allows you to save it to the server in the server's locale.
When displaying to new users it will display in their locale.
We had a system that was multi-lingual and had to work across the globe and this is the route that we went with.
@Braden,
I've never actually used any of the LS* functions. In fact, other than UTF-8 encoding for characters, this timezone exploration is really the first time I've looked at any of the globalization or localization functions. I'll definitely be taking at look at this LS* functions if you say they are valuable. Thanks!!
@Ben, I am glad you are making some Java posts since my company will soon be integrating and changing to Java, so there's another language I will be using your site for...I already use it for JQuery, css, ajax, database stuff, and ColdFusion. Why not Java, too? Love your Java stuff. Keep it coming. :-)
@Anna,
Ha ha, no problem :)
Thanks for sharing this approach. I will be looking into doing some heavy-duty localization efforts, and date/time stuff is a big piece that I am dreading. Looking forward to anything else that you'll write up, especially with the database stuff!
@Ben, because of course it is all about me. :-D haha. But seriously, hopefully, by the time we change our code over to Java, there will be enough stuff on here to float me on through. I have to admit, I'm a little nervous about it. But it's not like I have never had Java education before, so I am hoping I can lean on that, besides your site of course. :-) And I am a little excited about it, just a little anxious, too.
Some of the classes you're using are apparently not thread-safe, but, not being a Java programmer, I'll just post a link now...
http://blogs.atlassian.com/developer/2007/07/dateformat_objects_and_threads.html
@Chris,
This all go started because I wanted to schedule some time-based SMS integration :) That quickly lead down the rabbit hole. Been banging my head against this all week! Just happy to be making *some* headway. I'll be posting anything I find.
@Anna,
Just remember, ColdFusion sits on top of Java... so if you can do it in CF, you can do it in Java ... with 100x more code :P
@Danyal,
Yeah, the concept of thread-safe objects definitely is beyond my understanding of programming, at least at any real concrete level. I'll take a look at that link - thanks!
@Ben,
Thanks for that little piece of advice. I just wish some action would be taken. I am quite frankly kind of sick of listening to them bicker on the phone about whether ColdFusion could do this or that. The Java guys are pushing us to go to Java, because they claim that ColdFusion wasn't shipped with the latest version of Java, and so they are determined that we will be doing Java, so Java it is. They are saying that there are certain things that need to be done that can't be done using ColdFusion. Whatever. I'm just sick of all of the arguing. This is what made me sick of politics. lol. I guess it is just a different kind of politics now -- office politics.
Thread-safe objects ... hmm ... OK, in this context, an object is like a customer service rep.
A thread comes up for service and says hi, I'm January 4, 1998. The CSR says OK, let's see, what's your month ... January ... that's 01. Your date, 4, that's 04. Your year, 1998, yep. Here you go, 01/04/1998.
See, the CSR can't do it all at once, so he has to stop after each step. That's not a problem for a single-threaded app, but for a multi-threaded one, you can have issues.
Customer 1: Hi, I'm January 4, 1998.
CSR: OK, your month, January, that's 01.
Customer 2: Hi, I'm August 11, 1964.
CSR: OK, your date, 11, that's 11.
Customer 3: Hi, I'm November 22, 2024.
CSR: OK, your year, 2024, yep. Here you go, 01/11/2024.
(All three dates stare in confusion.)
When you have a rep (an object) that can't do everything in one shot, then you have to make sure that you don't let the rep get interrupted. He doesn't know the difference between customers(threads) because he can only ask one question at a time, so you have to wall off each thread.
In Java, you can kind of do that with the synchronized keyword ... http://download.oracle.com/javase/tutorial/essential/concurrency/locksync.html explains a little about how that works. Basically, you can lock code the same way that we can lock database work within a transaction. So then the above interaction would work like this with synchronization involved:
Customer 1: Hi, I'm January 4, 1998.
CSR: OK, your month, January, that's 01.
Customer 2: Hi, I'm August 11, 1964.
Intrinsic lock: Sorry, Customer 2, all lines are busy. Someone will be with you shortly.
CSR: OK, your date, 4, that's 04.
Customer 3: Hi, I'm November 22, 2024.
Intrinsic lock: Sorry, Customer 2, all lines are busy. Someone will be with you shortly.
Intrinsic lock: Sorry, Customer 3, all lines are busy. Someone will be with you shortly.
CSR: OK, your year, 1998, yep. Here you go, 01/04/1998.
Intrinsic lock: OK, the CSR is free.
...
So OK, minor Java explanation aside, what does it mean for CF people? Well ... um ... we don't write Java to call DateFormat, so we don't have a direct way to address that. My recommendation would be this: until you have a problem with DateFormat not returning the right date, don't worry about it. YAGNI. If it happens, you might be able to wrap that formatting code in a cflock to resolve the problem. (In an environment where concurrency is important, but order is also important, you might have to implement some sort of queue into which requests are placed, wrap the queue in a lock, and then process requests only from the queue.)
@Dave D,
Ahh, I see what you're saying. Yeah, since it appears to be one call on our end, it's not entirely apparent that behind the scenes it's making several calls to the same object for various points of data.
To be safe, I suppose I would just create a new DateFormatter and Date for each instance that I might need to use it. Probably, in a top-down page, I wouldn't see this being too much of a problem, though.
@Ben,
Right ... your example certainly doesn't need it, and people who want to adapt it to what they're using can always look at how it works to see if they might need some kind of locking mechanism.
I think this is another one of those things like you've said before: here's one way you can do something, a proof-of-concept type page. If you want to use it in a production environment, look it over carefully like you would any other existing code and adapt it to your needs.
Curious on why you didn't look at timezone.cfc. I've been using it for a long time and it works out nicely. The original project is up on riaforge.
@Ben,
In case you don't know yet, gmt = Greenwich Mean Time which is the international date line or UTC or Zulu time. All those terms equate to UTC 0 and EST (Eastern standard time) is -6, EDT (daylight) is -5 if I recall correctly.
@Tony,
The Timezone.cfc came up in my first few Google queries; but, I didn't really have a solid enough understanding of how time zones worked in general (and the massive diversity of them). I think I just needed to get a formal understanding of how world time was implemented before I started to look at any form of encapsulation.
Now that I've had a week of digging and some blog posts on top of it, I might try my own hand at some encapsulation.
@Randall,
I'm starting to wrap my head around all of this :) Slowly, steadily, the insanity is starting to make more sense.
Can I do the same thing without using createObject and CFObject ? I need to apply the same functionality but createObject and CFObject are not allowed.
Ben:
I am using this and it is working great locally but when i post it to my production server it is not working. I am using osx locally and centos in production. any idea why this is not working? I'm betting my head against the wall :(:(
help!?
Kevin