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