Sunday, January 14, 2007

Not Exactly Masters of Time and Space...

Code example and downloadable project corrected on 2007/01/16.

Previous correction note corrected on January 28, 2007. That is January. And the editors of the last correction have been sacked. And their moose also.


We are coming up on a change in the rules of Daylight Saving Time (DST) in the United States. The US government has written quite a bit about it. Sun has updated the JVM to deal with it. Apple has even updated its JVM to deal with it. But Apple has not yet updated WebObjects.

Even if an update to WebObjects is shipped before the DST change, one can still have a problem right now. If an application stores a date or time anywhere between March 11, 2007 and April 1, 2007, any arithmetic on that date might have a problem.

Let us try an example. Suppose that it is 7 pm on January 13, 2007 and I request an alarm for two months from now. Then I ask the application when that alarm will occur. The application says that the alarm will occur at March 13, 2007 at 7 pm. The application would be wrong.

One can fix the javafoundation.jar file, which I will describe below. But it is possible to avoid many problems of this type. I am going to write more about this later, perhaps much more, but a short answer is this. First, a date and time without a time zone means nothing. If your application lets users put in dates and times without letting them specify the time zone, you are asking for trouble. Second, always store dates and times in UTC. Period. Simply put, databases should never know about any time zone except UTC, and a user should never have to guess what the assumed time zone is for a particular date and time string.

If I followed these rules, I would be able to figure out the problem. I created the alarm at 7 pm on January 13, 2007, PST (Pacific Standard Time). Then I ask the application when that alarm will occur. The application says that the alarm will occur at 7 pm on March 13, 2007, PST. The application would still be wrong. But knowing that March 13 is after the DST change, I could figure out that 7 pm in PST is equal to 8 pm in PDT, which stands for Pacific Daylight Time. It may still be confusing, but if one displays the time zones, one does have enough information to figure out what has happened.

If you end up having a problem from the DST change, your problem may not look like this. But these kinds of application problems are confusing. Something that depends on date calculations may break in subtle ways. You may understand what is happening when you see it. But imagine explaining this to your manager, or your manager's manager. I would not want this job.

Enough talk. Here is some code, in the form of a jUnit test. The project that contains this code can be found here.

public void test2006SpringForward() {
NSTimestampFormatter formatter = new NSTimestampFormatter("%Y/%m/%d %H:%M:%S %Z");

String target1 = "2006/04/02 01:59:00 America/Los_Angeles";
String target2 = "2006/04/02 03:00:00 America/Los_Angeles";

// start at 1 minute before 2:00 AM on April 2, 2006 in the Pacific time zone
NSTimestamp ts1 = new NSTimestamp(2006, 4, 2, 9, 59, 0, TimeZone.getTimeZone("America/Los_Angeles"));

assertEquals("creating time", target1, formatter.format(ts1));

// add 1 minute to bring us to 12:59, a minute before midnight
NSTimestamp ts2 = ts1.timestampByAddingGregorianUnits(0, 0, 0, 0, 1, 0);

assertEquals("flipped over DST or not? ("+target2+") != ("+formatter.format(ts2)+")", target2, formatter.format(ts2));
}

public void test2007SpringForward() {
NSTimestampFormatter formatter = new NSTimestampFormatter("%Y/%m/%d %H:%M:%S %Z");

String target1 = "2007/03/11 01:59:00 America/Los_Angeles";
String target2 = "2007/03/11 03:00:00 America/Los_Angeles";

// start at 1 minute before 2:00 AM on March 11, 2007 in the Pacific time zone
NSTimestamp ts1 = new NSTimestamp(2007, 3, 11, 9, 59, 0, TimeZone.getTimeZone("America/Los_Angeles"));

assertEquals("creating time", target1, formatter.format(ts1));

// add 1 minute to bring us to 12:59, a minute before midnight
NSTimestamp ts2 = ts1.timestampByAddingGregorianUnits(0, 0, 0, 0, 1, 0);

assertEquals("flipped over DST or not? ("+target2+") != ("+formatter.format(ts2)+")", target2, formatter.format(ts2));
}


On my system, the first test passes and the second test fails. We have a problem.

% ant exec

exec:
[exec] .F.flipped over DST or not? (2007/03/11 03:00:00 America/Los_Angeles) != (2007/03/11 02:00:00 America/Los_Angeles) expected:<...3...> but was:<...2...>


It turns out that the fix is really very simple on the system I am using right now, which is running Mac OS X 10.4.8, with the latest versions of the JVM. I am tempted to try to describe the solution for Windows and Solaris, as well as Mac OS X, but I only have access to Mac OS X at the moment. On Mac OS X systems, the updates to the JVM that have already occurred have fixed the DST problem for most applications. But, for historical reasons, WebObjects uses its own version of the time zone information, and this has led us to the current problem.

I did put this in an ant build.xml file, though. Hopefully, this will make the use of it clearer to people not using Mac OS X. Here is the part of the build.xml that fixes the problem. You can change the properties below if needed on your system. You must run this with administrator privileges and manually invoke the fixFoundation target.

<property name="SLF" value="/System/Library/Frameworks" />
<property name="fRJ" value="framework/Resources/Java" />

<property name="zoneInfoDir" value="/usr/share/zoneinfo" />

<property name="jarfile" value="${SLF}/JavaFoundation.${fRJ}/javafoundation.jar" />

<target name="fixFoundation">
<tempfile property="temp" />
<mkdir dir="${temp}" />
<unjar src="${jarfile}" dest="${temp}" />
<delete quiet="true" file="${temp}/com/webobjects/foundation/TimeZoneInfo/zoneinfo.zip" />
<zip destfile="${temp}/com/webobjects/foundation/TimeZoneInfo/zoneinfo.zip" basedir="${zoneInfoDir}" excludes="*.tab,**/Riyadh87,**/Riyadh88,**/Riyadh89,**/\+VERSION" />
<move file="${jarfile}" tofile="${jarfile}_bak" />
<jar basedir="${temp}" destfile="${jarfile}" />
<delete quiet="true" dir="${temp}" />

</target>


Now, I can run the test above and see the following:

% ant exec

exec:
[exec] ..

BUILD SUCCESSFUL
Total time: 1 second
%


References:
http://www.energy.ca.gov/daylightsaving.html
http://developer.apple.com/documentation/webobjects/Reference/API/
http://www.faqs.org/docs/Linux-HOWTO/TimePrecision-HOWTO.html
http://ant.apache.org/manual/

3 Comments:

At 10:27 PM , Blogger GavinEadie said...

Ray and I swapped some notes this evening which reveal how many errors can creep into a tiny piece of code and how poking at them can reveal more!!

Ray noticed that "America/Los Angeles" is not a time zone, but "America/Los_Angeles" is -- this bug reveals itself if you are in a different zone ("America/New_York") in my case.

I noticed, while iterating through all the timezones that WebObjects knows about that (a) "Asia/Riyadh87" might be broken, and (b) WebObjects assumes every file in ".../foundation/TimeZoneInfo/zoneinfo.zip" and "/usr/share/zoneinfo/" is a timezone table -- they are not.

 
At 10:58 AM , Anonymous Colin Curtin said...

Hi Ray, just read your post to WO-Dev from yesterday (http://lists.apple.com/archives/Webobjects-dev/2007/Jan/msg00204.html). Are you going to change the code in this article to reflect your findings?

 
At 12:11 PM , Anonymous name said...

Please write anything else!

 

Post a Comment

Subscribe to Post Comments [Atom]

<< Home