Tuesday, June 26, 2012

Time and Date management in Java

Java is very well equipped to manage dates and times. Some utility classes will make your live much easier than if you had to do it from scratch. There are a couple of things to understand first, though.

This problem has several different aspects:

Astronomical aspects

  • For a given date and time in location A, what time is it in location B?
  • What is the UTC time, now?

Legal aspects

  • For a given UTC time, what legal time is it in location X?
  • What is the first week of the year in country A?
  • What is the first week of the year in country B?

Misc aspects

  • What is the difference between Time Zone and Time Offset?
The following Objects are used to manage time in Java:
  • java.util.Date
  • java.util.Calendar
  • java.text.SimpleDateFormat
  • java.util.TimeZone
The TimeZone can be used with
  • the format
  • the calendar
This is probably the most important thing to remember here:
When you set the TimeZone at the Calendar level, you are setting the date at a given TimeZone. For example, March 1st 2012 at 12:00 Pacific Time.
When you set the TimeZone at the Format level, you do so to display the date & time set previously as viewed from another time zone. For example you set the date as above (US West Coast), and you want to view it from the US East Coast. Notice that in that case, the date remains unchanged. Only the way to display it is modified.
Note:
We are explicitly setting the TimeZones here, for clarity. If not explicitly set, the default TimeZone is used.
  private final static SimpleDateFormat SDF = new SimpleDateFormat("EEE dd MMMM yyyy HH:mm:ss.SSS Z (z)");    
    ...
    Calendar cal = Calendar.getInstance();
    cal.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
    cal.set(Calendar.YEAR, 2012);
    cal.set(Calendar.MONTH, Calendar.MARCH);
    cal.set(Calendar.DAY_OF_MONTH, 1);
    cal.set(Calendar.HOUR_OF_DAY, 12);
    cal.set(Calendar.MINUTE, 0);
    cal.set(Calendar.SECOND, 0);
    cal.set(Calendar.MILLISECOND, 0);
    SDF.setTimeZone(TimeZone.getTimeZone("America/New_York"));
    System.out.println("New York:     \t Date is " + SDF.format(cal.getTime()));    
    SDF.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
    System.out.println("San Francisco:\t Date is " + SDF.format(cal.getTime()));    
Output:
 New York:       Date is Thu 01 March 2012 15:00:00.000 -0500 (EST)
 San Francisco:  Date is Thu 01 March 2012 12:00:00.000 -0800 (PST)
More examples will be given below.

The default time zone is found from the TimeZone Object:
    TimeZone tz = TimeZone.getDefault();
    System.out.println("Default Time Zone:" + tz.getID() + ", " + tz.getDisplayName());
Output:
 Default Time Zone:America/Los_Angeles, Pacific Standard Time
The TimeZone object will also provide the list of all the valid time zones.
    String[] ids = TimeZone.getAvailableIDs();
    for (String s : ids)
      System.out.println(s);
Output:
 Etc/GMT+12
 Etc/GMT+11
 MIT
 Pacific/Apia
 Pacific/Midway
 Pacific/Niue
 Pacific/Pago_Pago
 Pacific/Samoa
 US/Samoa
 America/Adak
 America/Atka
 Etc/GMT+10
 HST
 Pacific/Fakaofo
 Pacific/Honolulu
 Pacific/Johnston
 Pacific/Rarotonga
 Pacific/Tahiti
 SystemV/HST10
 US/Aleutian
 ...
Notice in the list above the different kind of time zones:
  • Etc/GMT+12
  • PDT
  • America/Los_Angeles
  • Etc/GMT+12 refers to a fixed time offset, of 12 hours in that case
  • PDT refers to the time in use during the winter on the US West Coast
  • America/Los_Angeles refers to a location where the time in use can be PDT and PST. The time offset is different for those two times.
Let us illustrate this last point, we will display two dates, March first and June first:
  private final static SimpleDateFormat SDF = new SimpleDateFormat("EEE dd MMMM yyyy HH:mm:ss.SSS Z (z)");

  ...
    // March 1st 2012
    Calendar cal = GregorianCalendar.getInstance();
    SDF.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
    cal.set(Calendar.YEAR, 2012);
    cal.set(Calendar.MONTH, Calendar.MARCH);
    cal.set(Calendar.DAY_OF_MONTH, 1);
    System.out.println("Pacific Coast:\t Date is " + SDF.format(cal.getTime()));
    
    // June 1st 2012
    cal.set(Calendar.YEAR, 2012);
    cal.set(Calendar.MONTH, Calendar.JUNE);
    cal.set(Calendar.DAY_OF_MONTH, 1);
    System.out.println("Pacific Coast:\t Date is " + SDF.format(cal.getTime()));    
Output:
 Pacific Coast:  Date is Thu 01 March 2012 00:00:00.000 -0800 (PST)
 Pacific Coast:  Date is Fri 01 June 2012 00:00:00.000 -0700 (PDT)
Notice the Time in use, and the Time Offset. The Time Zone remains the same.
Some Time Zones use Daylight saving, other do not:
    cal.set(Calendar.YEAR, 2012);
    cal.set(Calendar.MONTH, Calendar.MARCH);
    cal.set(Calendar.DAY_OF_MONTH, 1);
    SDF.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
    System.out.println("Pacific Coast:\t Date is " + SDF.format(cal.getTime()));
    SDF.setTimeZone(TimeZone.getTimeZone("Pacific/Honolulu"));
    System.out.println("Hawaii:\t Date is " + SDF.format(cal.getTime()));

    cal.set(Calendar.YEAR, 2012);
    cal.set(Calendar.MONTH, Calendar.JUNE);
    cal.set(Calendar.DAY_OF_MONTH, 1);
    SDF.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
    System.out.println("Pacific Coast:\t Date is " + SDF.format(cal.getTime()));    
    SDF.setTimeZone(TimeZone.getTimeZone("Pacific/Honolulu"));
    System.out.println("Hawaii:\t Date is " + SDF.format(cal.getTime()));
Output:
 Pacific Coast:  Date is Thu 01 March 2012 00:00:00.000 -0800 (PST)
 Hawaii:         Date is Wed 29 February 2012 22:00:00.000 -1000 (HST)
 Pacific Coast:  Date is Fri 01 June 2012 00:00:00.000 -0700 (PDT)
 Hawaii:         Date is Thu 31 May 2012 21:00:00.000 -1000 (HST)
Hawaiian Standard Time remains the same, Pacific Time goes form Daylight to Standard.
The TimeZone can be set at the Format level, or at the Calendar level:
    Calendar cal = GregorianCalendar.getInstance();
    System.out.println("Cal:" + cal.getTime());
    System.out.println("Default Time zone:\t Date is " + SDF.format(cal.getTime()));
    // Set the time zone on the FORMAT to display the UTC time
    SDF.setTimeZone(TimeZone.getTimeZone("Etc/UTC"));
    System.out.println("Mask set to UTC:\t Date is " + SDF.format(cal.getTime()));
    
    // Set the TimeZone at the DATE level
    System.out.println("HOUR is     " + cal.get(Calendar.HOUR_OF_DAY) + ", before changing the Date's TZ");
    cal.setTimeZone(TimeZone.getTimeZone("Etc/UTC"));
    System.out.println("HOUR is now " + cal.get(Calendar.HOUR_OF_DAY));
    System.out.println("Date changed to UTC:\t Date is " + SDF.format(cal.getTime()));
    // The date remains the same.
    System.out.println("Cal:" + cal.getTime());
Output:
 Cal:Tue Jun 26 10:26:35 PDT 2012
 Default Time zone:    Date is Tue 26 June 2012 10:26:35.606 -0700 (PDT)
 Mask set to UTC:      Date is Tue 26 June 2012 17:26:35.606 +0000 (UTC)
 HOUR is     10, before changing the Date's TZ
 HOUR is now 17
 Date changed to UTC:  Date is Tue 26 June 2012 17:26:35.606 +0000 (UTC)
 Cal:Tue Jun 26 10:26:35 PDT 2012
Notice above that changing the Date's TimeZone automatically adjusts the offsets (the HOUR_OF_DAY in that case). Only the TimeZone is changed, not the actual original date, it still describes the same instant, the exact same point in time.

For a given UTC Time, what time is it in San Francisco, and in Hawai'i:
    Calendar cal = GregorianCalendar.getInstance();
    cal.setTimeZone(TimeZone.getTimeZone("Etc/UTC")); 
    cal.set(Calendar.YEAR, 2012);
    cal.set(Calendar.MONTH, Calendar.MARCH);
    cal.set(Calendar.DAY_OF_MONTH, 1);
    cal.set(Calendar.HOUR_OF_DAY, 12);
    cal.set(Calendar.MINUTE, 0);
    cal.set(Calendar.SECOND, 0);
    cal.set(Calendar.MILLISECOND, 0);
    SDF.setTimeZone(TimeZone.getTimeZone("Etc/UTC"));
    System.out.println("UTC:\t Date is " + SDF.format(cal.getTime()));    
    SDF.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
    System.out.println("San Francisco:\t Date is " + SDF.format(cal.getTime()));    
    SDF.setTimeZone(TimeZone.getTimeZone("Pacific/Honolulu"));
    System.out.println("Hawaii:\t Date is " + SDF.format(cal.getTime()));    
In the code, we set the TimeZone at the Calendar level, meaning that we set the date at March 1st, 2012, at 12:00 UTC. Then we display it, viewed from "Etc/UTC", "America/Los_Angeles", and "Pacific/Honolulu".
 UTC:            Date is Thu 01 March 2012 12:00:00.000 +0000 (UTC)
 San Francisco:  Date is Thu 01 March 2012 04:00:00.000 -0800 (PST)
 Hawaii:         Date is Thu 01 March 2012 02:00:00.000 -1000 (HST)
Also, notice something important: some TimeZones do not have the same TimeOffsets all over the year. The Time Offset is automatically adjusted when you use a TimeZone subject to daylight saving.

You can manipulate the Calendar directly, without having to use a format. Let's assume you've set - like above - a Calendar at 12:00 UTC, on March 1st 2012. And you want to know the hour of the day on the US West Coast:
    cal.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
    System.out.println(cal.get(Calendar.YEAR) + "-" + 
                      (cal.get(Calendar.MONTH) + 1) + "-" +
                       cal.get(Calendar.DAY_OF_MONTH) + ", " + 
                       cal.get(Calendar.HOUR_OF_DAY) + " hour(s).);
Output:
 2012-3-1, 4 hour(s).
Notice that the months index begins with 0 (we had to add 1).

An aspect of the Country, the legal time and time zone

In some countries, the week begins on Sundays. In some others, it begins on Mondays.
Let us run the following code different ways:
    Calendar cal = new GregorianCalendar(2012, Calendar.JANUARY, 1);
    SimpleDateFormat sdf = new SimpleDateFormat("EEEE dd-MMMM-yyyy '\tis in week' w");    
    System.out.println(sdf.format(cal.getTime()));
The US way:
 Prompt> java -Duser.language=en -Duser.country=US sometests.FirstWeekTest
 Sunday 01-January-2012  is in week 1
The British way:
 Prompt> java -Duser.language=en -Duser.country=GB sometests.FirstWeekTest
 Sunday 01-January-2012  is in week 52
The French way:
 Prompt> java -Duser.language=fr -Duser.country=FR sometests.FirstWeekTest
 dimanche 01-janvier-2012  is in week 52
The Swedish way:
 Prompt> java -Duser.language=sv -Duser.country=SE sometests.FirstWeekTest 
 söndag 01-januari-2012  is in week 52
Sunday is the first day of the week in the US, but not in Europe. As a result, the number of the week is affected! Week #1 is the first one to begin in the year.
In Europe, January 1st 2012 is part of the last week of 2011. This last week's number is 53 (yes 53, this is not a typo) in the US, 52 in Europe.
This can be something to consider when making cross-continent appointments, like in "Let us meet on week 17".

Never underestimate date and time management, it can be a quite complex problem, to say the least.
At the end of the XIXth century, a clerk was affected to the synchronization of the clocks on the railroad network of Switzerland.
He was later awarded the Nobel Price of Physics in 1921.
His name was Albert Einstein...

1 comment: