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