Java Tip 44: Calculating holidays and their observances

Figure out all of those cool days off that you get for holidays

"Write once, run anywhere" is a big promise to keep when you are writing a Java applet to be used on the Internet. Most programmers may realize that the Internet goes further than the boundaries of countries, but their coding does not seem to reflect this in practice.

Many applets tend to assume that the user is in the same locale and country as the author of the code. What could be considered a straightforward applet may have a bigger impact on the user than was originally planned, especially when the user is using the applet in another locale, whose political borders -- but not the linguistic or cultural borders -- may have been erased by the Internet. Applets need to be cognizant of the environment in which they actually will run and need to determine on the fly what actions and behaviors to display.

Let's assume I wrote a Java calendar applet to be included on a Web page so that users could view and select certain dates for business purposes, for example, while avoiding a holiday. At first glance, this seems to be a simple project to complete. The initial question that pops into programmers' minds probably is "Which holidays do I indicate -- government/banking holidays and/or stock exchange holidays?" While this may seem to be a small wrinkle to iron out before coding, the problem is actually more involved than it appears. If we assume that all of the applet's users are from English-speaking countries that share the same holiday structure as the U.S., then any one of the multitude of calendar applets found in any random Java book would suffice. Unfortunately, this is real life, and the world is a little more complicated than a simple "How to..." book. So our little applet is going to dynamically calculate some of the holidays used in the U.S., Canada, and the rest of the world. By calculating the holidays, we can take the first step toward creating a truly dynamic runtime calendar applet that automatically adapts itself to the user's locale and displays the pertinent holidays for that user.

The facilities and idiosyncracies of Java

Before we start with the coding of the applet, let's take a look at some of the important facilities in Java that we will utilize. Java doesn't have the primitive date type that exists in many third- and fourth-generation languages. The "Date" object in Java is just that: an object used to encapsulate integer primitive types to store the year, month, date, and time values, along with some basic data manipulation methods. There is nothing exotic or glamorous there! The "Calendar" and "Gregorian Calendar" objects enhance the limited date manipulation facilities with a few more methods but overall are quite disappointing. The "Locale" object was released with the Java Development Kit (JDK) 1.1 and serves as a mechanism by which an applet determines the environment it must conform to as dictated by the norms of the user's country and language. Unfortunately, the Locale object currently seems to be more of a documentation myth: I have not been able to figure out how to extract the Locale information without first setting it. Numerous participants in the various Internet Java forums also have had no success getting the Locale object to work. It would be nice to see it auto-initialize and become an accurate source of information at runtime for applets. (Side note: If anyone knows how to successfully get the Locale object to perform, it would make a great follow up article to this one!)

I have decided to write a calendar applet that, theoretically, could be used in any country that has access to the Internet. Therefore, I need to examine the particular issues surrounding different calendars and holidays across the many countries whose people will be able to access the applet. In some countries, it is customary to start a calendar week on Monday instead of on Sunday. Some countries prefer to represent days on a calendar in a circle, instead of in "our" traditional box. You can see that what seemed like a fairly straightforward task -- that of constructing a calendar applet -- has quickly become a complex task.

Because covering every country, language, custom, and holiday would be prohibitive here, I will try to give you an overview of the coding, algorithms, and techniques required to fully deploy an international calendar, and the facets of Java required to complete our goals. For those of you who wish to explore further some the topics I have mentioned in this article, I recommend "The Calendar FAQ" and Global Interface Design (see the Resources section below).

Java will throw a number of curve balls to those who are not accustomed to its idiosyncrasies. For example, Date.getYear() returns a two-digit year (YYYY - 1900) and months numbered from zero to eleven, while days are numbered one to thirty-one. I seem to have a memory block with how many days are in each month and continually "shoot myself in the foot" on this one item. These sorts of "gotchas" can make the fairly straightforward calculation of holidays for the calendar a challenging proposition.

Calculating the observance of holidays

In the Gregorian calendar, most business holidays are fairly straightforward. They tend to follow one of these rules:

  • The holiday falls on the same month and date each year.

  • If the actual holiday falls on a Saturday, then frequently businesses provide a day off on the preceding Friday; if the holiday falls on a Sunday, then the observance is on the Monday after the holiday.

  • The holiday follows a mathematical cycle that is not easy for the user to distinguish. Easter Sunday, and, by default, the New York Stock Exchange- (NYSE) observed holiday, Good Friday, are based on a formula which repeats itself every 5,700,000 years in the Gregorian calendar. Good luck trying to find a calendar in the stationary store that goes that far ahead!

  • Holidays are observed through a combination of the above-mentioned rules.

Calculating exactly which Sunday Easter Sunday falls on is difficult compared to the other holidays, because originally it was based on the Hebrew calendar. The Hebrew calendar, along with Mayan and Islamic calendars, are lunar calendars, while the Gregorian calendar we observe is not. A static holiday on a lunar calendar like Easter or Passover requires quite a bit of math to translate it to the Gregorian calendar, this is why Easter and Passover seem to "rove" around the months of March and April of our calendar because they are following the moon and its phases!

The Islamic and Gregorian calendars

The Islamic calendar (or "Hijri" calendar) is purely a lunar calendar. It contains 12 months that are based on the motion of the moon, and because 12 synodic months are only 29.53 days, and 12 * 29.53 = 354.36 days, the Islamic calendar is consistently shorter than a 365-day tropical year. Therefore, the Islamic calendar shifts with respect to the Gregorian calendar. The Islamic calendar is the official calendar in countries around the Gulf, especially Saudi Arabia. But other Muslim countries use the Gregorian calendar for civil purposes and only turn to the Islamic calendar for religious purposes.

For added complexity, the months in the Islamic calendar only change when the lunar crescent is first seen (by an actual human being) after a new moon. Although new moons may be calculated quite precisely, the actual visibility of the crescent is much more difficult to predict. It depends on factors such as weather, the atmosphere, and the location of the observer. It is therefore very difficult to give accurate information in advance about when a new month will start. Some Muslims depend on a local sighting of the moon, whereas others depend on a sighting by authorities somewhere in the Muslim world. Both are valid Islamic practices, but they may lead to different starting days for the months.

Coding for the U.S. holidays

Let's take a look at some of the U.S. holidays -- when they are observed and how we can calculate them using Java. Here are some of the standard United States holidays that are calculated according to the following rules: New Year's Day observance seems fairly simple at first until we remember the wrinkle in the calculation that if the holiday falls on a Saturday, then often the day off is the preceding Friday, December 31st -- New Year's Eve.

    public static Date NewYearsDay (int nYear)
    {
    // January 1st
    int nMonth = 0; // January
    return new Date(nYear, nMonth, 1);
    }
    public static Date NewYearsDayObserved (int nYear)
    {
    int nX;
    int nMonth = 0;         // January
    int nMonthDecember = 11;    // December
    Date dtD;
    dtD = new Date(nYear, nMonth, 1);
    nX = dtD.getDay();
    if (nYear > 1900)
        {
        nYear -= 1900;
        }
    switch(nX)
        {
        case 0 : // Sunday
        return new Date(nYear, nMonth, 2);
        case 1 : // Monday
        case 2 : // Tuesday
        case 3 : // Wednesday
        case 4 : // Thursday
        case 5 : // Friday
        return new Date(nYear, nMonth, 1);
        default :
        // Saturday, then observe on friday of previous year
        return new Date(--nYear, nMonthDecember, 31);
        }
    }

Martin Luther King Day is observed on the third Monday in January. The most complicated aspect of this holiday is determining which states and municipalities observe it! Certain states do not recognize this day formally as a holiday and conduct business as usual.

    public Date MartinLutherKingObserved (int nYear)
    {
    // Third Monday in January
    int nX;
    int nMonth = 0; // January
    Date dtD;
    dtD = new Date(nYear, nMonth, 1);
    nX = dtD.getDay();
    switch(nX)
        {
        case 0 : // Sunday
        return new Date(nYear, nMonth, 16);
        case 1 : // Monday
        return new Date(nYear, nMonth, 15);
        case 2 : // Tuesday
        return new Date(nYear, nMonth, 21);
        case 3 : // Wednesday
        return new Date(nYear, nMonth, 20);
        case 4 : // Thursday
        return new Date(nYear, nMonth, 19);
        case 5 : // Friday
        return new Date(nYear, nMonth, 18);
        default : // Saturday
        return new Date(nYear, nMonth, 17);
        }
    }

President's Day, formerly Washington and Lincoln's birthday, is celebrated on the third Monday in February.

    public static Date PresidentsDayObserved (int nYear)
    {
    // Third Monday in February
    int nX;
    int nMonth = 1; // February
    Date dtD;
    dtD = new Date(nYear, nMonth, 1);
    nX = dtD.getDay();
    switch(nX)
        {
        case 0 : // Sunday
        return new Date(nYear, nMonth, 16);
        case 1 : // Monday
        return new Date(nYear, nMonth, 15);
        case 2 : // Tuesday
        return new Date(nYear, nMonth, 21);
        case 3 : // Wednesday
        return new Date(nYear, nMonth, 20);
        case 4 : // Thursday
        return new Date(nYear, nMonth, 19);
        case 5 : // Friday
        return new Date(nYear, nMonth, 18);
        default : // Saturday
        return new Date(nYear, nMonth, 17);
        }
    }

Good Friday, as observed by the New York Stock Exchange, or Easter Monday, observed in the United Kingdom, can only be accurately calculated using a formula that places Easter Sunday on the Gregorian calendar. The formula used to calculate Easter is complex in theory but easy enough for a beginning Java programmer to code accurately.

    public static Date GoodFridayObserved(int nYear)
    {
    // Get Easter Sunday and subtract two days
    int nEasterMonth    = 0;
    int nEasterDay      = 0;
    int nGoodFridayMonth    = 0;
    int nGoodFridayDay  = 0;
    Date dEasterSunday;
    dEasterSunday = EasterSunday(nYear);
    nEasterMonth = dEasterSunday.getMonth();
    nEasterDay = dEasterSunday.getDate();
    if (nEasterDay <= 3 && nEasterMonth == 3) // Check if <= April 3rd
        {
        switch(nEasterDay)
        {
        case 3 : 
            nGoodFridayMonth = nEasterMonth - 1;
            nGoodFridayDay   = nEasterDay - 2;
            break;
        case 2 :
            nGoodFridayMonth = nEasterMonth - 1;
            nGoodFridayDay   = 31;
            break;
        case 1 :
            nGoodFridayMonth = nEasterMonth - 1;
            nGoodFridayDay   = 31;
            break;
        default:
            nGoodFridayMonth = nEasterMonth;
            nGoodFridayDay   = nEasterDay - 2;
        }
        }
    else
        {
        nGoodFridayMonth = nEasterMonth;
        nGoodFridayDay   = nEasterDay - 2;
        }
    return new Date(nYear, nGoodFridayMonth, nGoodFridayDay);
    }

Memorial Day, the last Monday in May, returns us back to relatively simple date calculations.

    public static Date MemorialDayObserved (int nYear)
    {
    // Last Monday in May
    int nX;
    int nMonth = 4; //May
    Date dtD;
    dtD = new Date(nYear, nMonth, 31);
    nX = dtD.getDay();
    switch(nX)
        {
        case 0 : // Sunday
        return new Date(nYear, nMonth, 25);
        case 1 : // Monday
        return new Date(nYear, nMonth, 31);
        case 2 : // Tuesday
        return new Date(nYear, nMonth, 30);
        case 3 : // Wednesday
        return new Date(nYear, nMonth, 29);
        case 4 : // Thursday
        return new Date(nYear, nMonth, 28);
        case 5 : // Friday
        return new Date(nYear, nMonth, 27);
        default : // Saturday
        return new Date(nYear, nMonth, 26);
        }
    }
Related:
1 2 Page 1
Page 1 of 2