Friday, December 7, 2012

Time Zones and Daylight Saving Time in Java

You might think this would be simple but I actually spent a fair amount of time last week tracking down some confusing bugs. The problem is that the official documentation is pretty sparse and if you Google for support you will find many answers that are confused or flat-out wrong. Hopefully this will provide the straight dope.
The key Java classes are as follows:

java.util.TimeZone

This is an abstract class that contains the definition of a time zone, including that zone’s rules for Daylight Saving Time (or what Europeans refer to as “Summer Time.”) Any code that needs to explicitly deal with time zones should use a TimeZone object.
The best way to get a TimeZone is to call the static method TimeZone.getTimeZone() and pass it the standard “Olson name” such as “America/New_York” or “Pacific/Honolulu”.
This link lists all the standard time zone names.
You can call TimeZone.getTimeZone(“GMT”) or TimeZone.getTimeZone(“GMT-5″). None of these GMT-based TimeZone objects support Daylight Saving Time. (Note: “GMT” and “UTC” mean practically the same thing. GMT is defined in terms of astronomical observations and UTC is used for setting atomic clocks. For most practical purposes they can be treated as identical.)
You can also use common abbreviations e.g. TimeZone.getTimeZone(“EST”) instead of TimeZone.getTimeZone(“America/New_York”). This is strongly discouraged. Depending on circumstances you might get the wrong Daylight Saving Time behavior or even the totally wrong time zone, since the same 3-letter abbreviations are used around the world for different time zones.
When displaying a time zone to the user you should probably call TimeZone.getDisplayName(). Depending on the parameters you pass this will return a user-friendly value like “Eastern Standard Time”, “EST”, “Eastern Daylight Time” or “EDT”.

java.util.Calendar

This is an abstract class which serves as a wrapper around two independent values:
  • A time, stored as the number of milliseconds since January 1, 1970 00:000:00 GMT.
  • A TimeZone object which indicates how the time should be displayed.
(This is an oversimplification. The actual implementation is a a bit more complicated, but this is close enough as long as you are not actually digging into the source code.)
Note that the time offset is always supposed to be in GMT. If you see code samples that make a different assumption (and there are many out there on the web) ignore them.
Time zone conversions are simple: if you call setTimeZone() on a Calendar object you get the exact same time but displayed in the new time zone.
A more complex problem occurs when you get a time string from a user in a different time zone. If you parse the string “05-01-2012 08:35 AM” then the parser will generally give you a Calendar object for 8:35 AM in the computer’s default time zone.
If this is wrong then you will need to change the time offset to convert it to the correct time. If the time string was supposed to be in GMT then you can use the folowing code to convert it.

public static Calendar convertToGmt(Calendar c) {
    java.util.Date date = c.getTime();
    TimeZone tz = c.getTimeZone();
    long timeInMilliseconds = date.getTime();
    int offsetFromUTC = tz.getOffset(timeInMilliseconds);
    Calendar gmtCal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
    gmtCal.setTime(date);
    gmtCal.add(Calendar.MILLISECOND, offsetFromUTC);
    return gmtCal;
}

If it was supposed to be in a different time zone then you can call TimeZone.getOffset() for both time zones. The difference between the two values will give you the number of milliseconds that you need to add to do the conversion.
This code provides an alternate way to convert between arbitrary time zones.

public static Calendar convertToNewTimeZone(Calendar calendar, TimeZone timezone) {
    Calendar newCal = new GregorianCalendar(timezone);
    newCal.setLenient(false);
    boolean am = newCal.get(Calendar.AM_PM) == Calendar.AM;
    newCal.set(Calendar.YEAR, calendar.get(Calendar.YEAR));
    newCal.set(Calendar.MONTH, calendar.get(Calendar.MONTH));
    newCal.set(Calendar.DATE, calendar.get(Calendar.DATE));
    newCal.set(Calendar.HOUR, calendar.get(Calendar.HOUR));
    newCal.set(Calendar.MINUTE, calendar.get(Calendar.MINUTE));
    newCal.set(Calendar.SECOND, calendar.get(Calendar.SECOND));
    newCal.set(Calendar.MILLISECOND, calendar.get(Calendar.MILLISECOND));
    boolean ampm = calendar.get(Calendar.AM_PM) == Calendar.PM;
    if (am && ampm) { // cal = 0 but we want 1
        newCal.roll(Calendar.AM_PM, 1);
    } else if (!am && !ampm) { //cal = 1 but we want 0
        newCal.roll(Calendar.AM_PM, -1);
    }
    return newCal;
}

Once again, this gives you a Calendar object with the same wall-clock time in a different time zone, as opposed to getting the same actual time in a different time zone.

ISO 8601

To avoid such problems when sending dates and times between different time zones you can use the ISO 8601 formats commonly used in XML documents. These formats allow an optional trailing time zone indicator e.g.
2012-05-01T08:35:01.123Z
2012-05-01T08:35:01.123-05:00
A “Z” code indicates that the time is GMT. A “-05:00″ indicates a time zone that is 5 hours behind GMT. In the U.S. this could mean either “Eastern Standard Time” or “Central Daylight Time”.
Most standard XML libraries can handle these formats.
In the “-05:00″ example above the parser will return a Calendar subclass whose TimeZone object is “GMT-5″, not “America/New_York” or “America/Chicago”. You have the correct time but you don’t really know which official time zone it is.
The time zone indicator is optional. If the document contains
2012-05-01T08:35:01.123
that will be interpreted as being in the receiving computer’s default time zone.

java.util.Date

This is a wrapper around a count of milliseconds since midnight January 1, 1970. There is no associated time zone.
According to the documentation the millisecond count should always be in GMT, but this is often ignored. You will find many code samples on the web that attempt to deal with time zones by adding or subtracting hours. This is NOT recommended.
If you need to deal with time zones you should use a Calendar object.
The Date class has methods like getHours() and getMinutes() which are all deprecated. If you use them they will return the value in the computer’s default time zone. Date.toString() will also display in the computer’s default time zone.

java.sql.Date

This is intended to represent a SQL DATE field. The Java implementation is a simple wrapper around java.util.Date which makes sure that the time part is always set to midnight in your computer’s default time zone.
What is actually stored in the database depends on the implementation but can be assumed to consist of a year, month and day in some format.

java.sql.Time

This is intended to represent a SQL TIME field. The Java implementation is a thin wrapper around java.util.Date which makes sure that the date part is always set to January 1, 1970.
What is actually stored in the database depends on the implementation but can be assumed to consist of either an offset from midnight or a combination of hour, minutes and seconds in some format.
There is no support for time zones built in. If time zones are important the application will have to keep track of them separately.

java.sql.TimeStamp

This intended to represent a SQL TIMESTAMP field. The Java implementation is similar to java.util.Date in that it contains an offset from a fixed starting time, but it is much higher precision, supporting fractions of a microsecond instead of milliseconds.
What is actually stored in the database depends on the implementation but is usually some sort of offset from a starting date.
TimeStamp is similar to java.util.Date in that
  • It does not contain a TimeZone.
  • The internal offset is supposed to be in GMT and bad things can happen if it is not.
  • It is typically displayed in the computer’s default time zone.
Usually you create a TimeStamp from a java.util.Date object. (If you are starting with a Calendar object just call getTime() on it.) So basically you are writing out the millisecond offset in the GMT time zone. If you started with a Calendar its original time zone is lost.
When you read a TimeStamp from the database you are getting back the time with no time zone information. Calling getDate() will return it as a java.util.Date object. If you want it in a time zone other than your computer’s default time zone you can do something like this:

Calendar cal = new GregorianCalendar(desiredTimeZone);
cal.setTime(ts.getDate);


http://bugfox.net/blog/2012/05/04/time-zones-and-daylight-saving-time-in-java/

No comments:

Post a Comment