1/*
2 *******************************************************************************
3 * Copyright (C) 1996-2014, International Business Machines Corporation and    *
4 * others. All Rights Reserved.                                                *
5 *******************************************************************************
6 */
7
8package com.ibm.icu.util;
9
10import java.util.Date;
11
12/**
13 * <b>Note:</b> The Holiday framework is a technology preview.
14 * Despite its age, is still draft API, and clients should treat it as such.
15 *
16 * A Holiday subclass which represents holidays that occur
17 * a fixed number of days before or after Easter.  Supports both the
18 * Western and Orthodox methods for calculating Easter.
19 * @draft ICU 2.8 (retainAll)
20 * @provisional This API might change or be removed in a future release.
21 */
22public class EasterHoliday extends Holiday
23{
24    /**
25     * Construct a holiday that falls on Easter Sunday every year
26     *
27     * @param name The name of the holiday
28     * @draft ICU 2.8
29     * @provisional This API might change or be removed in a future release.
30     */
31    public EasterHoliday(String name)
32    {
33        super(name, new EasterRule(0, false));
34    }
35
36    /**
37     * Construct a holiday that falls a specified number of days before
38     * or after Easter Sunday each year.
39     *
40     * @param daysAfter The number of days before (-) or after (+) Easter
41     * @param name      The name of the holiday
42     * @draft ICU 2.8
43     * @provisional This API might change or be removed in a future release.
44     */
45    public EasterHoliday(int daysAfter, String name)
46    {
47        super(name, new EasterRule(daysAfter, false));
48    }
49
50    /**
51     * Construct a holiday that falls a specified number of days before
52     * or after Easter Sunday each year, using either the Western
53     * or Orthodox calendar.
54     *
55     * @param daysAfter The number of days before (-) or after (+) Easter
56     * @param orthodox  Use the Orthodox calendar?
57     * @param name      The name of the holiday
58     * @draft ICU 2.8
59     * @provisional This API might change or be removed in a future release.
60     */
61    public EasterHoliday(int daysAfter, boolean orthodox, String name)
62    {
63        super(name, new EasterRule(daysAfter, orthodox));
64    }
65
66    /**
67     * Shrove Tuesday, aka Mardi Gras, 48 days before Easter
68     * @draft ICU 2.8
69     * @provisional This API might change or be removed in a future release.
70     */
71    static public final EasterHoliday SHROVE_TUESDAY  = new EasterHoliday(-48,    "Shrove Tuesday");
72
73    /**
74     * Ash Wednesday, start of Lent, 47 days before Easter
75     * @draft ICU 2.8
76     * @provisional This API might change or be removed in a future release.
77     */
78    static public final EasterHoliday ASH_WEDNESDAY   = new EasterHoliday(-47,    "Ash Wednesday");
79
80    /**
81     * Palm Sunday, 7 days before Easter
82     * @draft ICU 2.8
83     * @provisional This API might change or be removed in a future release.
84     */
85    static public final EasterHoliday PALM_SUNDAY     = new EasterHoliday( -7,    "Palm Sunday");
86
87    /**
88     * Maundy Thursday, 3 days before Easter
89     * @draft ICU 2.8
90     * @provisional This API might change or be removed in a future release.
91     */
92    static public final EasterHoliday MAUNDY_THURSDAY = new EasterHoliday( -3,    "Maundy Thursday");
93
94    /**
95     * Good Friday, 2 days before Easter
96     * @draft ICU 2.8
97     * @provisional This API might change or be removed in a future release.
98     */
99    static public final EasterHoliday GOOD_FRIDAY     = new EasterHoliday( -2,    "Good Friday");
100
101    /**
102     * Easter Sunday
103     * @draft ICU 2.8
104     * @provisional This API might change or be removed in a future release.
105     */
106    static public final EasterHoliday EASTER_SUNDAY   = new EasterHoliday(  0,    "Easter Sunday");
107
108    /**
109     * Easter Monday, 1 day after Easter
110     * @draft ICU 2.8
111     * @provisional This API might change or be removed in a future release.
112     */
113    static public final EasterHoliday EASTER_MONDAY   = new EasterHoliday(  1,    "Easter Monday");
114
115    /**
116     * Ascension, 39 days after Easter
117     * @draft ICU 2.8
118     * @provisional This API might change or be removed in a future release.
119     */
120    static public final EasterHoliday ASCENSION       = new EasterHoliday( 39,    "Ascension");
121
122    /**
123     * Pentecost (aka Whit Sunday), 49 days after Easter
124     * @draft ICU 2.8
125     * @provisional This API might change or be removed in a future release.
126     */
127    static public final EasterHoliday PENTECOST       = new EasterHoliday( 49,    "Pentecost");
128
129    /**
130     * Whit Sunday (aka Pentecost), 49 days after Easter
131     * @draft ICU 2.8
132     * @provisional This API might change or be removed in a future release.
133     */
134    static public final EasterHoliday WHIT_SUNDAY     = new EasterHoliday( 49,    "Whit Sunday");
135
136    /**
137     * Whit Monday, 50 days after Easter
138     * @draft ICU 2.8
139     * @provisional This API might change or be removed in a future release.
140     */
141    static public final EasterHoliday WHIT_MONDAY     = new EasterHoliday( 50,    "Whit Monday");
142
143    /**
144     * Corpus Christi, 60 days after Easter
145     * @draft ICU 2.8
146     * @provisional This API might change or be removed in a future release.
147     */
148    static public final EasterHoliday CORPUS_CHRISTI  = new EasterHoliday( 60,    "Corpus Christi");
149}
150
151class EasterRule implements DateRule {
152    public EasterRule(int daysAfterEaster, boolean isOrthodox) {
153        this.daysAfterEaster = daysAfterEaster;
154        if (isOrthodox) {
155            orthodox.setGregorianChange(new Date(Long.MAX_VALUE));
156            calendar = orthodox;
157        }
158    }
159
160    /**
161     * Return the first occurrance of this rule on or after the given date
162     */
163    public Date firstAfter(Date start)
164    {
165        return doFirstBetween(start, null);
166    }
167
168    /**
169     * Return the first occurrance of this rule on or after
170     * the given start date and before the given end date.
171     */
172    public Date firstBetween(Date start, Date end)
173    {
174        return doFirstBetween(start, end);
175    }
176
177    /**
178     * Return true if the given Date is on the same day as Easter
179     */
180    public boolean isOn(Date date)
181    {
182        synchronized(calendar) {
183            calendar.setTime(date);
184            int dayOfYear = calendar.get(Calendar.DAY_OF_YEAR);
185
186            calendar.setTime(computeInYear(calendar.getTime(), calendar));
187
188            return calendar.get(Calendar.DAY_OF_YEAR) == dayOfYear;
189        }
190    }
191
192    /**
193     * Return true if Easter occurs between the two dates given
194     */
195    public boolean isBetween(Date start, Date end)
196    {
197        return firstBetween(start, end) != null; // TODO: optimize?
198    }
199
200    private Date doFirstBetween(Date start, Date end)
201    {
202        //System.out.println("doFirstBetween: start   = " + start.toString());
203        //System.out.println("doFirstBetween: end     = " + end.toString());
204
205        synchronized(calendar) {
206            // Figure out when this holiday lands in the given year
207            Date result = computeInYear(start, calendar);
208
209         //System.out.println("                result  = " + result.toString());
210
211            // We might have gotten a date that's in the same year as "start", but
212            // earlier in the year.  If so, go to next year
213            if (result.before(start))
214            {
215                calendar.setTime(start);
216                calendar.get(Calendar.YEAR);    // JDK 1.1.2 bug workaround
217                calendar.add(Calendar.YEAR, 1);
218
219                //System.out.println("                Result before start, going to next year: "
220                //                        + calendar.getTime().toString());
221
222                result = computeInYear(calendar.getTime(), calendar);
223                //System.out.println("                result  = " + result.toString());
224            }
225
226            if (end != null && !result.before(end)) {
227                //System.out.println("Result after end, returning null");
228                return null;
229            }
230            return result;
231        }
232    }
233
234    /**
235     * Compute the month and date on which this holiday falls in the year
236     * containing the date "date".  First figure out which date Easter
237     * lands on in this year, and then add the offset for this holiday to get
238     * the right date.
239     * <p>
240     * The algorithm here is taken from the
241     * <a href="http://www.faqs.org/faqs/calendars/faq/">Calendar FAQ</a>.
242     */
243    private Date computeInYear(Date date, GregorianCalendar cal)
244    {
245        if (cal == null) cal = calendar;
246
247        synchronized(cal) {
248            cal.setTime(date);
249
250            int year = cal.get(Calendar.YEAR);
251            int g = year % 19;  // "Golden Number" of year - 1
252            int i = 0;          // # of days from 3/21 to the Paschal full moon
253            int j = 0;          // Weekday (0-based) of Paschal full moon
254
255            if (cal.getTime().after( cal.getGregorianChange()))
256            {
257                // We're past the Gregorian switchover, so use the Gregorian rules.
258                int c = year / 100;
259                int h = (c - c/4 - (8*c+13)/25 + 19*g + 15) % 30;
260                i = h - (h/28)*(1 - (h/28)*(29/(h+1))*((21-g)/11));
261                j = (year + year/4 + i + 2 - c + c/4) % 7;
262            }
263            else
264            {
265                // Use the old Julian rules.
266                i = (19*g + 15) % 30;
267                j = (year + year/4 + i) % 7;
268            }
269            int l = i - j;
270            int m = 3 + (l+40)/44;              // 1-based month in which Easter falls
271            int d = l + 28 - 31*(m/4);          // Date of Easter within that month
272
273            cal.clear();
274            cal.set(Calendar.ERA, GregorianCalendar.AD);
275            cal.set(Calendar.YEAR, year);
276            cal.set(Calendar.MONTH, m-1);       // 0-based
277            cal.set(Calendar.DATE, d);
278            cal.getTime();                      // JDK 1.1.2 bug workaround
279            cal.add(Calendar.DATE, daysAfterEaster);
280
281            return cal.getTime();
282        }
283    }
284
285    private static GregorianCalendar gregorian = new GregorianCalendar(/* new SimpleTimeZone(0, "UTC") */);
286    private static GregorianCalendar orthodox = new GregorianCalendar(/* new SimpleTimeZone(0, "UTC") */);
287
288    private int               daysAfterEaster;
289    private GregorianCalendar calendar = gregorian;
290}
291