EasterHoliday.java
// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
*******************************************************************************
* Copyright (C) 1996-2014, International Business Machines Corporation and *
* others. All Rights Reserved. *
*******************************************************************************
*/
package com.ibm.icu.util;
import java.util.Date;
/**
* <b>Note:</b> The Holiday framework is a technology preview. Despite its age, is still draft API,
* and clients should treat it as such.
*
* <p>A Holiday subclass which represents holidays that occur a fixed number of days before or after
* Easter. Supports both the Western and Orthodox methods for calculating Easter.
*
* @draft ICU 2.8 (retainAll)
*/
public class EasterHoliday extends Holiday {
/**
* Construct a holiday that falls on Easter Sunday every year
*
* @param name The name of the holiday
* @draft ICU 2.8
*/
public EasterHoliday(String name) {
super(name, new EasterRule(0, false));
}
/**
* Construct a holiday that falls a specified number of days before or after Easter Sunday each
* year.
*
* @param daysAfter The number of days before (-) or after (+) Easter
* @param name The name of the holiday
* @draft ICU 2.8
*/
public EasterHoliday(int daysAfter, String name) {
super(name, new EasterRule(daysAfter, false));
}
/**
* Construct a holiday that falls a specified number of days before or after Easter Sunday each
* year, using either the Western or Orthodox calendar.
*
* @param daysAfter The number of days before (-) or after (+) Easter
* @param orthodox Use the Orthodox calendar?
* @param name The name of the holiday
* @draft ICU 2.8
*/
public EasterHoliday(int daysAfter, boolean orthodox, String name) {
super(name, new EasterRule(daysAfter, orthodox));
}
/**
* Shrove Tuesday, aka Mardi Gras, 48 days before Easter
*
* @draft ICU 2.8
*/
public static final EasterHoliday SHROVE_TUESDAY = new EasterHoliday(-48, "Shrove Tuesday");
/**
* Ash Wednesday, start of Lent, 47 days before Easter
*
* @draft ICU 2.8
*/
public static final EasterHoliday ASH_WEDNESDAY = new EasterHoliday(-47, "Ash Wednesday");
/**
* Palm Sunday, 7 days before Easter
*
* @draft ICU 2.8
*/
public static final EasterHoliday PALM_SUNDAY = new EasterHoliday(-7, "Palm Sunday");
/**
* Maundy Thursday, 3 days before Easter
*
* @draft ICU 2.8
*/
public static final EasterHoliday MAUNDY_THURSDAY = new EasterHoliday(-3, "Maundy Thursday");
/**
* Good Friday, 2 days before Easter
*
* @draft ICU 2.8
*/
public static final EasterHoliday GOOD_FRIDAY = new EasterHoliday(-2, "Good Friday");
/**
* Easter Sunday
*
* @draft ICU 2.8
*/
public static final EasterHoliday EASTER_SUNDAY = new EasterHoliday(0, "Easter Sunday");
/**
* Easter Monday, 1 day after Easter
*
* @draft ICU 2.8
*/
public static final EasterHoliday EASTER_MONDAY = new EasterHoliday(1, "Easter Monday");
/**
* Ascension, 39 days after Easter
*
* @draft ICU 2.8
*/
public static final EasterHoliday ASCENSION = new EasterHoliday(39, "Ascension");
/**
* Pentecost (aka Whit Sunday), 49 days after Easter
*
* @draft ICU 2.8
*/
public static final EasterHoliday PENTECOST = new EasterHoliday(49, "Pentecost");
/**
* Whit Sunday (aka Pentecost), 49 days after Easter
*
* @draft ICU 2.8
*/
public static final EasterHoliday WHIT_SUNDAY = new EasterHoliday(49, "Whit Sunday");
/**
* Whit Monday, 50 days after Easter
*
* @draft ICU 2.8
*/
public static final EasterHoliday WHIT_MONDAY = new EasterHoliday(50, "Whit Monday");
/**
* Corpus Christi, 60 days after Easter
*
* @draft ICU 2.8
*/
public static final EasterHoliday CORPUS_CHRISTI = new EasterHoliday(60, "Corpus Christi");
}
class EasterRule implements DateRule {
public EasterRule(int daysAfterEaster, boolean isOrthodox) {
this.daysAfterEaster = daysAfterEaster;
if (isOrthodox) {
calendar.setGregorianChange(new Date(Long.MAX_VALUE));
}
}
/** Return the first occurrence of this rule on or after the given date */
@Override
public Date firstAfter(Date start) {
return doFirstBetween(start, null);
}
/**
* Return the first occurrence of this rule on or after the given start date and before the
* given end date.
*/
@Override
public Date firstBetween(Date start, Date end) {
return doFirstBetween(start, end);
}
/** Return true if the given Date is on the same day as Easter */
@Override
public boolean isOn(Date date) {
synchronized (calendar) {
calendar.setTime(date);
int dayOfYear = calendar.get(Calendar.DAY_OF_YEAR);
calendar.setTime(computeInYear(calendar.getTime(), calendar));
return calendar.get(Calendar.DAY_OF_YEAR) == dayOfYear;
}
}
/** Return true if Easter occurs between the two dates given */
@Override
public boolean isBetween(Date start, Date end) {
return firstBetween(start, end) != null; // TODO: optimize?
}
private Date doFirstBetween(Date start, Date end) {
// System.out.println("doFirstBetween: start = " + start.toString());
// System.out.println("doFirstBetween: end = " + end.toString());
synchronized (calendar) {
// Figure out when this holiday lands in the given year
Date result = computeInYear(start, calendar);
// System.out.println(" result = " + result.toString());
// We might have gotten a date that's in the same year as "start", but
// earlier in the year. If so, go to next year
if (result.before(start)) {
calendar.setTime(start);
calendar.get(Calendar.YEAR); // JDK 1.1.2 bug workaround
calendar.add(Calendar.YEAR, 1);
// System.out.println(" Result before start, going to next year: "
// + calendar.getTime().toString());
result = computeInYear(calendar.getTime(), calendar);
// System.out.println(" result = " + result.toString());
}
if (end != null && !result.before(end)) {
// System.out.println("Result after end, returning null");
return null;
}
return result;
}
}
/**
* Compute the month and date on which this holiday falls in the year containing the date
* "date". First figure out which date Easter lands on in this year, and then add the offset for
* this holiday to get the right date.
*
* <p>The algorithm here is taken from the <a
* href="http://www.faqs.org/faqs/calendars/faq/">Calendar FAQ</a>.
*/
private Date computeInYear(Date date, GregorianCalendar cal) {
if (cal == null) cal = calendar;
synchronized (cal) {
cal.setTime(date);
int year = cal.get(Calendar.YEAR);
int g = year % 19; // "Golden Number" of year - 1
int i = 0; // # of days from 3/21 to the Paschal full moon
int j = 0; // Weekday (0-based) of Paschal full moon
if (cal.getTime().after(cal.getGregorianChange())) {
// We're past the Gregorian switchover, so use the Gregorian rules.
int c = year / 100;
int h = (c - c / 4 - (8 * c + 13) / 25 + 19 * g + 15) % 30;
i = h - (h / 28) * (1 - (h / 28) * (29 / (h + 1)) * ((21 - g) / 11));
j = (year + year / 4 + i + 2 - c + c / 4) % 7;
} else {
// Use the old Julian rules.
i = (19 * g + 15) % 30;
j = (year + year / 4 + i) % 7;
}
int l = i - j;
int m = 3 + (l + 40) / 44; // 1-based month in which Easter falls
int d = l + 28 - 31 * (m / 4); // Date of Easter within that month
cal.clear();
cal.set(Calendar.ERA, GregorianCalendar.AD);
cal.set(Calendar.YEAR, year);
cal.set(Calendar.MONTH, m - 1); // 0-based
cal.set(Calendar.DATE, d);
cal.getTime(); // JDK 1.1.2 bug workaround
cal.add(Calendar.DATE, daysAfterEaster);
return cal.getTime();
}
}
private int daysAfterEaster;
private GregorianCalendar calendar = new GregorianCalendar();
}