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    }