HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML

Java Quirks - Dates And Calendars, by Roedy Green

Dealing with dates and times is probably the most confusing aspect of Java for newbies. There are three reasons for this:

  1. The date and time classes are the most poorly designed of all the Sun class libraries.
  2. The standard class libraries force you to deal with time zone and time of day, even when they're irrelevant to your problem.
  3. The vocabulary used in the various date classes is inconsistent.
Vocabulary
  • Day of month: The day of the month 1.31, sometimes called the date.
  • Day of week: Day of the week for a given date; for example, Sunday=1 ..Saturday=7.
  • DST offset (daylight saving time offset): The number of millisecond correction to account for daylight saving time; 0 if daylight saving time is not in effect for the time stamp specified. If a one-hour daylight saving is in effect, the offset will be 3,600,000. Add both the DST and the zone offsets to UTC to get local time.
  • ISO day of week: Day of the week 1 to 7 for this date according to the ISO standard IS-8601. Monday=1...Sunday=7.
  • ISO week number: Week number 1 to 53 of the year that this date falls in, according to the rules of the ISO standard IS-8601, section 5.5. A week that lies partly in one year and partly in another is assigned a number in the year in which most of its days lie. This means that week 1 of any year is the week that contains 4 January or, equivalently, week 1 of any year is the week that contains the first Thursday in January. Most years have 52 weeks, but years that start on a Thursday and leap years that start on a Wednesday have 53 weeks. January 1 may well be in week 53 of the previous year!
  • Month of year: January to December; note that in GregorianCalendar, January is month 0. In contrast, in DateFormat, January is month 1.
  • Time stamp: An instant in cosmic time expressed in milliseconds since January 1, 1970, 0:00 in UTC. It can be a positive or negative 64-bit-long number. These are sometimes called dates or times.
  • Offset: How many milliseconds difference local time is from UTC. If you live in North America this will be a negative number. It's the sum of both the zone and DST offsets. You add the DST offset and the zone offset to UTC to get local time.
  • UTC: For all practical purposes it's just standard time in Greenwich, England. The people in Greenwich use daylight saving time, so the term GMT or Greenwich time would be ambiguous.
  • Week of year: There are many possible definitions. The default GregorianCalendar definition depends on whether you consider Sunday or Monday as the first day of the week, setFirstDayOfWeek (the default is locale specific), and how many days you insist must be present in the first week of the year, setMinimalDaysInFirstWeek (default 1). The first week of the year is week 1. January 1 may sometimes be considered in week 53 of the previous year.
  • Zone offset: How many milliseconds difference local time is from UTC if you ignore any daylight saving time correction. If you live in North America, this will be a negative number. You add both the DST and the zone offsets to UTC to get local time.

The Cast
You need to use quite a few different classes to solve even a simple problem involving dates (see Table 1). For details on these classes download the Sun JavaDoc from http://java.sun.com/j2se/ 1.3/#documentation.


Table 1

Displaying a Date
This program converts a date to a String for display using the default date format (see Listing 1). That format depends on what the user has configured as his or her preferred date format in the OS.

Note: You must tell the SimpleDateFormat your GregorianDate twice, once to get the time zone and once to get the time stamp.

Displaying a Date with Precise Control
If you want to control how your date looks, use a mask like the one in Listing 2.

Note: You must tell the SimpleDateFormat your GregorianDate twice, once to get the time zone and once to get the time stamp.

Parsing a Date
To convert a date from a String to an internal format is quite a production. Listing 3 shows one way of doing it.

Note: Be very careful with time zones. If you don't specify one, your date will be interpreted using the the local default time zone. If the user hasn't configured it correctly in his or her OS, you may get Pacific standard time or GMT without warning.

Elapsed Time in Hours Between Two Time Stamps
Look at the example program in Listing 4 to calculate how many hours until the next presidential inauguration.

Note: Specify the time stamp with GregorianCalendar.set in terms of eastern standard time, not UTC. Internally the time stamp is stored as UTC.

  • Don't create the TimeZone object with new.
  • The getTime().getTime() is not an error. The first getTime retrieves a Date object from GregorianCalendar, and the second retrieves a time stamp from the Date object.
  • Elapsed time in days is not simply hours/24. It's much more complicated than that.

How Long Until Christmas, Daddy?
This sounds like a simple problem. Programmers posted many different solutions to the comp.lang.java.programmer newsgroup before the gurus stopped finding holes in the logic. Part of the problem is that the question can have many answers. I show eight plausible solutions in Listing 5.

Gotchas
java.util.GregorianCalendar has far fewer bugs and gotchas than the old java.util.Date class, but it's still no picnic.

Had there been programmers when daylight saving time was first proposed, they would have vetoed it as insane and intractable. With daylight saving there's a fundamental ambiguity. In the fall when you set your clocks back one hour at 2 a.m., there are two different instants in time both called 1:30 a.m. local time. You can tell them apart only if you record whether you intended daylight saving or standard time with the reading. Unfortunately, there's no way to tell GregorianCalendar which one you intended. You must resort to telling it the local time with the dummy UTC time zone to avoid the ambiguity. Programmers usually close their eyes to this problem and just hope nobody does anything during this hour.

Millennium bugs are still not out of the calendar classes. Even in JDK 1.3 there's a 2001 bug. Consider the following code:

gc = new GregorianCalendar();
gc.setLenient(false );
/* Bug only manifests if lenient set false */
gc.set(2001 , 1, 1, 1, 0, 0);
nYear = gc.get (Calendar.YEAR);
/* throws exception */ The bug disappears at 7AM on 2001/01/01 for MST.
GregorianCalendar is controlled by a giant pile of untyped int magic constants. This technique totally destroys any hope of compile-time error checking. For example, to get the month use Gregorian- Calendar.get(Calendar.MONTH)).

GregorianCalendar has the raw GregorianCalendar.get(Calendar.ZONE_OFFSET) and the daylight saving GregorianCalendar.get(Calendar.DST_OFFSET), but no way to get the actual time zone offset being used. You must get these two separately and add them together.

GregorianCalendar.set(year, month, day, hour, minute) doesn't set the seconds to 0.

DateFormat and GregorianCalendar don't mesh properly. You must specify the Calendar twice, once indirectly as a Date.

If the user hasn't configured the time zone correctly, it'll default quietly to either PST or GMT.

In GregorianCalendar, months are numbered starting at January=0, rather than 1 as everyone else on the planet does. Yet days start at 1, as do days of the week with Sunday=1, Monday=2...Saturday=7. However, DateFormat.parse behaves in the traditional way with January=1.

Learning More
The calendar classes are full of surprises. To learn more about them see http://mindprod.com/gotchas.html#DATE. Dealing with pure dates is much simpler using the BigDate class. Download it from http://mindprod.com/products.html#BIGDATE.

Author Bio
Roedy Green maintains the Java glossary of Canadian Mind Products (http://mindprod.com). He spends most of his time in the comp.lang.java.* newsgroups helping newbies and has been writing custom computer programs since 1963.
[email protected]

	


Listing 1

import java.text.DateFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;
// display inauguration day as a String
public class Inauguration1
{
static public void main(String [] args)
{
// inauguration day is Thursday 2005 January 20 noon Eastern
                Standard Time.
TimeZone est = TimeZone.getTimeZone ("America/New_York");
GregorianCalendar inauguration = new GregorianCalendar(est);
inauguration.set(2005, Calendar.JANUARY, 20, 12, 0, 0);


// locale specific: e.g. Jan 20, 2005
DateFormat df = DateFormat.getDateInstance();


// set timezone
df.setCalendar(inauguration);


// set timestamp
String dateString = df.format(inauguration.getTime());
System.out.println (dateString);
        }
}


Listing 2

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;


// display inauguration day as a String
public class Inauguration2
{
static public void main(String [] args)
{
// inauguration day is Thursday 2005 January 20 noon Eastern
         Standard Time.
TimeZone est = TimeZone.getTimeZone ("America/New_York");
GregorianCalendar inauguration = new GregorianCalendar(est);
inauguration.set(2005, Calendar.JANUARY, 20, 12, 0, 0);
// mask for: Thursday 2005/01/20 12:00:00 PM EST Eastern Standard Time
SimpleDateFormat sdf = new SimpleDateFormat
("EEEE yyyy/MM/dd hh:mm:ss aa zz : zzzzzz");


// set timezone
sdf.setCalendar(inauguration);


// set timestamp
String dateString = sdf.format(inauguration .getTime());
System.out.println (dateString);
        }
}



Listing 3

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;
// parse a date
public class Inauguration3
{
static public void main(String [] args)
{
String dateString = "2005/01/20";
// define the format of the date
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
// set timezone to use in interpreting the date
TimeZone est = TimeZone.getTimeZone ("America/New_York");
GregorianCalendar inauguration = new GregorianCalendar(est);
sdf.setCalendar(inauguration);
try
{
// set timestamp
Date date = sdf.parse (dateString);
inauguration.setTime(date);
// inauguration is now ready with both timezone and timestamp
}
catch (ParseException e)
{
System.out.println ("bad date");
}
}
}


Listing 4

import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;
// Hours until the next presidential inauguration
public class Inauguration4
{
static final int MILLIS_PER_HOUR = 60 * 60 * 1000;
static public void main(String [] args)
{
// inauguration day is Thursday 2005 January 20 noon Eastern Standard Time.
TimeZone est = TimeZone.getTimeZone ("America/New_York");
GregorianCalendar inauguration = new GregorianCalendar(est);
inauguration.set(2005, Calendar.JANUARY, 20, 12, 0, 0);
// now is current time, using default timezone
GregorianCalendar now = new GregorianCalendar();
// milliseconds since 1970 Jan 1
long epochInauguration = inauguration.getTime( ).getTime( );
long epochNow = now.getTime().getTime();


double hours = (double) (epochInauguration - epochNow)/
                MILLIS_PER_HOUR;
System.out.println (hours + " hours until the inauguration.");
}
}



Listing 5

import cmp.business.BigDate;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;
/**
* How Long until Christmas?
*/
public class Christmas
{
static final int MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
public static void main (String [] args)
{
// How long till Christmas? (Sun's way.)
// this code will give a negative number just after Christmas.
System.out.println ("Your child asks, how long is it until "
+ "Christmas?");
System.out.println ("Here are the possible answers you might give"
+ " to an American child:");
GregorianCalendar now = new GregorianCalendar();
int thisYear = now.get(Calendar .YEAR);


// You may open presents at 7 AM December 25, local time
System.out.println ("You may open your presents at 7 AM Christmas"
+ " morning." );
GregorianCalendar christmas = new GregorianCalendar(thisYear, 
Calendar.DECEMBER, 25, 7, 0, 0);
// millis since 1970 Jan 1
long christmasTimeStamp = christmas.getTime( ).getTime( );
long nowTimeStamp = now.getTime() .getTime();
double dayUnitsDiff = (christmasTimeStamp - nowTimeStamp) /
(double) MILLISECONDS_PER_DAY;
System.out.println ("1. It is " + dayUnitsDiff + " day units of 24"
+ " hours until you may open your presents.");
System.out.println ("2. It is " + Math.ceil(dayUnitsDiff) + " day"
+ " units of 24 hours rounded up until you may open your "
+ "presents.");
System.out.println ("3. It is " + Math.round(dayUnitsDiff) +
" day units of 24 hours rounded until you may open your "
+ "presents.");
System.out.println ("4. It is " + Math.floor(dayUnitsDiff) +
" day units of 24 hours rounded down until you may open your"
+ " presents.");


int gmtChristmasOrdinal = (int) (christmasTimeStamp /
MILLISECONDS_PER_DAY);
int gmtNowOrdinal = (int) (nowTimeStamp / MILLISECONDS_PER_DAY);
int gmtDiffInDays = gmtChristmasOrdinal - gmtNowOrdinal;
System.out.println ("5. Children in Greenwich have " +
 gmtDiffInDays + " sleeps (midnight crossings) to go until\n" 
 + "you may open your presents.\n" +
"They may open theirs even sooner.");


// For children living in the USA, the timezone offset will be negative.
int christmasZoneOffset = christmas.get(Calendar .ZONE_OFFSET)
                now.get(Calendar .DST_OFFSET);
int localChristmasOrdinal = (int) ((christmasTimeStamp +
                christmasZoneOffset) / MILLISECONDS_PER_DAY);
int nowZoneOffset = now.get(Calendar .ZONE_OFFSET) +
                now.get(Calendar .DST_OFFSET);
int localNowOrdinal = (int) ( (nowTimeStamp + nowZoneOffset) /
                MILLISECONDS_PER_DAY);
int localDiffInDays = localChristmasOrdinal - localNowOrdinal;
System.out.println ("6. You have " + localDiffInDays +
" sleeps (midnight crossings) to go.");


// how long till Christmas? ( with BigDate. )
// this code will give a negative number just after Christmas.
BigDate today = BigDate .localToday();
BigDate nextChristmas = new BigDate(today.getYYYY( ), 12, 25);
System.out.println ("7. Uncle Roedy's Bigdate says it is " 
+ (nextChristmas .getOrdinal() - today.getOrdinal()) +
" sleeps until Christmas.");
int [ ] age = BigDate .age( today, nextChristmas);
System.out.println ("8. Or put another way " + age[0 ] +
" years and " + age [1] + " months and " + age[2] + " days to"
+ "go.");
}
}



  
 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.