001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
006 *
007 * Project Info: http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
022 * USA.
023 *
024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025 * in the United States and other countries.]
026 *
027 * ---------
028 * Week.java
029 * ---------
030 * (C) Copyright 2001-2007, by Object Refinery Limited and Contributors.
031 *
032 * Original Author: David Gilbert (for Object Refinery Limited);
033 * Contributor(s): Aimin Han;
034 *
035 * Changes
036 * -------
037 * 11-Oct-2001 : Version 1 (DG);
038 * 18-Dec-2001 : Changed order of parameters in constructor (DG);
039 * 19-Dec-2001 : Added a new constructor as suggested by Paul English (DG);
040 * 29-Jan-2002 : Worked on the parseWeek() method (DG);
041 * 13-Feb-2002 : Fixed bug in Week(Date) constructor (DG);
042 * 26-Feb-2002 : Changed getStart(), getMiddle() and getEnd() methods to
043 * evaluate with reference to a particular time zone (DG);
044 * 05-Apr-2002 : Reinstated this class to the JCommon library (DG);
045 * 24-Jun-2002 : Removed unnecessary main method (DG);
046 * 10-Sep-2002 : Added getSerialIndex() method (DG);
047 * 06-Oct-2002 : Fixed errors reported by Checkstyle (DG);
048 * 18-Oct-2002 : Changed to observe 52 or 53 weeks per year, consistent with
049 * GregorianCalendar. Thanks to Aimin Han for the code (DG);
050 * 02-Jan-2003 : Removed debug code (DG);
051 * 13-Mar-2003 : Moved to com.jrefinery.data.time package, and implemented
052 * Serializable (DG);
053 * 21-Oct-2003 : Added hashCode() method (DG);
054 * 24-May-2004 : Modified getFirstMillisecond() and getLastMillisecond() to
055 * take account of firstDayOfWeek setting in Java's Calendar
056 * class (DG);
057 * 30-Sep-2004 : Replaced getTime().getTime() with getTimeInMillis() (DG);
058 * 04-Nov-2004 : Reverted change of 30-Sep-2004, because it won't work for
059 * JDK 1.3 (DG);
060 * ------------- JFREECHART 1.0.x ---------------------------------------------
061 * 06-Mar-2006 : Fix for bug 1448828, incorrect calculation of week and year
062 * for the first few days of some years (DG);
063 * 05-Oct-2006 : Updated API docs (DG);
064 * 06-Oct-2006 : Refactored to cache first and last millisecond values (DG);
065 * 09-Jan-2007 : Fixed bug in next() (DG);
066 * 28-Aug-2007 : Added new constructor to avoid problem in creating new
067 * instances (DG);
068 *
069 */
070
071 package org.jfree.data.time;
072
073 import java.io.Serializable;
074 import java.util.Calendar;
075 import java.util.Date;
076 import java.util.Locale;
077 import java.util.TimeZone;
078
079 /**
080 * A calendar week. All years are considered to have 53 weeks, numbered from 1
081 * to 53, although in many cases the 53rd week is empty. Most of the time, the
082 * 1st week of the year *begins* in the previous calendar year, but it always
083 * finishes in the current year (this behaviour matches the workings of the
084 * <code>GregorianCalendar</code> class).
085 * <P>
086 * This class is immutable, which is a requirement for all
087 * {@link RegularTimePeriod} subclasses.
088 */
089 public class Week extends RegularTimePeriod implements Serializable {
090
091 /** For serialization. */
092 private static final long serialVersionUID = 1856387786939865061L;
093
094 /** Constant for the first week in the year. */
095 public static final int FIRST_WEEK_IN_YEAR = 1;
096
097 /** Constant for the last week in the year. */
098 public static final int LAST_WEEK_IN_YEAR = 53;
099
100 /** The year in which the week falls. */
101 private short year;
102
103 /** The week (1-53). */
104 private byte week;
105
106 /** The first millisecond. */
107 private long firstMillisecond;
108
109 /** The last millisecond. */
110 private long lastMillisecond;
111
112 /**
113 * Creates a new time period for the week in which the current system
114 * date/time falls.
115 */
116 public Week() {
117 this(new Date());
118 }
119
120 /**
121 * Creates a time period representing the week in the specified year.
122 *
123 * @param week the week (1 to 53).
124 * @param year the year (1900 to 9999).
125 */
126 public Week(int week, int year) {
127 if ((week < FIRST_WEEK_IN_YEAR) && (week > LAST_WEEK_IN_YEAR)) {
128 throw new IllegalArgumentException(
129 "The 'week' argument must be in the range 1 - 53.");
130 }
131 this.week = (byte) week;
132 this.year = (short) year;
133 peg(Calendar.getInstance());
134 }
135
136 /**
137 * Creates a time period representing the week in the specified year.
138 *
139 * @param week the week (1 to 53).
140 * @param year the year (1900 to 9999).
141 */
142 public Week(int week, Year year) {
143 if ((week < FIRST_WEEK_IN_YEAR) && (week > LAST_WEEK_IN_YEAR)) {
144 throw new IllegalArgumentException(
145 "The 'week' argument must be in the range 1 - 53.");
146 }
147 this.week = (byte) week;
148 this.year = (short) year.getYear();
149 peg(Calendar.getInstance());
150 }
151
152 /**
153 * Creates a time period for the week in which the specified date/time
154 * falls.
155 *
156 * @param time the time (<code>null</code> not permitted).
157 */
158 public Week(Date time) {
159 // defer argument checking...
160 this(time, RegularTimePeriod.DEFAULT_TIME_ZONE, Locale.getDefault());
161 }
162
163 /**
164 * Creates a time period for the week in which the specified date/time
165 * falls, calculated relative to the specified time zone.
166 *
167 * @param time the date/time (<code>null</code> not permitted).
168 * @param zone the time zone (<code>null</code> not permitted).
169 *
170 * @deprecated As of 1.0.7, use {@link #Week(Date, TimeZone, Locale)}.
171 */
172 public Week(Date time, TimeZone zone) {
173 // defer argument checking...
174 this(time, RegularTimePeriod.DEFAULT_TIME_ZONE, Locale.getDefault());
175 }
176
177 /**
178 * Creates a time period for the week in which the specified date/time
179 * falls, calculated relative to the specified time zone.
180 *
181 * @param time the date/time (<code>null</code> not permitted).
182 * @param zone the time zone (<code>null</code> not permitted).
183 * @param locale the locale (<code>null</code> not permitted).
184 *
185 * @since 1.0.7
186 */
187 public Week(Date time, TimeZone zone, Locale locale) {
188 if (time == null) {
189 throw new IllegalArgumentException("Null 'time' argument.");
190 }
191 if (zone == null) {
192 throw new IllegalArgumentException("Null 'zone' argument.");
193 }
194 if (locale == null) {
195 throw new IllegalArgumentException("Null 'locale' argument.");
196 }
197 Calendar calendar = Calendar.getInstance(zone, locale);
198 calendar.setTime(time);
199
200 // sometimes the last few days of the year are considered to fall in
201 // the *first* week of the following year. Refer to the Javadocs for
202 // GregorianCalendar.
203 int tempWeek = calendar.get(Calendar.WEEK_OF_YEAR);
204 if (tempWeek == 1
205 && calendar.get(Calendar.MONTH) == Calendar.DECEMBER) {
206 this.week = 1;
207 this.year = (short) (calendar.get(Calendar.YEAR) + 1);
208 }
209 else {
210 this.week = (byte) Math.min(tempWeek, LAST_WEEK_IN_YEAR);
211 int yyyy = calendar.get(Calendar.YEAR);
212 // alternatively, sometimes the first few days of the year are
213 // considered to fall in the *last* week of the previous year...
214 if (calendar.get(Calendar.MONTH) == Calendar.JANUARY
215 && this.week >= 52) {
216 yyyy--;
217 }
218 this.year = (short) yyyy;
219 }
220 peg(calendar);
221 }
222
223 /**
224 * Returns the year in which the week falls.
225 *
226 * @return The year (never <code>null</code>).
227 */
228 public Year getYear() {
229 return new Year(this.year);
230 }
231
232 /**
233 * Returns the year in which the week falls, as an integer value.
234 *
235 * @return The year.
236 */
237 public int getYearValue() {
238 return this.year;
239 }
240
241 /**
242 * Returns the week.
243 *
244 * @return The week.
245 */
246 public int getWeek() {
247 return this.week;
248 }
249
250 /**
251 * Returns the first millisecond of the week. This will be determined
252 * relative to the time zone specified in the constructor, or in the
253 * calendar instance passed in the most recent call to the
254 * {@link #peg(Calendar)} method.
255 *
256 * @return The first millisecond of the week.
257 *
258 * @see #getLastMillisecond()
259 */
260 public long getFirstMillisecond() {
261 return this.firstMillisecond;
262 }
263
264 /**
265 * Returns the last millisecond of the week. This will be
266 * determined relative to the time zone specified in the constructor, or
267 * in the calendar instance passed in the most recent call to the
268 * {@link #peg(Calendar)} method.
269 *
270 * @return The last millisecond of the week.
271 *
272 * @see #getFirstMillisecond()
273 */
274 public long getLastMillisecond() {
275 return this.lastMillisecond;
276 }
277
278 /**
279 * Recalculates the start date/time and end date/time for this time period
280 * relative to the supplied calendar (which incorporates a time zone).
281 *
282 * @param calendar the calendar (<code>null</code> not permitted).
283 *
284 * @since 1.0.3
285 */
286 public void peg(Calendar calendar) {
287 this.firstMillisecond = getFirstMillisecond(calendar);
288 this.lastMillisecond = getLastMillisecond(calendar);
289 }
290
291 /**
292 * Returns the week preceding this one. This method will return
293 * <code>null</code> for some lower limit on the range of weeks (currently
294 * week 1, 1900). For week 1 of any year, the previous week is always week
295 * 53, but week 53 may not contain any days (you should check for this).
296 *
297 * @return The preceding week (possibly <code>null</code>).
298 */
299 public RegularTimePeriod previous() {
300
301 Week result;
302 if (this.week != FIRST_WEEK_IN_YEAR) {
303 result = new Week(this.week - 1, this.year);
304 }
305 else {
306 // we need to work out if the previous year has 52 or 53 weeks...
307 if (this.year > 1900) {
308 int yy = this.year - 1;
309 Calendar prevYearCalendar = Calendar.getInstance();
310 prevYearCalendar.set(yy, Calendar.DECEMBER, 31);
311 result = new Week(prevYearCalendar.getActualMaximum(
312 Calendar.WEEK_OF_YEAR), yy);
313 }
314 else {
315 result = null;
316 }
317 }
318 return result;
319
320 }
321
322 /**
323 * Returns the week following this one. This method will return
324 * <code>null</code> for some upper limit on the range of weeks (currently
325 * week 53, 9999). For week 52 of any year, the following week is always
326 * week 53, but week 53 may not contain any days (you should check for
327 * this).
328 *
329 * @return The following week (possibly <code>null</code>).
330 */
331 public RegularTimePeriod next() {
332
333 Week result;
334 if (this.week < 52) {
335 result = new Week(this.week + 1, this.year);
336 }
337 else {
338 Calendar calendar = Calendar.getInstance();
339 calendar.set(this.year, Calendar.DECEMBER, 31);
340 int actualMaxWeek
341 = calendar.getActualMaximum(Calendar.WEEK_OF_YEAR);
342 if (this.week < actualMaxWeek) {
343 result = new Week(this.week + 1, this.year);
344 }
345 else {
346 if (this.year < 9999) {
347 result = new Week(FIRST_WEEK_IN_YEAR, this.year + 1);
348 }
349 else {
350 result = null;
351 }
352 }
353 }
354 return result;
355
356 }
357
358 /**
359 * Returns a serial index number for the week.
360 *
361 * @return The serial index number.
362 */
363 public long getSerialIndex() {
364 return this.year * 53L + this.week;
365 }
366
367 /**
368 * Returns the first millisecond of the week, evaluated using the supplied
369 * calendar (which determines the time zone).
370 *
371 * @param calendar the calendar (<code>null</code> not permitted).
372 *
373 * @return The first millisecond of the week.
374 *
375 * @throws NullPointerException if <code>calendar</code> is
376 * <code>null</code>.
377 */
378 public long getFirstMillisecond(Calendar calendar) {
379 Calendar c = (Calendar) calendar.clone();
380 c.clear();
381 c.set(Calendar.YEAR, this.year);
382 c.set(Calendar.WEEK_OF_YEAR, this.week);
383 c.set(Calendar.DAY_OF_WEEK, c.getFirstDayOfWeek());
384 c.set(Calendar.HOUR, 0);
385 c.set(Calendar.MINUTE, 0);
386 c.set(Calendar.SECOND, 0);
387 c.set(Calendar.MILLISECOND, 0);
388 //return c.getTimeInMillis(); // this won't work for JDK 1.3
389 return c.getTime().getTime();
390 }
391
392 /**
393 * Returns the last millisecond of the week, evaluated using the supplied
394 * calendar (which determines the time zone).
395 *
396 * @param calendar the calendar (<code>null</code> not permitted).
397 *
398 * @return The last millisecond of the week.
399 *
400 * @throws NullPointerException if <code>calendar</code> is
401 * <code>null</code>.
402 */
403 public long getLastMillisecond(Calendar calendar) {
404 Calendar c = (Calendar) calendar.clone();
405 c.clear();
406 c.set(Calendar.YEAR, this.year);
407 c.set(Calendar.WEEK_OF_YEAR, this.week + 1);
408 c.set(Calendar.DAY_OF_WEEK, c.getFirstDayOfWeek());
409 c.set(Calendar.HOUR, 0);
410 c.set(Calendar.MINUTE, 0);
411 c.set(Calendar.SECOND, 0);
412 c.set(Calendar.MILLISECOND, 0);
413 //return c.getTimeInMillis(); // this won't work for JDK 1.3
414 return c.getTime().getTime() - 1;
415 }
416
417 /**
418 * Returns a string representing the week (e.g. "Week 9, 2002").
419 *
420 * TODO: look at internationalisation.
421 *
422 * @return A string representing the week.
423 */
424 public String toString() {
425 return "Week " + this.week + ", " + this.year;
426 }
427
428 /**
429 * Tests the equality of this Week object to an arbitrary object. Returns
430 * true if the target is a Week instance representing the same week as this
431 * object. In all other cases, returns false.
432 *
433 * @param obj the object (<code>null</code> permitted).
434 *
435 * @return <code>true</code> if week and year of this and object are the
436 * same.
437 */
438 public boolean equals(Object obj) {
439
440 if (obj == this) {
441 return true;
442 }
443 if (!(obj instanceof Week)) {
444 return false;
445 }
446 Week that = (Week) obj;
447 if (this.week != that.week) {
448 return false;
449 }
450 if (this.year != that.year) {
451 return false;
452 }
453 return true;
454
455 }
456
457 /**
458 * Returns a hash code for this object instance. The approach described by
459 * Joshua Bloch in "Effective Java" has been used here:
460 * <p>
461 * <code>http://developer.java.sun.com/developer/Books/effectivejava
462 * /Chapter3.pdf</code>
463 *
464 * @return A hash code.
465 */
466 public int hashCode() {
467 int result = 17;
468 result = 37 * result + this.week;
469 result = 37 * result + this.year;
470 return result;
471 }
472
473 /**
474 * Returns an integer indicating the order of this Week object relative to
475 * the specified object:
476 *
477 * negative == before, zero == same, positive == after.
478 *
479 * @param o1 the object to compare.
480 *
481 * @return negative == before, zero == same, positive == after.
482 */
483 public int compareTo(Object o1) {
484
485 int result;
486
487 // CASE 1 : Comparing to another Week object
488 // --------------------------------------------
489 if (o1 instanceof Week) {
490 Week w = (Week) o1;
491 result = this.year - w.getYear().getYear();
492 if (result == 0) {
493 result = this.week - w.getWeek();
494 }
495 }
496
497 // CASE 2 : Comparing to another TimePeriod object
498 // -----------------------------------------------
499 else if (o1 instanceof RegularTimePeriod) {
500 // more difficult case - evaluate later...
501 result = 0;
502 }
503
504 // CASE 3 : Comparing to a non-TimePeriod object
505 // ---------------------------------------------
506 else {
507 // consider time periods to be ordered after general objects
508 result = 1;
509 }
510
511 return result;
512
513 }
514
515 /**
516 * Parses the string argument as a week.
517 * <P>
518 * This method is required to accept the format "YYYY-Wnn". It will also
519 * accept "Wnn-YYYY". Anything else, at the moment, is a bonus.
520 *
521 * @param s string to parse.
522 *
523 * @return <code>null</code> if the string is not parseable, the week
524 * otherwise.
525 */
526 public static Week parseWeek(String s) {
527
528 Week result = null;
529 if (s != null) {
530
531 // trim whitespace from either end of the string
532 s = s.trim();
533
534 int i = Week.findSeparator(s);
535 if (i != -1) {
536 String s1 = s.substring(0, i).trim();
537 String s2 = s.substring(i + 1, s.length()).trim();
538
539 Year y = Week.evaluateAsYear(s1);
540 int w;
541 if (y != null) {
542 w = Week.stringToWeek(s2);
543 if (w == -1) {
544 throw new TimePeriodFormatException(
545 "Can't evaluate the week.");
546 }
547 result = new Week(w, y);
548 }
549 else {
550 y = Week.evaluateAsYear(s2);
551 if (y != null) {
552 w = Week.stringToWeek(s1);
553 if (w == -1) {
554 throw new TimePeriodFormatException(
555 "Can't evaluate the week.");
556 }
557 result = new Week(w, y);
558 }
559 else {
560 throw new TimePeriodFormatException(
561 "Can't evaluate the year.");
562 }
563 }
564
565 }
566 else {
567 throw new TimePeriodFormatException(
568 "Could not find separator.");
569 }
570
571 }
572 return result;
573
574 }
575
576 /**
577 * Finds the first occurrence of ' ', '-', ',' or '.'
578 *
579 * @param s the string to parse.
580 *
581 * @return <code>-1</code> if none of the characters was found, the
582 * index of the first occurrence otherwise.
583 */
584 private static int findSeparator(String s) {
585
586 int result = s.indexOf('-');
587 if (result == -1) {
588 result = s.indexOf(',');
589 }
590 if (result == -1) {
591 result = s.indexOf(' ');
592 }
593 if (result == -1) {
594 result = s.indexOf('.');
595 }
596 return result;
597 }
598
599 /**
600 * Creates a year from a string, or returns null (format exceptions
601 * suppressed).
602 *
603 * @param s string to parse.
604 *
605 * @return <code>null</code> if the string is not parseable, the year
606 * otherwise.
607 */
608 private static Year evaluateAsYear(String s) {
609
610 Year result = null;
611 try {
612 result = Year.parseYear(s);
613 }
614 catch (TimePeriodFormatException e) {
615 // suppress
616 }
617 return result;
618
619 }
620
621 /**
622 * Converts a string to a week.
623 *
624 * @param s the string to parse.
625 * @return <code>-1</code> if the string does not contain a week number,
626 * the number of the week otherwise.
627 */
628 private static int stringToWeek(String s) {
629
630 int result = -1;
631 s = s.replace('W', ' ');
632 s = s.trim();
633 try {
634 result = Integer.parseInt(s);
635 if ((result < 1) || (result > LAST_WEEK_IN_YEAR)) {
636 result = -1;
637 }
638 }
639 catch (NumberFormatException e) {
640 // suppress
641 }
642 return result;
643
644 }
645
646 }