| 1 | package net.sourceforge.calendardate; |
| 2 | |
| 3 | import java.io.IOException; |
| 4 | import java.io.Serializable; |
| 5 | import java.util.Calendar; |
| 6 | import java.util.Date; |
| 7 | import java.util.GregorianCalendar; |
| 8 | import java.util.TimeZone; |
| 9 | |
| 10 | /** |
| 11 | * This class represents a date in the Gregorian calendar (for example, December |
| 12 | * 20, 1998). It is designed to be a simpler, immutable version of |
| 13 | * <code>java.util.GregorianCalendar</code>. |
| 14 | * <p> |
| 15 | * <b>Don't confuse this class with <code>java.util.Date</code>! </b> They |
| 16 | * represent two separate things: |
| 17 | * <ul> |
| 18 | * <li><code>CalendarDate</code> represents a 'date in the calendar' (e.g. |
| 19 | * "John Smith's birthday") while |
| 20 | * <li><code>java.util.Date</code> represent an 'instant in time' (e.g. "When |
| 21 | * order 23711 was received"). A better name for this class would have been |
| 22 | * 'DateTime'. |
| 23 | * </ul> |
| 24 | * These two ways of recording time are not directly related: For any given |
| 25 | * 'instant in time' the corresponding 'date in the calendar' depends on the |
| 26 | * timezone. Similiarly the 'instant in time' when a 'date in the calendar' |
| 27 | * begins depends on the timezone. (This is why you must supply a |
| 28 | * <code>java.util.TimeZone</code> when converting between instances of |
| 29 | * <code>CalendarDate</code> and <code>Date</code>.) |
| 30 | * <p> |
| 31 | * This class is thread-safe and immutable. |
| 32 | * <p> |
| 33 | * |
| 34 | * @see java.util.Date |
| 35 | * @see java.util.GregorianCalendar |
| 36 | */ |
| 37 | public final class CalendarDate implements Comparable, Serializable { |
| 38 | |
| 39 | static final long serialVersionUID = 8577551385869073340L; |
| 40 | |
| 41 | /** The value returned by getDayOfWeek() representing Sunday */ |
| 42 | public static final int SUNDAY = 1; |
| 43 | |
| 44 | /** The value returned by getDayOfWeek() representing Monday */ |
| 45 | public static final int MONDAY = 2; |
| 46 | |
| 47 | /** The value returned by getDayOfWeek() representing Tuesday */ |
| 48 | public static final int TUESDAY = 3; |
| 49 | |
| 50 | /** The value returned by getDayOfWeek() representing Wednesday */ |
| 51 | public static final int WEDNESDAY = 4; |
| 52 | |
| 53 | /** The value returned by getDayOfWeek() representing Thursday */ |
| 54 | public static final int THURSDAY = 5; |
| 55 | |
| 56 | /** The value returned by getDayOfWeek() representing Friday */ |
| 57 | public static final int FRIDAY = 6; |
| 58 | |
| 59 | /** The value returned by getDayOfWeek() representing Saturday */ |
| 60 | public static final int SATURDAY = 7; |
| 61 | |
| 62 | /** The value returned by getMonth() representing January */ |
| 63 | public static final int JANUARY = 1; |
| 64 | |
| 65 | /** The value returned by getMonth() representing February */ |
| 66 | public static final int FEBRUARY = 2; |
| 67 | |
| 68 | /** The value returned by getMonth() representing March */ |
| 69 | public static final int MARCH = 3; |
| 70 | |
| 71 | /** The value returned by getMonth() representing April */ |
| 72 | public static final int APRIL = 4; |
| 73 | |
| 74 | /** The value returned by getMonth() representing May */ |
| 75 | public static final int MAY = 5; |
| 76 | |
| 77 | /** The value returned by getMonth() representing June */ |
| 78 | public static final int JUNE = 6; |
| 79 | |
| 80 | /** The value returned by getMonth() representing July */ |
| 81 | public static final int JULY = 7; |
| 82 | |
| 83 | /** The value returned by getMonth() representing August */ |
| 84 | public static final int AUGUST = 8; |
| 85 | |
| 86 | /** The value returned by getMonth() representing September */ |
| 87 | public static final int SEPTEMBER = 9; |
| 88 | |
| 89 | /** The value returned by getMonth() representing October */ |
| 90 | public static final int OCTOBER = 10; |
| 91 | |
| 92 | /** The value returned by getMonth() representing November */ |
| 93 | public static final int NOVEMBER = 11; |
| 94 | |
| 95 | /** The value returned by getMonth() representing December */ |
| 96 | public static final int DECEMBER = 12; |
| 97 | |
| 98 | /** Days since 1 Jan, 1 A.D., or -1 if not calculated yet */ |
| 99 | private transient int daysSinceEpoch = -1; |
| 100 | |
| 101 | private int year; |
| 102 | |
| 103 | private int month; |
| 104 | |
| 105 | private int dayOfMonth; |
| 106 | |
| 107 | /** |
| 108 | * The number of days in the year up to (but not including) a month. |
| 109 | */ |
| 110 | private static final int[] cumulDaysToMonth = { 0, // Jan |
| 111 | 31, // Feb |
| 112 | 59, // Mar |
| 113 | 90, // Apr |
| 114 | 120, // May |
| 115 | 151, // Jun |
| 116 | 181, // Jul |
| 117 | 212, // Aug |
| 118 | 243, // Sep |
| 119 | 273, // Oct |
| 120 | 304, // Nov |
| 121 | 334 // Dec |
| 122 | }; |
| 123 | |
| 124 | private static final int[] daysInMonth = { 31, // Jan |
| 125 | 28, // Feb |
| 126 | 31, // Mar |
| 127 | 30, // Apr |
| 128 | 31, // May |
| 129 | 30, // Jun |
| 130 | 31, // Jul |
| 131 | 31, // Aug |
| 132 | 30, // Sep |
| 133 | 31, // Oct |
| 134 | 30, // Nov |
| 135 | 31, // Dec |
| 136 | }; |
| 137 | |
| 138 | /** |
| 139 | * The earliest date that can be represented by this class (Januray 1, 1600 |
| 140 | * A.D.) |
| 141 | * <p> |
| 142 | * Note: This date may change to before 1600 in later releases of |
| 143 | * CalendarDate. |
| 144 | */ |
| 145 | public static final CalendarDate EARLIEST = new CalendarDate(1600, 1, 1); |
| 146 | |
| 147 | /** |
| 148 | * The latest date that can be represented by this class (December 31, 2999 |
| 149 | * A.D.) |
| 150 | * <p> |
| 151 | * Note: This date may change to after 2999 in later releases of |
| 152 | * CalendarDate. |
| 153 | */ |
| 154 | public static final CalendarDate LATEST = new CalendarDate(2999, 12, 31); |
| 155 | |
| 156 | /** |
| 157 | * Creates a date represented by the given year, month and day. |
| 158 | * |
| 159 | * @param year |
| 160 | * The year of the date to create |
| 161 | * @param month |
| 162 | * The month of the date to create (1 = January, 12 = December) |
| 163 | * @param dayOfMonth |
| 164 | * The day of the month of the date to create. |
| 165 | * @throws IllegalArgumentException |
| 166 | * if the year, month and dayOfMonth combination are not valid |
| 167 | * in a Gregorian calendar. |
| 168 | */ |
| 169 | public CalendarDate(int year, int month, int dayOfMonth) { |
| 170 | this(year, month, dayOfMonth, false); |
| 171 | } |
| 172 | |
| 173 | /** |
| 174 | * Creates a CalendarDate representing the date in the given timezone at the |
| 175 | * given instant in time. |
| 176 | * <p> |
| 177 | * <b>Think carefully about what timezone to use!</b> Often you will want |
| 178 | * to use the timezone of the 'user' - which is not always represented by |
| 179 | * <code>TimeZone.getDefault()</code> |
| 180 | * |
| 181 | * @param tzone |
| 182 | * The timezone to be considered |
| 183 | * @param instantInTime |
| 184 | * The instant in time to be considered |
| 185 | * @throws IllegalArgumentException |
| 186 | * if the instant in time is out of range in the given timezone |
| 187 | * @see #isOutsideRange(TimeZone, Date) |
| 188 | */ |
| 189 | public CalendarDate(TimeZone tzone, Date instantInTime) { |
| 190 | GregorianCalendar cal = new GregorianCalendar(tzone); |
| 191 | cal.setTime(instantInTime); |
| 192 | init(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH)); |
| 193 | } |
| 194 | |
| 195 | /** |
| 196 | * Creates a CalendarDate representing the current date in the given |
| 197 | * timezone. Equivalent to <code>CalendarDate(tzone, new Date())</code> |
| 198 | * |
| 199 | * @param tzone |
| 200 | * The timezone to be considered |
| 201 | */ |
| 202 | public CalendarDate(TimeZone tzone) { |
| 203 | this(tzone, new Date()); |
| 204 | } |
| 205 | |
| 206 | /** |
| 207 | * Returns true if the given instant in time is before EARLIEST or after |
| 208 | * LATEST in the given timezone. |
| 209 | */ |
| 210 | public static boolean isOutsideRange(TimeZone tzone, Date instantInTime) { |
| 211 | GregorianCalendar cal = new GregorianCalendar(tzone); |
| 212 | cal.setTime(instantInTime); |
| 213 | return yearOutOfRange(cal.get(Calendar.YEAR)); |
| 214 | } |
| 215 | |
| 216 | /** |
| 217 | * |
| 218 | * @param year |
| 219 | * @param month |
| 220 | * @param dayOfMonth |
| 221 | * @param lenient |
| 222 | */ |
| 223 | private CalendarDate(int year, int month, int dayOfMonth, boolean lenient) { |
| 224 | if (!lenient) { |
| 225 | checkValidYearMonthDay(year, month, dayOfMonth); |
| 226 | } |
| 227 | init(year, month, dayOfMonth); |
| 228 | } |
| 229 | |
| 230 | private void init(int year, int month, int dayOfMonth) { |
| 231 | this.year = year; |
| 232 | this.month = month; |
| 233 | this.dayOfMonth = dayOfMonth; |
| 234 | correctNonLenientFieldsIfNecessary(); |
| 235 | if (yearOutOfRange(getYear())) { |
| 236 | throw new IllegalArgumentException("Date year out of range: " + year); |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | private void correctNonLenientFieldsIfNecessary() { |
| 241 | if (!isValidYearMonthDay(year, month, dayOfMonth)) { |
| 242 | GregorianCalendar cal = new GregorianCalendar(year, month - 1, dayOfMonth); |
| 243 | this.year = cal.get(Calendar.YEAR); |
| 244 | this.month = cal.get(Calendar.MONTH) + 1; |
| 245 | this.dayOfMonth = cal.get(Calendar.DAY_OF_MONTH); |
| 246 | } |
| 247 | |
| 248 | } |
| 249 | |
| 250 | private static boolean isValidYearMonthDay(int year, int month, int dayOfMonth) { |
| 251 | if ((dayOfMonth == 29) && (month == 2) && isLeapYear(year)) { |
| 252 | return true; |
| 253 | } |
| 254 | if ((month <= 0) || (month > 12)) { |
| 255 | return false; |
| 256 | } |
| 257 | if ((dayOfMonth <= 0) || (dayOfMonth > daysInMonth[month - 1])) { |
| 258 | return false; |
| 259 | } |
| 260 | return true; |
| 261 | } |
| 262 | |
| 263 | private static boolean yearOutOfRange(int year) { |
| 264 | return (year < 1600) || (year >= 3000); |
| 265 | } |
| 266 | |
| 267 | /** |
| 268 | * Throws an exception if the input isn't a valid day in the Gregorian |
| 269 | * Calendar |
| 270 | */ |
| 271 | private void checkValidYearMonthDay(int year, int month, int dayOfMonth) { |
| 272 | if (!isValidYearMonthDay(year, month, dayOfMonth)) { |
| 273 | throw new IllegalArgumentException("Year/month/day combination is invalid: " + year |
| 274 | + "/" + month + "/" + dayOfMonth); |
| 275 | } |
| 276 | } |
| 277 | |
| 278 | /** |
| 279 | * The day of the month |
| 280 | * |
| 281 | * @return The day of the month (in range 1 to 31) |
| 282 | */ |
| 283 | public synchronized int getDayOfMonth() { |
| 284 | return dayOfMonth; |
| 285 | } |
| 286 | |
| 287 | /** |
| 288 | * The day of the week for this date |
| 289 | * |
| 290 | * @return Day of week in range 1 (Sunday) to 7 (Saturday) |
| 291 | */ |
| 292 | public synchronized int getDayOfWeek() { |
| 293 | return getDaysSinceEpoch() % 7 + 1; |
| 294 | } |
| 295 | |
| 296 | /** |
| 297 | * The month of this date |
| 298 | * |
| 299 | * @return Month in range 1 to 12 |
| 300 | */ |
| 301 | public synchronized int getMonth() { |
| 302 | return month; |
| 303 | } |
| 304 | |
| 305 | /** |
| 306 | * The year of this date |
| 307 | * |
| 308 | * @return The year |
| 309 | */ |
| 310 | public synchronized int getYear() { |
| 311 | return year; |
| 312 | } |
| 313 | |
| 314 | /** |
| 315 | * Returns a new date which is this date offset by numDays. |
| 316 | * |
| 317 | * @param numDays |
| 318 | * the number of days to be added to this date (can be negative) |
| 319 | * @return A new date offset by numDays |
| 320 | * @throws IllegalArgumentException |
| 321 | * if the resulting day would be before EARLIEST or after |
| 322 | * LATEST. That is, if numDays < this.daysUntil(EARLIEST) or |
| 323 | * numDays > this.daysUntil(LATEST) |
| 324 | */ |
| 325 | public CalendarDate addDays(int numDays) { |
| 326 | return new CalendarDate(getYear(), getMonth(), getDayOfMonth() + numDays, true); |
| 327 | } |
| 328 | |
| 329 | /** |
| 330 | * Returns the number of days until the given date |
| 331 | * |
| 332 | * @param otherDay |
| 333 | * The date to compare to |
| 334 | * @return The number of days until otherDay (can be negative) |
| 335 | */ |
| 336 | public int daysUntil(CalendarDate otherDay) { |
| 337 | return otherDay.getDaysSinceEpoch() - this.getDaysSinceEpoch(); |
| 338 | } |
| 339 | |
| 340 | /** |
| 341 | * Returns the number of month changes until the given day. Note that this |
| 342 | * means there is just one 'month' between 1 November and 31 December. |
| 343 | * |
| 344 | * @param otherDay |
| 345 | * The date to compare to |
| 346 | * @return The number of month changes until the given day |
| 347 | */ |
| 348 | public int monthsUntil(CalendarDate otherDay) { |
| 349 | return (otherDay.getMonth() - this.getMonth()) |
| 350 | + (12 * (otherDay.getYear() - this.getYear())); |
| 351 | } |
| 352 | |
| 353 | /** |
| 354 | * Days since epoch (1 Jan, 1 A.D.) |
| 355 | */ |
| 356 | private synchronized int getDaysSinceEpoch() { |
| 357 | if (daysSinceEpoch == -1) { |
| 358 | int year = getYear(); |
| 359 | int month = getMonth(); |
| 360 | int daysThisYear = cumulDaysToMonth[month - 1] + getDayOfMonth() - 1; |
| 361 | if ((month > 2) && isLeapYear(year)) { |
| 362 | daysThisYear++; |
| 363 | } |
| 364 | |
| 365 | daysSinceEpoch = daysToYear(year) + daysThisYear; |
| 366 | } |
| 367 | return daysSinceEpoch; |
| 368 | } |
| 369 | |
| 370 | /** |
| 371 | * Number of days up to, but not including, the given year since epoch. |
| 372 | * |
| 373 | * @param year |
| 374 | * @return |
| 375 | */ |
| 376 | static int daysToYear(int year) { |
| 377 | return (365 * year) + numLeapsToYear(year); |
| 378 | } |
| 379 | |
| 380 | /** |
| 381 | * Returns the number of leap years from the epoch until (but not including) |
| 382 | * the given year. The epoch begins on 1 Jan, 1AD |
| 383 | * |
| 384 | * @param year |
| 385 | * @return The number of leap years |
| 386 | */ |
| 387 | static int numLeapsToYear(int year) { |
| 388 | int num4y = (year - 1) / 4; |
| 389 | int num100y = (year - 1) / 100; |
| 390 | int num400y = (year - 1) / 400; |
| 391 | int numLeaps = num4y - num100y + num400y; |
| 392 | return numLeaps; |
| 393 | } |
| 394 | |
| 395 | /** |
| 396 | * Returns true if the year is a leap year in the Gregorian calendar |
| 397 | * |
| 398 | * @param year |
| 399 | * The year to consider |
| 400 | * @return True if <code>year</code> is a leap year |
| 401 | */ |
| 402 | public static boolean isLeapYear(int year) { |
| 403 | return (year % 400 == 0) || ((year % 100 != 0) && (year % 4 == 0)); |
| 404 | } |
| 405 | |
| 406 | /** |
| 407 | * If the given object is a CalendarDate: |
| 408 | * <ol> |
| 409 | * <li>returns less than 0 if this date is before the given date |
| 410 | * <li>returns 0 if this date is equal to the given date |
| 411 | * <li>returns more than 0 if this date is after the given date |
| 412 | * </ol> |
| 413 | * |
| 414 | * @param other |
| 415 | * the date to compare this one to |
| 416 | * @throws ClassCastException |
| 417 | * if <code>other</code> is not an instance of CalendarDate |
| 418 | * @see java.lang.Comparable#compareTo(java.lang.Object) |
| 419 | */ |
| 420 | public int compareTo(Object other) { |
| 421 | return this.getDaysSinceEpoch() - ((CalendarDate) other).getDaysSinceEpoch(); |
| 422 | } |
| 423 | |
| 424 | /** |
| 425 | * Returns true if this date is before the given date |
| 426 | * |
| 427 | * @param other |
| 428 | * The date to consider |
| 429 | * @return true if this date is before the given date |
| 430 | */ |
| 431 | public boolean isBefore(CalendarDate other) { |
| 432 | return compareTo(other) < 0; |
| 433 | } |
| 434 | |
| 435 | /** |
| 436 | * Returns true if this date is after the given date |
| 437 | * |
| 438 | * @param other |
| 439 | * The date to consider |
| 440 | * @return true if this date is after the given date |
| 441 | */ |
| 442 | public boolean isAfter(CalendarDate other) { |
| 443 | return compareTo(other) > 0; |
| 444 | } |
| 445 | |
| 446 | /** |
| 447 | * Returns true if the given object is a CalendarDate representing the same |
| 448 | * date as this object |
| 449 | * |
| 450 | * @param other |
| 451 | * The date to test against |
| 452 | * @return true if this is the same date as <code>other</code> |
| 453 | */ |
| 454 | public boolean equals(Object other) { |
| 455 | if (other instanceof CalendarDate) { |
| 456 | return (this.compareTo(other) == 0); |
| 457 | } |
| 458 | return false; |
| 459 | } |
| 460 | |
| 461 | public int hashCode() { |
| 462 | return (375 * getYear()) + (35 * getMonth()) + getDayOfMonth(); |
| 463 | } |
| 464 | |
| 465 | /** |
| 466 | * Returns a string form of this date in the form "2004-9-23" |
| 467 | * |
| 468 | * @return A string form of this date |
| 469 | */ |
| 470 | public String toString() { |
| 471 | return new StringBuffer().append(getYear()).append("-").append(getMonth()).append("-") |
| 472 | .append(getDayOfMonth()).toString(); |
| 473 | } |
| 474 | |
| 475 | private void writeObject(java.io.ObjectOutputStream out) throws IOException { |
| 476 | out.writeInt(getYear()); |
| 477 | out.writeInt(getMonth()); |
| 478 | out.writeInt(getDayOfMonth()); |
| 479 | out.writeInt(daysSinceEpoch); |
| 480 | } |
| 481 | |
| 482 | private void readObject(java.io.ObjectInputStream in) throws IOException, |
| 483 | ClassNotFoundException { |
| 484 | init(in.readInt(), in.readInt(), in.readInt()); |
| 485 | daysSinceEpoch = in.readInt(); |
| 486 | } |
| 487 | |
| 488 | /** |
| 489 | * Returns the instant in time when this day begins in the given timezone. |
| 490 | * |
| 491 | * @param timezone |
| 492 | * The timezone to consider |
| 493 | * @return the instant in time when this day begins |
| 494 | */ |
| 495 | public Date toDate(TimeZone timezone) { |
| 496 | return toDate(timezone, 0, 0, 0); |
| 497 | } |
| 498 | |
| 499 | /** |
| 500 | * Returns the instant in time when the given time of day is reached in the |
| 501 | * given timezone. If the time occurs twice on this date (as may happen when |
| 502 | * coming out of daylight savings time) the second occurrence will be |
| 503 | * returned. |
| 504 | * |
| 505 | * @param timezone |
| 506 | * The timezone to for which the date applies |
| 507 | * @param hour |
| 508 | * The hour of the day in range 0 - 23 |
| 509 | * @param min |
| 510 | * The minute of the day in range 0 - 59 |
| 511 | * @param sec |
| 512 | * The second of the day in range 0 - 60 (60 is for leap-seconds) |
| 513 | * @return The instant in time when the given time of day is reached in the |
| 514 | * given timezone |
| 515 | * @throws IllegalArgumentException |
| 516 | * if the hour, min or sec parameters are outside the correct |
| 517 | * range |
| 518 | */ |
| 519 | public Date toDate(TimeZone timezone, int hour, int min, int sec) { |
| 520 | checkHourMinSec(hour, min, sec); |
| 521 | GregorianCalendar cal = new GregorianCalendar(timezone); |
| 522 | cal.clear(); |
| 523 | cal.set(getYear(), getMonth() - 1, getDayOfMonth(), hour, min, sec); |
| 524 | return cal.getTime(); |
| 525 | } |
| 526 | |
| 527 | private void checkHourMinSec(int hour, int min, int sec) { |
| 528 | if ((hour < 0) || (hour >= 24)) { |
| 529 | throw new IllegalArgumentException("Hour out of range: " + hour); |
| 530 | } |
| 531 | if ((min < 0) || (min >= 60)) { |
| 532 | throw new IllegalArgumentException("Minute out of range: " + min); |
| 533 | } |
| 534 | // Leap seconds mean some minutes are 61 seconds long! |
| 535 | if ((sec < 0) || (sec >= 61)) { |
| 536 | throw new IllegalArgumentException("Second out of range: " + hour); |
| 537 | } |
| 538 | } |
| 539 | |
| 540 | /** |
| 541 | * Returns the number of days in the given month. |
| 542 | * |
| 543 | * @param year |
| 544 | * The year |
| 545 | * @param month |
| 546 | * The month in range 1 - 12 |
| 547 | * @return The number of days in the given month |
| 548 | */ |
| 549 | public static int daysInMonth(int year, int month) { |
| 550 | int result = daysInMonth[month - 1]; |
| 551 | if ((month == 2) && (isLeapYear(year))) { |
| 552 | result++; |
| 553 | } |
| 554 | return result; |
| 555 | } |
| 556 | |
| 557 | /** |
| 558 | * Adds a given number of months to the date while attempting to keep the |
| 559 | * day of the month. Note that this method is NOT transitive. For example: |
| 560 | * <ul> |
| 561 | * <li><code>new CalendarDate(2005, 12, 31).addMonths(2).addMonths(1)</code> |
| 562 | * results in "March 28, 2006"; but, |
| 563 | * <li><code>new CalendarDate(2005, 12, 31).addMonths(3)</code> results |
| 564 | * in "March 31, 2006" |
| 565 | * </ul> |
| 566 | * |
| 567 | * @param numMonths |
| 568 | * the number of months to be added (can be negative) |
| 569 | */ |
| 570 | public CalendarDate addMonths(int numMonths) { |
| 571 | int newMonthsSinceEpoch = getYear() * 12 + getMonth() + numMonths - 1; |
| 572 | int newYear = newMonthsSinceEpoch / 12; |
| 573 | int newMonth = (newMonthsSinceEpoch % 12) + 1; |
| 574 | int newDay = Math.min(getDayOfMonth(), daysInMonth(newYear, newMonth)); |
| 575 | return new CalendarDate(newYear, newMonth, newDay); |
| 576 | } |
| 577 | |
| 578 | } |