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 | } |