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