001    /*
002    // $Id: //open/mondrian/src/main/mondrian/util/Schedule.java#8 $
003    // This software is subject to the terms of the Common Public License
004    // Agreement, available at the following URL:
005    // http://www.opensource.org/licenses/cpl.html.
006    // Copyright (C) 2002-2006 Julian Hyde
007    // All Rights Reserved.
008    // You must accept the terms of that agreement to use this software.
009    */
010    
011    package mondrian.util;
012    
013    import java.sql.Time;
014    import java.util.Calendar;
015    import java.util.Date;
016    import java.util.TimeZone;
017    
018    /**
019     * A <code>Schedule</code> generates a series of time events.
020     *
021     * <p> Create a schedule using one of the factory methods:<ul>
022     * <li>{@link #createOnce},</li>
023     * <li>{@link #createDaily},</li>
024     * <li>{@link #createWeekly},</li>
025     * <li>{@link #createMonthlyByDay},</li>
026     * <li>{@link #createMonthlyByWeek}.</li></ul>
027     *
028     * <p> Then use the {@link #nextOccurrence} method to find the next occurrence
029     * after a particular point in time.
030     *
031     * <p> The <code>begin</code> and <code>end</code> parameters represent the
032     * points in time between which the schedule is active. Both are optional.
033     * However, if a schedule type supports a <code>period</code> parameter, and
034     * you supply a value greater than 1, <code>begin</code> is used to determine
035     * the start of the cycle. If <code>begin</code> is not specified, the cycle
036     * starts at the epoch (January 1st, 1970).
037     *
038     * <p> The {@link Date} parameters in this API -- <code>begin</code> and
039     * <code>end</code>, the <code>time</code> parameter to {@link #createOnce},
040     * and the <code>earliestDate</code> parameter and value returned from {@link
041     * #nextOccurrence} -- always represent a point in time (GMT), not a local
042     * time.  If a schedule is to start at 12 noon Tokyo time, April 1st, 2002, it
043     * is the application's reponsibility to convert this into a UTC {@link Date}
044     * value.
045     *
046     * @author jhyde
047     * @since May 7, 2002
048     * @version $Id: //open/mondrian/src/main/mondrian/util/Schedule.java#8 $
049     */
050    public class Schedule {
051    
052        // members
053    
054        private DateSchedule dateSchedule;
055        private TimeSchedule timeSchedule;
056        private TimeZone tz;
057        private Date begin;
058        private Date end;
059    
060        // constants
061    
062        /**
063         * Indicates that a schedule should fire on the last day of the month.
064         * @see #createMonthlyByDay
065         */
066        public static final int LAST_DAY_OF_MONTH = 0;
067        /**
068         * Indicates that a schedule should fire on the last week of the month.
069         * @see #createMonthlyByWeek
070         */
071        public static final int LAST_WEEK_OF_MONTH = 0;
072    
073        static final TimeZone utcTimeZone = TimeZone.getTimeZone("UTC");
074    
075        static final int allDaysOfWeekBitmap =
076                (1 << Calendar.MONDAY) |
077                (1 << Calendar.TUESDAY) |
078                (1 << Calendar.WEDNESDAY) |
079                (1 << Calendar.THURSDAY) |
080                (1 << Calendar.FRIDAY) |
081                (1 << Calendar.SATURDAY) |
082                (1 << Calendar.SUNDAY);
083        static final int allDaysOfMonthBitmap = 0xefffFffe | // bits 1..31
084                (1 << LAST_DAY_OF_MONTH);
085        static final int allWeeksOfMonthBitmap = 0x0000003e | // bits 1..5
086                (1 << LAST_WEEK_OF_MONTH);
087    
088        // constructor(s) and factory methods
089    
090        /**
091         * Please use the factory methods {@link #createDaily} etc. to create a
092         * Schedule.
093         */
094        private Schedule(
095                DateSchedule dateSchedule,
096                TimeSchedule timeSchedule,
097                TimeZone tz,
098                Date begin,
099                Date end) {
100            this.dateSchedule = dateSchedule;
101            this.timeSchedule = timeSchedule;
102            this.tz = tz;
103            this.begin = begin;
104            this.end = end;
105        }
106    
107        /**
108         * Creates a calendar which fires only once.
109         *
110         * @param date date and time to fire, must be UTC
111         * @param tz timezone
112         *
113         * @pre tz != null
114         * @pre date != null
115         * @post return != null
116         */
117        public static Schedule createOnce(Date date, TimeZone tz) {
118            Calendar calendar = ScheduleUtil.createCalendar(date);
119            Time timeOfDay = ScheduleUtil.createTime(
120                    calendar.get(Calendar.HOUR_OF_DAY),
121                    calendar.get(Calendar.MINUTE),
122                    calendar.get(Calendar.SECOND));
123            calendar.add(Calendar.SECOND, 1);
124            Date datePlusDelta = calendar.getTime();
125            return createDaily(date, datePlusDelta, tz, timeOfDay, 1);
126        }
127    
128        /**
129         * Creates a calendar which fires every day.
130         *
131         * @param begin open lower bound, may be null, must be UTC
132         * @param end closed upper bound, may be null, must be UTC
133         * @param tz timezone
134         * @param timeOfDay time at which to fire
135         * @param period causes the schedule to fire every <code>period</code>
136         *     days. If <code>period</code> is greater than 1, the cycle starts
137         *     at the begin point of the schedule, or at the epoch (1 January,
138         *     1970) if <code>begin</code> is not specified.
139         *
140         * @pre tz != null
141         * @pre period > 0
142         * @post return != null
143         */
144        public static Schedule createDaily(
145                Date begin, Date end, TimeZone tz, Time timeOfDay, int period) {
146            DateSchedule dateSchedule = new DailyDateSchedule(
147                    begin == null ? null : ScheduleUtil.createCalendar(begin),
148                    period);
149            return new Schedule(
150                    dateSchedule,
151                    new OnceTimeSchedule(ScheduleUtil.createTimeCalendar(timeOfDay)),
152                    tz,
153                    begin,
154                    end);
155        }
156    
157        /**
158         * Creates a calendar which fires on particular days each week.
159         *
160         * @param tz timezone
161         * @param daysOfWeekBitmap a bitmap of day values, for example
162         *     <code>(1 << {@link Calendar#TUESDAY}) |
163         *           (1 << {@link Calendar#THURSDAY})</code> to fire on Tuesdays
164         *     and Thursdays
165         * @param timeOfDay time at which to fire
166         * @param begin open lower bound, may be null
167         * @param end closed upper bound, may be null
168         * @param period causes the schedule to be active every <code>period</code>
169         *     weeks. If <code>period</code> is greater than 1, the cycle starts
170         *     at the begin point of the schedule, or at the epoch (1 January,
171         *     1970) if <code>begin</code> is not specified.
172         *
173         * @pre tz != null
174         * @pre period > 0
175         * @post return != null
176         */
177        public static Schedule createWeekly(
178                Date begin, Date end, TimeZone tz,
179                Time timeOfDay, int period, int daysOfWeekBitmap) {
180            DateSchedule dateSchedule = new WeeklyDateSchedule(
181                    begin == null ? null : ScheduleUtil.createCalendar(begin),
182                    period,
183                    daysOfWeekBitmap);
184            return new Schedule(
185                    dateSchedule,
186                    new OnceTimeSchedule(ScheduleUtil.createTimeCalendar(timeOfDay)),
187                    tz,
188                    begin,
189                    end);
190        }
191    
192        /**
193         * Creates a calendar which fires on particular days of each month.
194         * For example,<blockquote>
195         *
196         * <pre>createMonthlyByDay(
197         *     null, null, TimeZone.getTimeZone("PST"), 1,
198         *     (1 << 12) | (1 << 14) | (1 << {@link #LAST_DAY_OF_MONTH}))</pre>
199         *
200         * </blockquote> creates a schedule which fires on the 12th, 14th and last
201         * day of the month.
202         *
203         * @param begin open lower bound, may be null
204         * @param end closed upper bound, may be null
205         * @param tz timezone
206         * @param daysOfMonthBitmap a bitmap of day values, may include
207         *     {@link #LAST_DAY_OF_MONTH}
208         * @param timeOfDay time at which to fire
209         * @param period causes the schedule to be active every <code>period</code>
210         *     months. If <code>period</code> is greater than 1, the cycle starts
211         *     at the begin point of the schedule, or at the epoch (1 January,
212         *     1970) if <code>begin</code> is not specified.
213         *
214         * @pre tz != null
215         * @pre period > 0
216         * @post return != null
217         */
218        public static Schedule createMonthlyByDay(
219                Date begin, Date end, TimeZone tz, Time timeOfDay, int period,
220                int daysOfMonthBitmap) {
221            DateSchedule dateSchedule = new MonthlyByDayDateSchedule(
222                    begin == null ? null : ScheduleUtil.createCalendar(begin),
223                    period, daysOfMonthBitmap);
224            return new Schedule(
225                    dateSchedule,
226                    new OnceTimeSchedule(ScheduleUtil.createTimeCalendar(timeOfDay)),
227                    tz,
228                    begin,
229                    end);
230        }
231    
232        /**
233         * Creates a calendar which fires on particular days of particular weeks of
234         * a month. For example,<blockquote>
235         *
236         * <pre>createMonthlyByWeek(
237         *     null, null, TimeZone.getTimeZone("PST"),
238         *     (1 << Calendar.TUESDAY) | (1 << Calendar.THURSDAY),
239         *     (1 << 2) | (1 << {@link #LAST_WEEK_OF_MONTH})</pre>
240         *
241         * </blockquote> creates a schedule which fires on the 2nd and last Tuesday
242         * and Thursday of the month.
243         *
244         * @param begin open lower bound, may be null
245         * @param end closed upper bound, may be null
246         * @param tz timezone
247         * @param daysOfWeekBitmap a bitmap of day values, for example
248         *     <code>(1 << Calendar.TUESDAY) | (1 << Calendar.THURSDAY)</code>
249         * @param weeksOfMonthBitmap a bitmap of week values (may include
250         *     {@link #LAST_WEEK_OF_MONTH}
251         * @param timeOfDay time at which to fire
252         * @param period causes the schedule be active every <code>period</code>
253         *     months. If <code>period</code> is greater than 1, the cycle starts
254         *     at the begin point of the schedule, or at the epoch (1 January,
255         *     1970) if <code>begin</code> is not specified.
256         *
257         * @pre tz != null
258         * @pre period > 0
259         * @post return != null
260         */
261        public static Schedule createMonthlyByWeek(
262                Date begin, Date end, TimeZone tz,
263                Time timeOfDay, int period, int daysOfWeekBitmap,
264                int weeksOfMonthBitmap) {
265            DateSchedule dateSchedule = new MonthlyByWeekDateSchedule(
266                    begin == null ? null : ScheduleUtil.createCalendar(begin),
267                    period,
268                    daysOfWeekBitmap,
269                    weeksOfMonthBitmap);
270            return new Schedule(
271                    dateSchedule,
272                    new OnceTimeSchedule(ScheduleUtil.createTimeCalendar(timeOfDay)),
273                    tz,
274                    begin,
275                    end);
276        }
277    
278        /**
279         * Returns the next occurrence of this schedule after a given date. If
280         * <code>after</code> is null, returns the first occurrence. If there are
281         * no further occurrences, returns null.
282         *
283         * @param after if not null, returns the first occurrence after this
284         *    point in time; if null, returns the first occurrence ever.
285         * @param strict If <code>after</code> is an occurrence,
286         *     <code>strict</code> determines whether this method returns it, or
287         *     the next occurrence. If <code>strict</code> is true, the value
288         *     returned is strictly greater than <code>after</code>.
289         */
290        public Date nextOccurrence(Date after, boolean strict) {
291            if (after == null ||
292                    begin != null && begin.after(after)) {
293                after = begin;
294                strict = false;
295            }
296            if (after == null) {
297                after = new Date(0);
298            }
299            Date next = nextOccurrence0(after, strict);
300            // if there is an upper bound, and this is not STRICTLY before it,
301            // there's no next occurrence
302            if (next != null &&
303                    end != null &&
304                    !next.before(end)) {
305                next = null;
306            }
307            return next;
308        }
309    
310        private Date nextOccurrence0(Date after, boolean strict) {
311            Calendar next = ScheduleUtil.createCalendar(after);
312            if (tz == null || tz.getID().equals("GMT")) {
313                return nextOccurrence1(next, strict);
314            } else {
315                int offset;
316                if (next == null) {
317                    offset = tz.getRawOffset();
318                } else {
319                    offset = ScheduleUtil.timezoneOffset(tz, next);
320                }
321                // Add the offset to the calendar, so that the calendar looks like
322                // the local time (even though it is still in GMT). Suppose an
323                // event runs at 12:00 JST each day. At 02:00 GMT they ask for the
324                // next event. We convert this to local time, 11:00 JST, by adding
325                // the 9 hour offset. We will convert the result back to GMT by
326                // subtracting the offset.
327                next.add(Calendar.MILLISECOND, offset);
328                Date result = nextOccurrence1(next, strict);
329                if (result == null) {
330                    return null;
331                }
332                Calendar resultCalendar = ScheduleUtil.createCalendar(result);
333                int offset2 = ScheduleUtil.timezoneOffset(tz, resultCalendar);
334                // Shift the result back again.
335                resultCalendar.add(Calendar.MILLISECOND, -offset2);
336                return resultCalendar.getTime();
337            }
338        }
339    
340        private Date nextOccurrence1(Calendar earliest, boolean strict) {
341            Calendar earliestDay = ScheduleUtil.floor(earliest);
342            Calendar earliestTime = ScheduleUtil.getTime(earliest);
343            // first, try a later time on the same day
344            Calendar nextDay = dateSchedule.nextOccurrence(earliestDay, false);
345            Calendar nextTime = timeSchedule.nextOccurrence(earliestTime, strict);
346            if (nextTime == null) {
347                // next, try the first time on a later day
348                nextDay = dateSchedule.nextOccurrence(earliestDay, true);
349                nextTime = timeSchedule.nextOccurrence(ScheduleUtil.midnightTime, false);
350            }
351            if (nextDay == null || nextTime == null) {
352                return null;
353            }
354            nextDay.set(Calendar.HOUR_OF_DAY, nextTime.get(Calendar.HOUR_OF_DAY));
355            nextDay.set(Calendar.MINUTE, nextTime.get(Calendar.MINUTE));
356            nextDay.set(Calendar.SECOND, nextTime.get(Calendar.SECOND));
357            nextDay.set(Calendar.MILLISECOND, nextTime.get(Calendar.MILLISECOND));
358            return nextDay.getTime();
359        }
360    }
361    
362    /**
363     * A <code>TimeSchedule</code> generates a series of times within a day.
364     */
365    interface TimeSchedule {
366        /**
367         * Returns the next occurrence at or after <code>after</code>. If
368         * <code>after</code> is null, returns the first occurrence. If there are
369         * no further occurrences, returns null.
370         *
371         * @param strict if true, return time must be after <code>after</code>, not
372         *     equal to it
373         */
374        Calendar nextOccurrence(Calendar earliest, boolean strict);
375    }
376    
377    /**
378     * A <code>OnceTimeSchedule</code> fires at one and only one time.
379     */
380    class OnceTimeSchedule implements TimeSchedule {
381        Calendar time;
382        OnceTimeSchedule(Calendar time) {
383            ScheduleUtil.assertTrue(time != null);
384            ScheduleUtil.assertTrue(ScheduleUtil.isTime(time));
385            this.time = time;
386        }
387        public Calendar nextOccurrence(Calendar after, boolean strict) {
388            if (after == null) {
389                return time;
390            }
391            if (time.after(after)) {
392                return time;
393            }
394            if (!strict && time.equals(after)) {
395                return time;
396            }
397            return null;
398        }
399    }
400    
401    /**
402     * A <code>DateSchedule</code> returns a series of dates.
403     */
404    interface DateSchedule {
405        /**
406         * Returns the next date when this schedule fires.
407         *
408         * @pre earliest != null
409         */
410        Calendar nextOccurrence(Calendar earliest, boolean strict);
411    };
412    
413    /**
414     * A <code>DailyDateSchedule</code> fires every day.
415     */
416    class DailyDateSchedule implements DateSchedule {
417        int period;
418        int beginOrdinal;
419        DailyDateSchedule(Calendar begin, int period) {
420            this.period = period;
421            ScheduleUtil.assertTrue(period > 0, "period must be positive");
422            this.beginOrdinal = ScheduleUtil.julianDay(
423                    begin == null ? ScheduleUtil.epochDay : begin);
424        }
425    
426        public Calendar nextOccurrence(Calendar day, boolean strict) {
427            day = (Calendar) day.clone();
428            if (strict) {
429                day.add(Calendar.DATE, 1);
430            }
431            while (true) {
432                int ordinal = ScheduleUtil.julianDay(day);
433                if ((ordinal - beginOrdinal) % period == 0) {
434                    return day;
435                }
436                day.add(Calendar.DATE, 1);
437            }
438        }
439    }
440    
441    /**
442     * A <code>WeeklyDateSchedule</code> fires every week. A bitmap indicates
443     * which days of the week it fires.
444     */
445    class WeeklyDateSchedule implements DateSchedule {
446        int period;
447        int beginOrdinal;
448        int daysOfWeekBitmap;
449    
450        WeeklyDateSchedule(Calendar begin, int period, int daysOfWeekBitmap) {
451            this.period = period;
452            ScheduleUtil.assertTrue(period > 0, "period must be positive");
453            this.beginOrdinal = ScheduleUtil.julianDay(
454                    begin == null ? ScheduleUtil.epochDay : begin) / 7;
455            this.daysOfWeekBitmap = daysOfWeekBitmap;
456            ScheduleUtil.assertTrue(
457                    (daysOfWeekBitmap & Schedule.allDaysOfWeekBitmap) != 0,
458                    "weekly schedule must have at least one day set");
459            ScheduleUtil.assertTrue(
460                    (daysOfWeekBitmap & Schedule.allDaysOfWeekBitmap) == daysOfWeekBitmap,
461                    "weekly schedule has bad bits set: " + daysOfWeekBitmap);
462        }
463    
464        public Calendar nextOccurrence(Calendar earliest, boolean strict) {
465            earliest = (Calendar) earliest.clone();
466            if (strict) {
467                earliest.add(Calendar.DATE, 1);
468            }
469            int i = 7 + period; // should be enough
470            while (i-- > 0) {
471                int dayOfWeek = earliest.get(Calendar.DAY_OF_WEEK);
472                if ((daysOfWeekBitmap & (1 << dayOfWeek)) != 0) {
473                    int ordinal = ScheduleUtil.julianDay(earliest) / 7;
474                    if ((ordinal - beginOrdinal) % period == 0) {
475                        return earliest;
476                    }
477                }
478                earliest.add(Calendar.DATE, 1);
479            }
480            throw ScheduleUtil.newInternal(
481                    "weekly date schedule is looping -- maybe the " +
482                    "bitmap is empty: " + daysOfWeekBitmap);
483        }
484    }
485    
486    /**
487     * A <code>MonthlyByDayDateSchedule</code> fires on a particular set of days
488     * every month.
489     */
490    class MonthlyByDayDateSchedule implements DateSchedule {
491        int period;
492        int beginMonth;
493        int daysOfMonthBitmap;
494    
495        MonthlyByDayDateSchedule(
496                Calendar begin, int period, int daysOfMonthBitmap) {
497            this.period = period;
498            ScheduleUtil.assertTrue(period > 0, "period must be positive");
499            this.beginMonth = begin == null ? 0 : monthOrdinal(begin);
500            this.daysOfMonthBitmap = daysOfMonthBitmap;
501            ScheduleUtil.assertTrue(
502                    (daysOfMonthBitmap & Schedule.allDaysOfMonthBitmap) != 0,
503                    "monthly day schedule must have at least one day set");
504            ScheduleUtil.assertTrue(
505                    (daysOfMonthBitmap & Schedule.allDaysOfMonthBitmap) ==
506                    daysOfMonthBitmap,
507                    "monthly schedule has bad bits set: " + daysOfMonthBitmap);
508        }
509    
510        public Calendar nextOccurrence(Calendar earliest, boolean strict) {
511            earliest = (Calendar) earliest.clone();
512            if (strict) {
513                earliest.add(Calendar.DATE, 1);
514            }
515            int i = 31 + period; // should be enough
516            while (i-- > 0) {
517                int month = monthOrdinal(earliest);
518                if ((month - beginMonth) % period != 0) {
519                    // not this month! move to first of next month
520                    earliest.set(Calendar.DAY_OF_MONTH, 1);
521                    earliest.add(Calendar.MONTH, 1);
522                    continue;
523                }
524                int dayOfMonth = earliest.get(Calendar.DAY_OF_MONTH);
525                if ((daysOfMonthBitmap & (1 << dayOfMonth)) != 0) {
526                    return earliest;
527                }
528                earliest.add(Calendar.DATE, 1);
529                if ((daysOfMonthBitmap & (1 << Schedule.LAST_DAY_OF_MONTH)) != 0 &&
530                        earliest.get(Calendar.DAY_OF_MONTH) == 1) {
531                    // They want us to fire on the last day of the month, and
532                    // now we're at the first day of the month, so we must have
533                    // been at the last. Backtrack and return it.
534                    earliest.add(Calendar.DATE, -1);
535                    return earliest;
536                }
537            }
538            throw ScheduleUtil.newInternal(
539                    "monthly-by-day date schedule is looping -- maybe " +
540                    "the bitmap is empty: " + daysOfMonthBitmap);
541        }
542    
543        private static int monthOrdinal(Calendar earliest) {
544            return earliest.get(Calendar.YEAR) * 12 +
545                earliest.get(Calendar.MONTH);
546        }
547    }
548    
549    /**
550     * A <code>MonthlyByWeekDateSchedule</code> fires on particular days of
551     * particular weeks of a month.
552     */
553    class MonthlyByWeekDateSchedule implements DateSchedule {
554        int period;
555        int beginMonth;
556        int daysOfWeekBitmap;
557        int weeksOfMonthBitmap;
558    
559        MonthlyByWeekDateSchedule(
560                Calendar begin, int period, int daysOfWeekBitmap,
561                int weeksOfMonthBitmap) {
562            this.period = period;
563            ScheduleUtil.assertTrue(period > 0, "period must be positive");
564            this.beginMonth = begin == null ? 0 : monthOrdinal(begin);
565            this.daysOfWeekBitmap = daysOfWeekBitmap;
566            ScheduleUtil.assertTrue(
567                    (daysOfWeekBitmap & Schedule.allDaysOfWeekBitmap) != 0,
568                    "weekly schedule must have at least one day set");
569            ScheduleUtil.assertTrue(
570                    (daysOfWeekBitmap & Schedule.allDaysOfWeekBitmap) ==
571                    daysOfWeekBitmap,
572                    "weekly schedule has bad bits set: " + daysOfWeekBitmap);
573            this.weeksOfMonthBitmap = weeksOfMonthBitmap;
574            ScheduleUtil.assertTrue(
575                    (weeksOfMonthBitmap & Schedule.allWeeksOfMonthBitmap) != 0,
576                    "weeks of month schedule must have at least one week set");
577            ScheduleUtil.assertTrue(
578                    (weeksOfMonthBitmap & Schedule.allWeeksOfMonthBitmap) ==
579                    weeksOfMonthBitmap,
580                    "week of month schedule has bad bits set: " +
581                    weeksOfMonthBitmap);
582        }
583    
584        public Calendar nextOccurrence(Calendar earliest, boolean strict) {
585            earliest = (Calendar) earliest.clone();
586            if (strict) {
587                earliest.add(Calendar.DATE, 1);
588            }
589             // should be enough... worst case is '5th Monday of every 3rd month'
590            int i = 365 + period;
591            while (i-- > 0) {
592                int month = monthOrdinal(earliest);
593                if ((month - beginMonth) % period != 0) {
594                    // not this month! move to first of next month
595                    earliest.set(Calendar.DAY_OF_MONTH, 1);
596                    earliest.add(Calendar.MONTH, 1);
597                    continue;
598                }
599                // is it one of the days we're interested in?
600                int dayOfWeek = earliest.get(Calendar.DAY_OF_WEEK);
601                if ((daysOfWeekBitmap & (1 << dayOfWeek)) != 0) {
602                    // is it the Yth occurrence of day X?
603                    int dayOfMonth = earliest.get(Calendar.DAY_OF_MONTH);
604                    int weekOfMonth = (dayOfMonth + 6) / 7; // 1-based
605                    if ((weeksOfMonthBitmap & (1 << weekOfMonth)) != 0) {
606                        return earliest;
607                    }
608                    // is it the last occurrence of day X?
609                    if ((weeksOfMonthBitmap & (1 << Schedule.LAST_WEEK_OF_MONTH))
610                            != 0) {
611                        // we're in the last week of the month iff a week later is
612                        // in the first week of the next month
613                        earliest.add(Calendar.WEEK_OF_MONTH, 1);
614                        boolean isLast = earliest.get(Calendar.DAY_OF_MONTH) <= 7;
615                        earliest.add(Calendar.WEEK_OF_MONTH, -1);
616                        if (isLast) {
617                            return earliest;
618                        }
619                    }
620                }
621                earliest.add(Calendar.DATE, 1);
622            }
623            throw ScheduleUtil.newInternal(
624                    "monthy-by-week date schedule is cyclic");
625        }
626    
627        private static int monthOrdinal(Calendar earliest) {
628            return earliest.get(Calendar.YEAR) * 12 +
629                earliest.get(Calendar.MONTH);
630        }
631    }
632    
633    /**
634     * Utility functions for {@link Schedule} and supporting classes.
635     */
636    class ScheduleUtil {
637        static final Calendar epochDay = ScheduleUtil.createCalendar(new Date(0));
638        static final Calendar midnightTime = ScheduleUtil.createTimeCalendar(0,0,0);
639    
640        public static void assertTrue(boolean b) {
641            if (!b) {
642                throw new Error("assertion failed");
643            }
644        }
645        public static void assertTrue(boolean b, String s) {
646            if (!b) {
647                throw new Error("assertion failed: " + s);
648            }
649        }
650        public static Error newInternal() {
651            return new Error("internal error");
652        }
653        public static Error newInternal(Throwable e, String s) {
654            return new Error("internal error '" + e + "': " + s);
655        }
656        public static Error newInternal(String s) {
657            return new Error("internal error: " + s);
658        }
659        public static boolean lessThan(Time t1, Time t2, boolean strict) {
660            if (strict) {
661                return t1.getTime() < t2.getTime();
662            } else {
663                return t1.getTime() <= t2.getTime();
664            }
665        }
666        public static boolean lessThan(Date d1, Date d2, boolean strict) {
667            if (strict) {
668                return d1.getTime() < d2.getTime();
669            } else {
670                return d1.getTime() <= d2.getTime();
671            }
672        }
673        public static boolean is0000(Calendar calendar) {
674            return calendar.get(Calendar.HOUR_OF_DAY) == 0 &&
675                calendar.get(Calendar.MINUTE) == 0 &&
676                calendar.get(Calendar.SECOND) == 0 &&
677                calendar.get(Calendar.MILLISECOND) == 0;
678        }
679        public static boolean isTime(Calendar calendar) {
680            return calendar.get(Calendar.YEAR) ==
681                    ScheduleUtil.epochDay.get(Calendar.YEAR) &&
682                calendar.get(Calendar.DAY_OF_YEAR) ==
683                    ScheduleUtil.epochDay.get(Calendar.DAY_OF_YEAR);
684        }
685        /**
686         * Returns a calendar rounded down to the previous midnight.
687         */
688        public static Calendar floor(Calendar calendar) {
689            if (calendar == null) {
690                return null;
691            }
692            calendar = (Calendar) calendar.clone();
693            calendar.set(Calendar.HOUR_OF_DAY, 0);
694            calendar.set(Calendar.MINUTE, 0);
695            calendar.set(Calendar.SECOND, 0);
696            calendar.set(Calendar.MILLISECOND, 0);
697            return calendar;
698        }
699        /**
700         * Returns a calendar rounded up to the next midnight, unless it is already
701         * midnight.
702         */
703        public static Calendar ceiling(Calendar calendar) {
704            if (calendar == null) {
705                return null;
706            }
707            if (is0000(calendar)) {
708                return calendar;
709            }
710            calendar = (Calendar) calendar.clone();
711            calendar.add(Calendar.DATE, 1);
712            return calendar;
713        }
714    
715        /**
716         * Extracts the time part of a date. Given a null date, returns null.
717         */
718        public static Calendar getTime(Calendar calendar) {
719            if (calendar == null) {
720                return null;
721            }
722            return createTimeCalendar(
723                    calendar.get(Calendar.HOUR_OF_DAY),
724                    calendar.get(Calendar.MINUTE),
725                    calendar.get(Calendar.SECOND));
726        }
727        /**
728         * Creates a calendar in UTC, and initializes it to <code>date</code>.
729         *
730         * @pre date != null
731         * @post return != null
732         */
733        public static Calendar createCalendar(Date date) {
734            Calendar calendar = Calendar.getInstance();
735            calendar.setTimeZone(Schedule.utcTimeZone);
736            calendar.setTime(date);
737            return calendar;
738        }
739        /**
740         * Creates a calendar in UTC, and initializes it to a given year, month,
741         * day, hour, minute, second. <b>NOTE: month is 1-based</b>
742         */
743        public static Calendar createCalendar(
744                int year, int month, int day, int hour, int minute, int second) {
745            Calendar calendar = Calendar.getInstance();
746            calendar.setTimeZone(Schedule.utcTimeZone);
747            calendar.toString(); // calls complete()
748            calendar.set(Calendar.YEAR, year);
749            calendar.set(Calendar.MONTH, month - 1); // CONVERT TO 0-BASED!!
750            calendar.set(Calendar.DAY_OF_MONTH, day);
751            calendar.set(Calendar.HOUR_OF_DAY, hour);
752            calendar.set(Calendar.MINUTE, minute);
753            calendar.set(Calendar.SECOND, second);
754            calendar.set(Calendar.MILLISECOND, 0);
755            return calendar;
756        }
757        /**
758         * Creates a calendar from a time. Milliseconds are ignored.
759         *
760         * @pre time != null
761         * @post return != null
762         */
763        public static Calendar createTimeCalendar(Time time) {
764            Calendar calendar = (Calendar) ScheduleUtil.epochDay.clone();
765            calendar.setTimeZone(Schedule.utcTimeZone);
766            calendar.setTime(time);
767            return createTimeCalendar(
768                    calendar.get(Calendar.HOUR_OF_DAY),
769                    calendar.get(Calendar.MINUTE),
770                    calendar.get(Calendar.SECOND));
771        }
772        /**
773         * Creates a calendar and sets it to a given hours, minutes, seconds.
774         */
775        public static Calendar createTimeCalendar(
776                int hours, int minutes, int seconds) {
777            Calendar calendar = (Calendar) ScheduleUtil.epochDay.clone();
778            calendar.set(Calendar.HOUR_OF_DAY, hours);
779            calendar.set(Calendar.MINUTE, minutes);
780            calendar.set(Calendar.SECOND, seconds);
781            calendar.set(Calendar.MILLISECOND, 0);
782            return calendar;
783        }
784        /**
785         * Creates a calendar and sets it to a given year, month, date.
786         */
787        public static Calendar createDateCalendar(
788                int year, int month, int dayOfMonth) {
789            Calendar calendar = Calendar.getInstance();
790            calendar.setTimeZone(Schedule.utcTimeZone);
791            calendar.set(Calendar.YEAR, year);
792            calendar.set(Calendar.MONTH, month);
793            calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth);
794            return calendar;
795        }
796        /**
797         * Creates a {@link java.sql.Time}
798         */
799        public static Time createTime(int hour, int minutes, int second) {
800            return new Time(
801                    createTimeCalendar(hour, minutes, second).getTime().getTime());
802        }
803        /**
804         * Returns the julian day number of a given date. (Is there a better way
805         * to do this?)
806         */
807        public static int julianDay(Calendar calendar) {
808            int year = calendar.get(Calendar.YEAR),
809                day = calendar.get(Calendar.DAY_OF_YEAR),
810                leapDays = (year / 4) - (year / 100) + (year / 400);
811            return year * 365 + leapDays + day;
812        }
813        /**
814         * Returns the offset from UTC in milliseconds in this timezone on a given
815         * date.
816         */
817        public static int timezoneOffset(TimeZone tz, Calendar calendar) {
818            return tz.getOffset(
819                    calendar.get(Calendar.ERA),
820                    calendar.get(Calendar.YEAR),
821                    calendar.get(Calendar.MONTH),
822                    calendar.get(Calendar.DAY_OF_MONTH),
823                    calendar.get(Calendar.DAY_OF_WEEK),
824                    (1000 *
825                        (60 *
826                            (60 * calendar.get(Calendar.HOUR_OF_DAY) +
827                                calendar.get(Calendar.MINUTE)) +
828                        calendar.get(Calendar.SECOND)) +
829                    calendar.get(Calendar.MILLISECOND)));
830        }
831    }
832    
833    // End Schedule.java