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