001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2005, 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-2004, 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.1 2005/10/25 21:35:24 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     *
063     */
064    
065    package org.jfree.data.time;
066    
067    import java.io.Serializable;
068    import java.util.Calendar;
069    import java.util.Date;
070    import java.util.TimeZone;
071    
072    /**
073     * A calendar week.  All years are considered to have 53 weeks, numbered from 1 
074     * to 53, although in many cases the 53rd week is empty.  Most of the time, the
075     * 1st week of the year *begins* in the previous calendar year, but it always 
076     * finishes in the current year (this behaviour matches the workings of the 
077     * <code>GregorianCalendar</code> class).
078     * <P>
079     * This class is immutable, which is a requirement for all 
080     * {@link RegularTimePeriod} subclasses.
081     */
082    public class Week extends RegularTimePeriod implements Serializable {
083    
084        /** For serialization. */
085        private static final long serialVersionUID = 1856387786939865061L;
086        
087        /** Constant for the first week in the year. */
088        public static final int FIRST_WEEK_IN_YEAR = 1;
089    
090        /** Constant for the last week in the year. */
091        public static final int LAST_WEEK_IN_YEAR = 53;
092    
093        /** The year in which the week falls. */
094        private Year year;
095    
096        /** The week (1-53). */
097        private int week;
098    
099        /**
100         * Creates a new time period for the week in which the current system 
101         * date/time falls.
102         */
103        public Week() {
104            this(new Date());
105        }
106    
107        /**
108         * Creates a time period representing the week in the specified year.
109         *
110         * @param week  the week (1 to 53).
111         * @param year  the year (1900 to 9999).
112         */
113        public Week(int week, int year) {
114            this(week, new Year(year));
115        }
116    
117        /**
118         * Creates a time period representing the week in the specified year.
119         *
120         * @param week  the week (1 to 53).
121         * @param year  the year (1900 to 9999).
122         */
123        public Week(int week, Year year) {
124            if ((week < FIRST_WEEK_IN_YEAR) && (week > LAST_WEEK_IN_YEAR)) {
125                throw new IllegalArgumentException(
126                    "The 'week' argument must be in the range 1 - 53."
127                );
128            }
129            this.week = week;
130            this.year = year;
131        }
132    
133        /**
134         * Creates a time period for the week in which the specified date/time 
135         * falls.
136         *
137         * @param time  the time (<code>null</code> not permitted).
138         */
139        public Week(Date time) {
140            // defer argument checking...
141            this(time, RegularTimePeriod.DEFAULT_TIME_ZONE);
142        }
143    
144        /**
145         * Creates a time period for the week in which the specified date/time 
146         * falls, calculated relative to the specified time zone.
147         *
148         * @param time  the date/time (<code>null</code> not permitted).
149         * @param zone  the time zone (<code>null</code> not permitted).
150         */
151        public Week(Date time, TimeZone zone) {
152            if (time == null) {
153                throw new IllegalArgumentException("Null 'time' argument.");   
154            }
155            if (zone == null) {
156                throw new IllegalArgumentException("Null 'zone' argument.");   
157            }
158            Calendar calendar = Calendar.getInstance(zone);
159            calendar.setTime(time);
160    
161            // sometimes the last few days of the year are considered to fall in 
162            // the *first* week of the following year.  Refer to the Javadocs for 
163            // GregorianCalendar.
164            int tempWeek = calendar.get(Calendar.WEEK_OF_YEAR);
165            if (tempWeek == 1 
166                    && calendar.get(Calendar.MONTH) == Calendar.DECEMBER) {
167                this.week = 1;
168                this.year =  new Year(calendar.get(Calendar.YEAR) + 1);
169            }
170            else {
171                this.week = Math.min(tempWeek, LAST_WEEK_IN_YEAR);
172                this.year = new Year(calendar.get(Calendar.YEAR));
173            }
174    
175        }
176    
177        /**
178         * Returns the year in which the week falls.
179         *
180         * @return The year (never <code>null</code>).
181         */
182        public Year getYear() {
183            return this.year;
184        }
185    
186        /**
187         * Returns the year in which the week falls, as an integer value.
188         *
189         * @return The year.
190         */
191        public int getYearValue() {
192            return this.year.getYear();
193        }
194    
195        /**
196         * Returns the week.
197         *
198         * @return The week.
199         */
200        public int getWeek() {
201            return this.week;
202        }
203    
204        /**
205         * Returns the week preceding this one.  This method will return 
206         * <code>null</code> for some lower limit on the range of weeks (currently 
207         * week 1, 1900).  For week 1 of any year, the previous week is always week 
208         * 53, but week 53 may not contain any days (you should check for this).
209         *
210         * @return The preceding week (possibly <code>null</code>).
211         */
212        public RegularTimePeriod previous() {
213    
214            Week result;
215            if (this.week != FIRST_WEEK_IN_YEAR) {
216                result = new Week(this.week - 1, this.year);
217            }
218            else {
219                // we need to work out if the previous year has 52 or 53 weeks...
220                Year prevYear = (Year) this.year.previous();
221                if (prevYear != null) {
222                    int yy = prevYear.getYear();
223                    Calendar prevYearCalendar = Calendar.getInstance();
224                    prevYearCalendar.set(yy, Calendar.DECEMBER, 31);
225                    result = new Week(
226                        prevYearCalendar.getActualMaximum(Calendar.WEEK_OF_YEAR), 
227                        prevYear
228                    );
229                }
230                else {
231                    result = null;
232                }
233            }
234            return result;
235    
236        }
237    
238        /**
239         * Returns the week following this one.  This method will return 
240         * <code>null</code> for some upper limit on the range of weeks (currently 
241         * week 53, 9999).  For week 52 of any year, the following week is always 
242         * week 53, but week 53 may not contain any days (you should check for 
243         * this).
244         *
245         * @return The following week (possibly <code>null</code>).
246         */
247        public RegularTimePeriod next() {
248    
249            Week result;
250            if (this.week < 52) {
251                result = new Week(this.week + 1, this.year);
252            }
253            else {
254                Calendar calendar = Calendar.getInstance();
255                calendar.set(this.year.getYear(), Calendar.DECEMBER, 31);
256                int actualMaxWeek 
257                    = calendar.getActualMaximum(Calendar.WEEK_OF_YEAR);
258                if (this.week != actualMaxWeek) {
259                    result = new Week(this.week + 1, this.year);
260                }
261                else {
262                    Year nextYear = (Year) this.year.next();
263                    if (nextYear != null) {
264                        result = new Week(FIRST_WEEK_IN_YEAR, nextYear);
265                    }
266                    else {
267                        result = null;
268                    }
269                }
270            }
271            return result;
272    
273        }
274    
275        /**
276         * Returns a serial index number for the week.
277         *
278         * @return The serial index number.
279         */
280        public long getSerialIndex() {
281            return this.year.getYear() * 53L + this.week;
282        }
283    
284        /**
285         * Returns the first millisecond of the week, evaluated using the supplied
286         * calendar (which determines the time zone).
287         *
288         * @param calendar  the calendar.
289         *
290         * @return The first millisecond of the week.
291         */
292        public long getFirstMillisecond(Calendar calendar) {
293            Calendar c = (Calendar) calendar.clone();
294            c.clear();
295            c.set(Calendar.YEAR, this.year.getYear());
296            c.set(Calendar.WEEK_OF_YEAR, this.week);
297            c.set(Calendar.DAY_OF_WEEK, c.getFirstDayOfWeek());
298            c.set(Calendar.HOUR, 0);
299            c.set(Calendar.MINUTE, 0);
300            c.set(Calendar.SECOND, 0);
301            c.set(Calendar.MILLISECOND, 0);
302            //return c.getTimeInMillis();  // this won't work for JDK 1.3
303            return c.getTime().getTime();
304        }
305    
306        /**
307         * Returns the last millisecond of the week, evaluated using the supplied
308         * calendar (which determines the time zone).
309         *
310         * @param calendar  the calendar.
311         *
312         * @return The last millisecond of the week.
313         */
314        public long getLastMillisecond(Calendar calendar) {
315            RegularTimePeriod next = next();
316            return next.getFirstMillisecond(calendar) - 1;
317        }
318    
319        /**
320         * Returns a string representing the week (e.g. "Week 9, 2002").
321         *
322         * TODO: look at internationalisation.
323         *
324         * @return A string representing the week.
325         */
326        public String toString() {
327            return "Week " + this.week + ", " + this.year;
328        }
329    
330        /**
331         * Tests the equality of this Week object to an arbitrary object.  Returns
332         * true if the target is a Week instance representing the same week as this
333         * object.  In all other cases, returns false.
334         * @param obj The object.
335         *
336         * @return <code>true</code> if week and year of this and object are the 
337         *         same.
338         */
339        public boolean equals(Object obj) {
340    
341            if (obj == this) {
342                return true;
343            }
344            if (!(obj instanceof Week)) {
345                return false;
346            }
347            Week that = (Week) obj;
348            if (this.week != that.week) {
349                return false;
350            }
351            if (!this.year.equals(that.year)) {
352                return false;
353            }
354            return true;
355    
356        }
357    
358        /**
359         * Returns a hash code for this object instance.  The approach described by
360         * Joshua Bloch in "Effective Java" has been used here:
361         * <p>
362         * <code>http://developer.java.sun.com/developer/Books/effectivejava
363         * /Chapter3.pdf</code>
364         * 
365         * @return A hash code.
366         */
367        public int hashCode() {
368            int result = 17;
369            result = 37 * result + this.week;
370            result = 37 * result + this.year.hashCode();
371            return result;
372        }
373    
374        /**
375         * Returns an integer indicating the order of this Week object relative to
376         * the specified object:
377         *
378         * negative == before, zero == same, positive == after.
379         *
380         * @param o1  the object to compare.
381         *
382         * @return negative == before, zero == same, positive == after.
383         */
384        public int compareTo(Object o1) {
385    
386            int result;
387    
388            // CASE 1 : Comparing to another Week object
389            // --------------------------------------------
390            if (o1 instanceof Week) {
391                Week w = (Week) o1;
392                result = this.year.getYear() - w.getYear().getYear();
393                if (result == 0) {
394                    result = this.week - w.getWeek();
395                }
396            }
397    
398            // CASE 2 : Comparing to another TimePeriod object
399            // -----------------------------------------------
400            else if (o1 instanceof RegularTimePeriod) {
401                // more difficult case - evaluate later...
402                result = 0;
403            }
404    
405            // CASE 3 : Comparing to a non-TimePeriod object
406            // ---------------------------------------------
407            else {
408                // consider time periods to be ordered after general objects
409                result = 1;
410            }
411    
412            return result;
413    
414        }
415    
416        /**
417         * Parses the string argument as a week.
418         * <P>
419         * This method is required to accept the format "YYYY-Wnn".  It will also
420         * accept "Wnn-YYYY". Anything else, at the moment, is a bonus.
421         *
422         * @param s  string to parse.
423         *
424         * @return <code>null</code> if the string is not parseable, the week 
425         *         otherwise.
426         */
427        public static Week parseWeek(String s) {
428    
429            Week result = null;
430            if (s != null) {
431    
432                // trim whitespace from either end of the string
433                s = s.trim();
434    
435                int i = Week.findSeparator(s);
436                if (i != -1) {
437                    String s1 = s.substring(0, i).trim();
438                    String s2 = s.substring(i + 1, s.length()).trim();
439    
440                    Year y = Week.evaluateAsYear(s1);
441                    int w;
442                    if (y != null) {
443                        w = Week.stringToWeek(s2);
444                        if (w == -1) {
445                            throw new TimePeriodFormatException(
446                                "Can't evaluate the week."
447                            );
448                        }
449                        result = new Week(w, y);
450                    }
451                    else {
452                        y = Week.evaluateAsYear(s2);
453                        if (y != null) {
454                            w = Week.stringToWeek(s1);
455                            if (w == -1) {
456                                throw new TimePeriodFormatException(
457                                    "Can't evaluate the week."
458                                );
459                            }
460                            result = new Week(w, y);
461                        }
462                        else {
463                            throw new TimePeriodFormatException(
464                                "Can't evaluate the year."
465                            );
466                        }
467                    }
468    
469                }
470                else {
471                    throw new TimePeriodFormatException(
472                        "Could not find separator."
473                    );
474                }
475    
476            }
477            return result;
478    
479        }
480    
481        /**
482         * Finds the first occurrence of ' ', '-', ',' or '.'
483         *
484         * @param s  the string to parse.
485         *
486         * @return <code>-1</code> if none of the characters was found, the
487         *      index of the first occurrence otherwise.
488         */
489        private static int findSeparator(String s) {
490    
491            int result = s.indexOf('-');
492            if (result == -1) {
493                result = s.indexOf(',');
494            }
495            if (result == -1) {
496                result = s.indexOf(' ');
497            }
498            if (result == -1) {
499                result = s.indexOf('.');
500            }
501            return result;
502        }
503    
504        /**
505         * Creates a year from a string, or returns null (format exceptions
506         * suppressed).
507         *
508         * @param s  string to parse.
509         *
510         * @return <code>null</code> if the string is not parseable, the year 
511         *         otherwise.
512         */
513        private static Year evaluateAsYear(String s) {
514    
515            Year result = null;
516            try {
517                result = Year.parseYear(s);
518            }
519            catch (TimePeriodFormatException e) {
520                // suppress
521            }
522            return result;
523    
524        }
525    
526        /**
527         * Converts a string to a week.
528         *
529         * @param s  the string to parse.
530         * @return <code>-1</code> if the string does not contain a week number,
531         *         the number of the week otherwise.
532         */
533        private static int stringToWeek(String s) {
534    
535            int result = -1;
536            s = s.replace('W', ' ');
537            s = s.trim();
538            try {
539                result = Integer.parseInt(s);
540                if ((result < 1) || (result > LAST_WEEK_IN_YEAR)) {
541                    result = -1;
542                }
543            }
544            catch (NumberFormatException e) {
545                // suppress
546            }
547            return result;
548    
549        }
550        
551    }