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